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

The security machine's brief and environment name only the tools actually on PATH, and say which are missing #9509

Merged opened this

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

Merged

mason/forge:mason/9506-the-security-machine-s-brief-and-environ → main 1 commit fork 1 thread · 1 unresolved

  • Merged. mason/9506-the-security-machine-s-brief-and-environ was merged into main as 1b42197.

Review

Sign in to review this change.
3 files changed +80 −16
docs/features/machines.md +7 −2
@@ -42,7 +42,12 @@ cannot widen its own machine by editing the file on its branch):
4242 ## What a run gets
4343
4444 `FORGE_MACHINE` and `FORGE_MACHINE_TOOLS` (names only) in its environment, a
45 line in its brief naming the tools its machine carries, and its `apps` reach.
45 line in its brief, and its `apps` reach. Both report what is **installed**, not
46 what is declared (#9506): `FORGE_MACHINE_TOOLS` lists only the machine's tools
47 found on the forge's PATH (`Bun.which`, PATH only: nothing is downloaded or
48 run), and the brief names those with what each is for, then lists the rest as
49 "not installed on this forge". A machine with none of its tools on PATH gets
50 one line saying so. Installing the tools is a separate step, not done yet.
4651
4752 ## A shell into a machine (#9471)
4853
@@ -88,6 +93,6 @@ repository's machines:
8893 ## Not done yet (split from #9418)
8994
9095 Disk ceilings are not declared (#9460); the tools are declared, not yet
91 installed in an image; the browser terminal (ghostty-web) that speaks to the
96 installed in an image (the brief reports which are on PATH); the browser terminal (ghostty-web) that speaks to the
9297 shell socket is not built; the shell's cell does not reach the repository's
9398 apps even when its machine says `apps`.
src/crew/machines.ts +42 −8
@@ -205,20 +205,54 @@ export function machineLimits(machine: Machine): { memoryMiB?: number; cpus?: nu
205205 };
206206 }
207207
208 /** The run's environment for its machine: names only, nothing secret. */
209 export function machineEnv(machine: Machine): Record<string, string> {
208 /**
209 * Whether a tool is on PATH: its path, or null. `Bun.which` reads PATH only;
210 * nothing is downloaded or run. Injected in tests.
211 */
212 export type ToolLookup = (name: string) => string | null;
213
214 const onPath: ToolLookup = (name) => Bun.which(name);
215
216 /** A machine's tools split into those on PATH and those this forge lacks. */
217 export function installedTools(
218 machine: Machine,
219 lookup: ToolLookup = onPath,
220 ): { found: string[]; missing: string[] } {
221 const found: string[] = [];

Looks up the declared name only. zap ships as zap.sh, so it would be reported missing even when present. Next: map tool name to command name.

222 const missing: string[] = [];
223 for (const tool of machine.tools) (lookup(tool) === null ? missing : found).push(tool);
224 return { found, missing };
225 }
226
227 /** The run's environment for its machine: names only, nothing secret, and only tools on PATH. */
228 export function machineEnv(
229 machine: Machine,
230 lookup: ToolLookup = onPath,
231 ): Record<string, string> {
210232 return {
211233 FORGE_MACHINE: machine.name,
212 FORGE_MACHINE_TOOLS: machine.tools.join(","),
234 FORGE_MACHINE_TOOLS: installedTools(machine, lookup).found.join(","),
213235 };
214236 }
215237
216 /** One line for the brief: where the shell runs and what it was given. */
217 export function machineNote(machine: Machine): string | undefined {
238 /**
239 * One line for the brief: where the shell runs and which of its tools are
240 * on PATH. It never claims a tool that is absent; none for a machine
241 * without tools.
242 */
243 export function machineNote(
244 machine: Machine,
245 lookup: ToolLookup = onPath,
246 ): string | undefined {
218247 if (machine.tools.length === 0) return undefined;
248 const { found, missing } = installedTools(machine, lookup);
249 const where = `Your shell runs in the \`${machine.name}\` machine`;
250 if (found.length === 0)
251 return `${where}; none of its tools is installed on this forge (${missing.join(", ")}).`;
219252 return (
220 `Your shell runs in the \`${machine.name}\` machine. Tools it is meant to carry (use those on PATH; none is installed for you, and each may be aimed only at the forge's own copies): ` +
221 machine.tools.map((t) => `${t} (${TOOLS[t]})`).join("; ") +
222 "."
253 `${where}. Tools on PATH (aim each only at the forge's own copies): ` +
254 found.map((t) => `${t} (${TOOLS[t]})`).join("; ") +
255 "." +
256 (missing.length === 0 ? "" : ` Not installed on this forge: ${missing.join(", ")}.`)
223257 );
224258 }
test/crew-machines.test.ts +31 −6
@@ -80,16 +80,41 @@ describe("machines", () => {
8080 expect(reachableApps(closed, urls)).toEqual([]);
8181 });
8282
83 const everyTool = (name: string) => `/usr/bin/${name}`;
84 const gitleaksOnly = (name: string) => (name === "gitleaks" ? "/usr/bin/gitleaks" : null);
85 const noTool = () => null;
86
8387 test("a machine's environment carries names, never a secret", () => {
84 expect(machineEnv(SECURITY).FORGE_MACHINE).toBe("security");
85 expect(machineEnv(SECURITY).FORGE_MACHINE_TOOLS).toContain("gitleaks");
86 expect(Object.keys(machineEnv(SECURITY)).sort()).toEqual(["FORGE_MACHINE", "FORGE_MACHINE_TOOLS"]);
88 expect(machineEnv(SECURITY, everyTool).FORGE_MACHINE).toBe("security");
89 expect(machineEnv(SECURITY, everyTool).FORGE_MACHINE_TOOLS).toContain("gitleaks");
90 expect(Object.keys(machineEnv(SECURITY, everyTool)).sort()).toEqual(["FORGE_MACHINE", "FORGE_MACHINE_TOOLS"]);
8791 });
8892
8993 test("the brief names the tools a machine carries, and says nothing for the standard one", () => {
90 expect(machineNote(STANDARD)).toBeUndefined();
91 expect(machineNote(SECURITY)).toContain("gitleaks");
92 expect(machineNote(SECURITY)).toContain("forge's own copies");
94 expect(machineNote(STANDARD, everyTool)).toBeUndefined();
95 expect(machineNote(SECURITY, everyTool)).toContain("gitleaks");
96 expect(machineNote(SECURITY, everyTool)).toContain("forge's own copies");
97 expect(machineNote(SECURITY, everyTool)).not.toContain("Not installed");
98 });
99
100 test("only the tools on PATH are claimed: the rest are named as not installed (#9506)", () => {
101 expect(machineEnv(SECURITY, gitleaksOnly).FORGE_MACHINE_TOOLS).toBe("gitleaks");
102 const note = machineNote(SECURITY, gitleaksOnly)!;
103 const [present, absent] = note.split("Not installed on this forge:");
104 expect(present).toContain(`gitleaks (${TOOLS["gitleaks"]})`);
105 expect(present).not.toContain("trivy");
106 expect(absent).toContain("trivy");
107 expect(absent).not.toContain("gitleaks");
108 expect(machineNote(STANDARD, gitleaksOnly)).toBeUndefined();
109 expect(machineEnv(STANDARD, gitleaksOnly).FORGE_MACHINE_TOOLS).toBe("");
110 });
111
112 test("a machine with none of its tools on PATH says so in one line, claiming none (#9506)", () => {
113 expect(machineEnv(SECURITY, noTool).FORGE_MACHINE_TOOLS).toBe("");
114 const note = machineNote(SECURITY, noTool)!;
115 expect(note.split("\n")).toHaveLength(1);
116 expect(note).toContain("none of its tools is installed on this forge");
117 expect(note).not.toContain(TOOLS["gitleaks"]!);
93118 });
94119 });
95120

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs