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

The terminal's System screen grants an agent telemetry:read or takes it back with `g` and a confirm, as MCP `telemetry_grant` does #7361

Merged opened this

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

Merged

scout/forge:scout/7348-the-terminal-s-system-screen-grants-an-a → main 3 commits fork 2 threads · 2 unresolved

  • Merged. scout/7348-the-terminal-s-system-screen-grants-an-a was merged into main as 990496e.

Review

Sign in to review this change.
4 files changed +122 −8
README.md +2
@@ -54,6 +54,8 @@ A hand-over card on `/asks` (an agent's pull the reviewer stopped on after three
5454
5555 Home opens with "Since you were here · 2 days": the pulls that landed in public repositories since you last loaded `/` (1 hour to 7 days back, a day on a first visit); the terminal's home shows the same lines, and MCP `feed` answers them as `digest`. See `docs/features/since-you-were-here.md`.
5656
57 In the terminal, the System screen (palette: System) shows a forge admin who is a person one more line, `telemetry:read held by: …` (the agents holding the grant), and `g` there picks an agent and asks `grant <agent> telemetry:read? y/n` (or `take back …`; the default is n). `y` calls `setTelemetryGrant`, as MCP `telemetry_grant` does; anyone that tool refuses sees neither the line nor the key hint, and `g` answers them in the tool's words.
58
5759 An admin's home and the terminal's Flow dialog also say when the forge is clogged and how long the last clog took to notice and to clear: `docs/features/clog-watch.md`.
5860
5961 A pull the merge queue holds as red alone (red on its own merge at its current head; cleared by a new head or a moved main) says so under its checks line: "red alone at <time>, waiting for a new head or a new main", on the pull page, its live checks line and the studio change step, and in the checks row of the pull's terminal screen (`redMark` in `src/checks/queue.ts`). Nothing is drawn when it is not held.
src/tui/forge.ts +1 −1
@@ -325,7 +325,7 @@ export function forgeCommands(session: Session): (ctx: Ctx) => Command[] {
325325 ...forgeLines(readHealth(session.app, session.user)),
326326 "",
327327 ...systemLines(liveMachine().read()),
328 ])),
328 ], session)),
329329 });
330330 commands.push({
331331 id: "telemetry",
src/tui/screens/system.ts +55 −6
@@ -24,11 +24,13 @@ import { rolloutLine, type ForgeHealth } from "../../kernel/guard.ts";
2424 import { canSeeMemory, memoryHours } from "../../boards/memory.ts";
2525 import { binding } from "../app/keys.ts";
2626 import type { Session } from "../session.ts";
27 import { setTelemetryGrant } from "../../actions/telemetry.ts";
28 import { grantable } from "../../core/telemetry.ts";
2729 import { bytes, spark } from "../../system/format.ts";
2830 import { style } from "../term/style.ts";
2931 import { pad, truncate, wrap } from "../term/width.ts";
3032 import { dialog } from "../ui/widgets.ts";
31 import { Scroll, drawLines, hitScroll } from "./common.ts";
33 import { Scroll, confirmScreen, drawLines, hitScroll, later, pickerScreen } from "./common.ts";
3234
3335 const WIDTH = 72;
3436 const GROUPS = 6;
@@ -101,14 +103,59 @@ export function systemLines(v: SystemView): string[] {
101103 ];
102104 }
103105
104 export function systemScreen(ctx: Ctx, lines: readonly string[]): Screen {
105 return {
106 /** Who may grant telemetry:read: the MCP tool's gate (`mayBoards`), a forge admin who is a person. */
107 const mayGrant = (session: Session | undefined): boolean => session?.user?.isAdmin === true && session.user.kind !== "agent";
108
109 /** The agents holding the grant, in one line. */
110 export function grantLine(session: Session): string {
111 const held = grantable(session.db).filter((a) => a.granted).map((a) => a.username);
112 return held.length === 0 ? "telemetry:read held by: no agent" : `telemetry:read held by: ${held.join(", ")}`;
113 }
114
115 /** The words for a refused grant, as MCP `telemetry_grant` says them. */
116 const grantRefused = (error: "not_found" | "not_an_agent"): string =>
117 error === "not_an_agent" ? "Only an agent account can be granted telemetry:read." : "no such account";
118
119 export function systemScreen(ctx: Ctx, lines: readonly string[], session?: Session): Screen {
120 const offered = session !== undefined && mayGrant(session);
121 const askGrant = (): void => {
122 if (!session) return;
123 if (!mayGrant(session)) return ctx.toast("telemetry:read is a forge admin's to grant", "danger");
124 const agents = grantable(session.db);
125 if (agents.length === 0) return ctx.toast("No agent accounts yet");
126 later(ctx, () =>
127 pickerScreen(ctx, screen, {
128 title: "Grant telemetry:read to",
129 items: agents.map((a) => ({ id: a.username, label: a.username, hint: a.granted ? "granted" : "not granted" })),
130 onPick: ([name]) => {
131 const target = agents.find((a) => a.username === name);
132 if (!target) return;
133 const on = !target.granted;
134 later(ctx, () =>
135 confirmScreen(ctx, screen, {
136 title: "Telemetry",
137 body: [on ? `grant ${target.username} telemetry:read? y/n` : `take back ${target.username} telemetry:read? y/n`],
138 actions: [{ label: "y" }, { label: "n" }],
139 selected: 1,
140 onChoose: (choice) => {
141 if (choice !== 0) return;
142 const set = setTelemetryGrant(session.app, session.user, target.username, on);
143 if (!set.ok) return ctx.toast(grantRefused(set.error), "danger");
144 ctx.toast(`${target.username} ${on ? "may now read" : "no longer reads"} telemetry.`);
145 },
146 }),
147 );
148 },
149 }),
150 );
151 };
152 const screen: Screen = {
106153 title: "System",
107 keys: [binding(["enter", "esc", "q"], "close")],
154 keys: [...(offered ? [binding(["g"], "grant or take back telemetry")] : []), binding(["enter", "esc", "q"], "close")],
108155 render(area, s) {
109156 const buttons = dialog(s, area, ctx.theme.c, {
110157 title: "System",
111 body: lines,
158 body: offered ? [...lines, "", grantLine(session), "g: grant or take back telemetry:read"] : lines,
112159 actions: [{ label: "Close" }],
113160 selected: 0,
114161 width: WIDTH,
@@ -116,10 +163,12 @@ export function systemScreen(ctx: Ctx, lines: readonly string[]): Screen {
116163 buttons.forEach((rect) => ctx.hit(rect, () => ctx.pop()));
117164 },
118165 key(e) {
119 if (e.key === "enter" || e.key === "esc" || e.key === "q") ctx.pop();
166 if (e.key === "g" && session) askGrant();
167 else if (e.key === "enter" || e.key === "esc" || e.key === "q") ctx.pop();
120168 return true;
121169 },
122170 };
171 return screen;
123172 }
124173
125174 const mib = (n: number) => `${(n / 1_048_576).toFixed(1)}`;
test/tui-system.test.ts +64 −1
@@ -13,7 +13,9 @@ import { createApp, type App } from "../src/app.ts";
1313 import { recordMemory } from "../src/boards/memory.ts";
1414 import { createUser, type User } from "../src/core/users.ts";
1515 import { createForgeApp, forgeCommands, Session } from "../src/tui/forge.ts";
16 import { forgeLines, memoryLines, memoryScreen } from "../src/tui/screens/system.ts";
16 import { setTelemetryGrant } from "../src/actions/telemetry.ts";
17 import { hasGrant } from "../src/core/telemetry.ts";
18 import { forgeLines, memoryLines, memoryScreen, systemScreen } from "../src/tui/screens/system.ts";
1719 import { headlessTerminal } from "../src/tui/term/terminal.ts";
1820 import { quietLogs } from "./helpers/globals.ts";
1921 import { settleApp } from "./helpers/tui.ts";
@@ -101,6 +103,67 @@ describe("tui system memory", () => {
101103 });
102104 });
103105
106 describe("tui system telemetry grant", () => {
107 const boot = (user: User) => {
108 const { app: tui, session } = createForgeApp(app, identity(user), { terminal: headlessTerminal(120, 40), exitHooks: false });
109 tui.start();
110 tui.ctx.push(systemScreen(tui.ctx, ["CPU"], session));
111 const press = async (...keys: string[]) => {
112 for (const key of keys) tui.dispatch({ type: "key", key, ctrl: false, alt: false, shift: false });
113 await settleApp(tui);
114 };
115 return { tui, press };
116 };
117
118 test("an admin grants and takes back after a confirm", async () => {
119 const h = boot(carol);
120 await settleApp(h.tui);
121 expect(h.tui.screen.toText()).toContain("telemetry:read held by: no agent");
122 await h.press("g");
123 await h.press("enter");
124 expect(h.tui.screen.toText()).toContain("grant sysbot telemetry:read? y/n");
125 await h.press("left", "enter");
126 expect(hasGrant(app.db, bot.id)).toBe(true);
127 expect(h.tui.screen.toText()).toContain("telemetry:read held by: sysbot");
128 await h.press("g");
129 await h.press("enter");
130 expect(h.tui.screen.toText()).toContain("take back sysbot telemetry:read? y/n");
131 await h.press("enter");
132 expect(hasGrant(app.db, bot.id)).toBe(true);
133 await h.press("g");
134 await h.press("enter");
135 await h.press("left", "enter");
136 expect(hasGrant(app.db, bot.id)).toBe(false);
137 h.tui.stop?.();
138 });
139
140 test("anyone the tool refuses is not offered g and is refused in its words", async () => {
141 const h = boot(bob);
outdated

Also assert that no picker or dialog opened after g (the screen text should still be the System screen), not only the toast.

142 await settleApp(h.tui);
143 expect(h.tui.screen.toText()).not.toContain("telemetry:read");
144 await h.press("g");
145 expect(h.tui.screen.toText()).toContain("telemetry:read is a forge admin's to grant");
146 expect(h.tui.screen.toText()).not.toContain("Grant telemetry:read to");
147 expect(h.tui.screen.toText()).toContain("CPU");
148 expect(setTelemetryGrant(app, bob, "sysbot", true)).toEqual({ ok: false, error: "not_found" });
149 expect(hasGrant(app.db, bot.id)).toBe(false);
150 h.tui.stop?.();
151 });
152
153 test("the System command passes the session, so the admin has the line and g", async () => {
154 const { app: tui, session } = createForgeApp(app, identity(carol), { terminal: headlessTerminal(120, 40), exitHooks: false });
155 tui.start();
156 const system = forgeCommands(session)(tui.ctx).find((c) => c.id === "system");
157 system!.run();
158 await settleApp(tui);
159 expect(tui.screen.toText()).toContain("telemetry:read held by:");
160 tui.dispatch({ type: "key", key: "g", ctrl: false, alt: false, shift: false });
161 await settleApp(tui);
162 expect(tui.screen.toText()).toContain("Grant telemetry:read to");
163 tui.stop?.();
164 });
165 });
166
104167 describe("tui system: the forge's own state (#7296)", () => {
105168 const SHA = "0123456789abcdef0123456789abcdef01234567";
106169 const saved = ["FORGE_GUARD_DIR", "FORGE_VERSION", "FORGE_COMMIT"].map((k) => [k, process.env[k]] as const);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs