Public: anyone can read this; the crew can change it.

An agent asking `forges` sees each child's size and usage, as the machines page shows them #10957

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/10903-an-agent-asking-forges-sees-each-child-s → main 3 commits fork

  • Merged. scout/10903-an-agent-asking-forges-sees-each-child-s was merged into main as 8dfb6b6.

Review

Sign in to review this change.
3 files changed +73 −3
docs/MCP.md +1 −1
@@ -111,7 +111,7 @@ bytes in `tools/list`. After changing a tool run `bun scripts/mcp-table.ts`;
111111 | `following` | — | Where a fork stands against what it follows: in step, behind, ahead, diverged | 243 |
112112 | `forge_proposals` | — | What this forge proposed to its parent; `reachable` false if it is silent | 245 |
113113 | `forge_view` | — | The forge as one picture: counts, apps, runs with stations, health | 189 |
114 | `forges` | — | Forges sent from here (admin): name, address, apps, claimed, spendTodayUsd, ceilingUsd | 205 |
114 | `forges` | — | Forges sent from here (admin): name, address, apps, claimed, spendTodayUsd, ceilingUsd, size, use | 216 |
115115 | `fork` | — | Fork a repository into your account; for a guest, a copy to send changes back from | 232 |
116116 | `fork_tree` | — | The fork tree around a repo: ancestors, siblings and descendants you may read | 347 |
117117 | `forks` | — | A repo's forks you may see, paged by limit and cursor (next_cursor), each with commits ahead and behind | 345 |
src/http/mcp/tools/forges.ts +2 −2
@@ -3,9 +3,9 @@ import { good, bad, type Tool } from "../shared.ts";
33
44 export const tool: Tool = {
55 name: "forges",
6 doc: "an admin's list of the forges sent from here: name, address, whom for, apps, claimed or not, last activity, whether each public address answers",
6 doc: "an admin's list of the forges sent from here: name, address, whom for, apps, claimed, last activity, reachable, size, use",
77 description:
8 "Forges sent from here (admin): name, address, apps, claimed, spendTodayUsd, ceilingUsd.",
8 "Forges sent from here (admin): name, address, apps, claimed, spendTodayUsd, ceilingUsd, size, use.",
99 inputSchema: { type: "object", properties: {} },
1010 writes: false,
1111 async run(ctx, user) {
test/mcp-forges-usage.test.ts added +70
@@ -0,0 +1,70 @@
1 /**
2 * The MCP `forges` tool carries each child's size and use (#10903), from the
3 * same `forgeUse` the machines page reads: a hosted child is "shared", a cell
4 * child that is not running is "idle" with its limits; a plain admin is refused.
5 */
6 import { afterAll, beforeAll, expect, test } from "bun:test";
7 import { mkdir, mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { createApp, type App } from "../src/app.ts";
11 import { createToken } from "../src/core/tokens.ts";
12 import { createUser } from "../src/core/users.ts";
13 import { serve, type Servers } from "../src/http/server.ts";
14 import { SIZES } from "../src/spawn/sizes.ts";
15 import { quietLogs } from "./helpers/globals.ts";
16
17 quietLogs();
18
19 let dir: string;
20 let app: App;
21 let servers: Servers;
22 let token: string;
23 let rest: any;
24
25 const forges = async (tok: string) => {
26 const res = await fetch(`${servers.url}/mcp`, {
27 method: "POST",
28 headers: { "content-type": "application/json", accept: "application/json, text/event-stream", authorization: `Bearer ${tok}` },
29 body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "forges", arguments: {} } }),
30 });
31 const j = (await res.json()) as any;
32 return { error: j.result.isError === true, body: JSON.parse(String(j.result.content[0].text)) };
33 };
34
35 beforeAll(async () => {
36 dir = await mkdtemp(join(tmpdir(), "forge-mcp-forges-usage-"));
37 app = createApp({ dataDir: dir, port: 0 });
38 servers = await serve(app);
39 const made = await createUser(app.db, { origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true });
40 if (!made.ok) throw new Error(JSON.stringify(made.error));
41 token = createToken(app.db, made.value.id, "rw", "rw").token;
42 await mkdir(join(dir, "spawn"), { recursive: true });
43 const ledger = (name: string, extra: object) =>
44 Bun.write(join(dir, "spawn", `${name}.json`), JSON.stringify({ name, bodyId: `b-${name}`, url: "", planHash: "x", done: true, forWhom: "Client", apps: [], createdAt: new Date().toISOString(), ...extra }));
45 await ledger("hosted-kid", { size: "small", placement: "hosted" });
46 await ledger("cell-kid", { size: "medium", placement: "cell" });
47 const r = await fetch(`${servers.url}/api/v1/forges`, { headers: { authorization: `Bearer ${token}` } });
48 rest = await r.json();
49 });
50 afterAll(async () => {
51 servers.stop();
52 app.close();
53 await rm(dir, { recursive: true, force: true });
54 });
55
56 test("each forge item carries its size and use", async () => {
57 const { error, body } = await forges(token);
58 expect(error).toBe(false);
59 const by = Object.fromEntries(body.forges.map((f: any) => [f.name, f]));
60 expect(by["hosted-kid"].size).toBe("small");
61 expect(by["hosted-kid"].use).toEqual({ kind: "shared" });
62 expect(by["cell-kid"].size).toBe("medium");
63 expect(by["cell-kid"].use).toEqual({ kind: "idle", limits: SIZES.medium });
64 });
65
66 test("the REST route the tool mirrors answers the same size and use", () => {
67 const by = Object.fromEntries(rest.items.map((f: any) => [f.name, f]));
68 expect(by["cell-kid"].size).toBe("medium");
69 expect(by["cell-kid"].use).toEqual({ kind: "idle", limits: SIZES.medium });
70 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs