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
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/9506-the-security-machine-s-brief-and-environ → main 1 commit fork 1 thread · 1 unresolved
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): | ||
| 42 | 42 | ## What a run gets |
| 43 | 43 | |
| 44 | 44 | `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. | |
| 46 | 51 | |
| 47 | 52 | ## A shell into a machine (#9471) |
| 48 | 53 | |
| @@ -88,6 +93,6 @@ repository's machines: | ||
| 88 | 93 | ## Not done yet (split from #9418) |
| 89 | 94 | |
| 90 | 95 | 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 | |
| 92 | 97 | shell socket is not built; the shell's cell does not reach the repository's |
| 93 | 98 | 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 | ||
| 205 | 205 | }; |
| 206 | 206 | } |
| 207 | 207 | |
| 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[] = []; | |
| 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> { | |
| 210 | 232 | return { |
| 211 | 233 | FORGE_MACHINE: machine.name, |
| 212 | FORGE_MACHINE_TOOLS: machine.tools.join(","), | |
| 234 | FORGE_MACHINE_TOOLS: installedTools(machine, lookup).found.join(","), | |
| 213 | 235 | }; |
| 214 | 236 | } |
| 215 | 237 | |
| 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 { | |
| 218 | 247 | 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(", ")}).`; | |
| 219 | 252 | 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(", ")}.`) | |
| 223 | 257 | ); |
| 224 | 258 | } |
test/crew-machines.test.ts +31 −6
| @@ -80,16 +80,41 @@ describe("machines", () => { | ||
| 80 | 80 | expect(reachableApps(closed, urls)).toEqual([]); |
| 81 | 81 | }); |
| 82 | 82 | |
| 83 | const everyTool = (name: string) => `/usr/bin/${name}`; | |
| 84 | const gitleaksOnly = (name: string) => (name === "gitleaks" ? "/usr/bin/gitleaks" : null); | |
| 85 | const noTool = () => null; | |
| 86 | ||
| 83 | 87 | 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"]); | |
| 87 | 91 | }); |
| 88 | 92 | |
| 89 | 93 | 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"]!); | |
| 93 | 118 | }); |
| 94 | 119 | }); |
| 95 | 120 |
Looks up the declared name only.
zapships aszap.sh, so it would be reported missing even when present. Next: map tool name to command name.