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

Machines: configurations crew run in, and a shell in the browser #9462

Merged opened this in answer to

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

Merged

mason/forge:mason/9418-machines-configurations-crew-run-in-and → main 2 commits fork 3 threads · 3 unresolved

  • Merged. mason/9418-machines-configurations-crew-run-in-and was merged into main as 8b45bbf.

Review

Sign in to review this change.
7 files changed +445 −2
docs/features/machines.md added +38
@@ -0,0 +1,38 @@
1 # Machines (#9418, first slice)
2
3 A machine is a named configuration a crew member's shell runs in: the tools
4 its role is given, what it may use up, and how far it may reach.
5 `src/crew/machines.ts` holds the catalog and the validation;
6 `src/crew/machines_read.ts` reads a repository's choice.
7
8 ## Configure
9
10 At the top level of `forge.json`, read at the **default branch's tip** (a run
11 cannot widen its own machine by editing the file on its branch):
12
13 ```json
14 "machines": { "audit": { "tools": ["gitleaks", "trivy"], "reach": [] } },
15 "machineFor": { "reviewer": "audit" }
16 ```
17
18 - `tools` are picked from a list (`TOOLS`): gitleaks, trivy, osv-scanner,
19 nuclei, zap, testssl.sh, nmap. A name is never a command.
20 - `reach` is `["apps"]` (the repository's own production and previews, as
21 today) or `[]`. A machine cannot name a host, so it can only narrow the
22 sandbox; the forge's secrets stay denied by `crew/sandbox.ts` whatever a
23 machine says.
24 - `machineFor` maps a crew role (planner, builder, reviewer) to a machine. A
25 role with none runs in `standard`, which is today's sandbox exactly.
26 - `standard` and `security` are built in. `security` carries the investigation
27 tools, aimed at the forge's own copies only.
28 - An invalid declaration falls back to `standard`; it never stops the crew.
29
30 ## What a run gets
31
32 `FORGE_MACHINE` and `FORGE_MACHINE_TOOLS` (names only) in its environment, a
33 line in its brief naming the tools its machine carries, and its `apps` reach.
34
35 ## Not done yet (split from #9418)
36
37 Memory and CPU ceilings for a machine wait for the cgroup work (#9460); the tools are declared, not yet installed in an image; the browser
38 shell (ghostty-web, tmux, WebSocket, session records) is not started.
src/crew/brief.ts +10
@@ -7,6 +7,7 @@
77 * rules (docs/APPS.md) hold whether or not the agent reads them.
88 */
99
10 import { machineNote, type Machine } from "./machines.ts";
1011 import type { OwnApp } from "./app_hosts.ts";
1112 import type { Owed } from "./team_share.ts";
1213 import type { Agent, CrewRole } from "../core/agents.ts";
@@ -50,6 +51,8 @@ export interface BriefInput {
5051 readonly resuming: boolean;
5152 /** What this agent is for on this repository; a planner writes maps. */
5253 readonly role?: CrewRole | undefined;
54 /** The machine this role's shell runs in (`crew/machines.ts`). */
55 readonly machine?: Machine | undefined;
5356 /**
5457 * The policy in force: the app's contract as a table, when it has one.
5558 * A builder is told the room it works in before it starts, so the doors
@@ -100,6 +103,12 @@ export function fenced(text: string, info = ""): string[] {
100103 return [fence + info, text, fence];
101104 }
102105
106 /** The brief line naming a machine's tools; none for a machine without any. */
107 function machineLines(machine: Machine | undefined): string[] {
108 const note = machine === undefined ? undefined : machineNote(machine);
109 return note === undefined ? [] : [note, ""];
110 }
111
103112 /**
outdated

This function now sits between promptVersion's doc comment and promptVersion. Move it above that comment.

104113 * A short name for the prompt an agent ran on: a hash of its brief, its
105114 * role and the standing system-prompt text. Per-run facts (issue, branch)
@@ -123,6 +132,7 @@ export function systemPrompt(input: BriefInput): string {
123132 "",
124133 "Read docs/VISION.md first: it says what the forge is for and the order of work. Done means reachable: a function nobody can call is not done. Where the repository has docs/PRINCIPLES.md, read it too: what it holds to, and what enforces each.",
125134 "",
135 ...machineLines(input.machine),
126136 "How to finish:",
127137 "- Commit your work with clear messages and push: `git push`.",
128138 "- If `forge.json` declares a `check`, the forge runs it against every push to your pull, and that verdict is what lands it; a failing check comes back to you with its log.",
src/crew/machines.ts added +194
@@ -0,0 +1,194 @@
1 /**
2 * Machines (#9418): the named configuration a crew member's shell runs in.
3 * A machine says which investigation tools its role is given, what it may
4 * use up, and how far it may reach. It can only narrow the sandbox
5 * (`crew/sandbox.ts`), never widen it: reach is a choice between names the
6 * forge owns (`apps`), not a host list, so no machine configuration
7 * can name a system the forge does not own, and the forge's secrets stay
8 * denied by the sandbox whatever a machine says.
9 *
10 * A repository declares machines at the top level of `forge.json`, read at
11 * the default branch's tip, never from the agent's own branch (an agent
12 * cannot hand itself a bigger machine):
13 *
14 * "machines": { "audit": { "tools": ["gitleaks"], "reach": [] } },
15 * "machineFor": { "reviewer": "audit" }
16 *
17 * `standard` (today's sandbox) and `security` ship built in. Pure: text in,
18 * plain data out.
19 */
20
21 import { CREW_ROLES, type CrewRole } from "../core/agents.ts";
22 import { err, ok, type Result } from "../kernel/result.ts";
23
24 /**
25 * What a machine may reach beyond the forge itself (always reachable: the
26 * shell pushes and reads through it): the repository's own app, production
27 * and previews. A machine can drop it; it cannot add anything.
28 */
29 export const REACHES = ["apps"] as const;
30 export type Reach = (typeof REACHES)[number];
31
32 /**
33 * The tools a machine can be given, from a list (not free text, so a name
34 * is never a command). The security machine's first set, from the research
35 * on #9418; they investigate the forge's own copies only.
36 */
37 export const TOOLS: Readonly<Record<string, string>> = {
38 gitleaks: "finds secrets committed to a repository",
39 trivy: "scans dependencies and images for known vulnerabilities",
40 "osv-scanner": "matches lockfiles against the OSV vulnerability database",
41 nuclei: "runs template-driven checks against a running app",
42 zap: "probes a running web app for common weaknesses",
43 "testssl.sh": "checks a TLS endpoint's protocols and ciphers",
44 nmap: "maps open ports on a host the machine may reach",
45 };
46
47 export interface Machine {
48 readonly name: string;
49 readonly label: string;
50 readonly tools: readonly string[];
51 readonly reach: readonly Reach[];
52 }
53
54 /** Today's sandbox, unchanged: the forge and the repository's own app. */
55 export const STANDARD: Machine = {
56 name: "standard",
57 label: "Standard — the crew's usual sandbox",
58 tools: [],
59 reach: ["apps"],
60 };
61
62 /** The investigation machine: scanners aimed at the forge's own copies. */
63 export const SECURITY: Machine = {
64 name: "security",
65 label: "Security — investigation tools, the forge's own copies only",
66 tools: ["gitleaks", "trivy", "osv-scanner", "nuclei", "zap", "testssl.sh", "nmap"],
67 reach: ["apps"],
68 };
69
70 export const BUILT_IN: readonly Machine[] = [STANDARD, SECURITY];
71
72 export interface MachineConfig {
73 /** Built-ins first, then the repository's, a same name replacing. */
74 readonly machines: readonly Machine[];
75 /** Crew role to machine name; a role not here runs in `standard`. */
76 readonly byRole: Readonly<Partial<Record<CrewRole, string>>>;
77 }
78
79 export const NO_CONFIG: MachineConfig = { machines: BUILT_IN, byRole: {} };
80
81 const NAME_RE = /^[a-z][a-z0-9-]{0,31}$/;
82 const MACHINES_MAX = 16;
83
84 const isObject = (v: unknown): v is Record<string, unknown> =>
85 typeof v === "object" && v !== null && !Array.isArray(v);
86
87 const invalid = (message: string) => err(`forge.json: ${message}`);
88
89 /** The machines `forge.json` text declares, validated; absent text is none. */
90 export function parseMachines(
91 forgeJson: string | undefined,
92 ): Result<MachineConfig, string> {
93 if (forgeJson === undefined) return ok(NO_CONFIG);
94 let root: unknown;
95 try {
96 root = JSON.parse(forgeJson);
97 } catch {
98 // The manifest parser owns the report of a file that is not JSON.
99 return ok(NO_CONFIG);
100 }
101 if (!isObject(root)) return ok(NO_CONFIG);
102 const declared = root["machines"];
103 const mapping = root["machineFor"];
104 const own: Machine[] = [];
105 if (declared !== undefined) {
106 if (!isObject(declared))
107 return invalid("machines must be an object of named machines");
108 const names = Object.keys(declared);
109 if (names.length > MACHINES_MAX)
110 return invalid(`machines may name at most ${MACHINES_MAX}`);
111 for (const name of names) {
112 const made = parseMachine(name, declared[name]);
113 if (!made.ok) return made;
114 own.push(made.value);
115 }
116 }
117 const machines = [
118 ...BUILT_IN.filter((b) => !own.some((m) => m.name === b.name)),
119 ...own,
120 ];
121 const byRole: Partial<Record<CrewRole, string>> = {};
122 if (mapping !== undefined) {
123 if (!isObject(mapping))
124 return invalid("machineFor must map a crew role to a machine name");
125 for (const [role, name] of Object.entries(mapping)) {
126 if (!(CREW_ROLES as readonly string[]).includes(role))
127 return invalid(`machineFor "${role}" is not a crew role (${CREW_ROLES.join(", ")})`);
128 if (typeof name !== "string" || !machines.some((m) => m.name === name))
129 return invalid(
130 `machineFor ${role} names a machine that is not declared (${machines.map((m) => m.name).join(", ")})`,
131 );
132 byRole[role as CrewRole] = name;
133 }
134 }
135 return ok({ machines, byRole });
136 }
137
138 function parseMachine(name: string, value: unknown): Result<Machine, string> {
139 if (!NAME_RE.test(name))
140 return invalid(`machine name "${name.slice(0, 40)}" must be lowercase letters, digits and dashes`);
141 if (!isObject(value)) return invalid(`machines.${name} must be an object`);
142 const tools = value["tools"] ?? [];
143 if (!Array.isArray(tools)) return invalid(`machines.${name}.tools must be a list`);
144 for (const tool of tools)
145 if (typeof tool !== "string" || !Object.hasOwn(TOOLS, tool))
146 return invalid(`machines.${name}.tools: pick from ${Object.keys(TOOLS).join(", ")}`);
147 const reach = value["reach"] ?? ["apps"];
148 if (
149 !Array.isArray(reach) ||
150 reach.some((r) => !(REACHES as readonly unknown[]).includes(r))
151 )
152 return invalid(`machines.${name}.reach: pick from ${REACHES.join(", ")} (a machine cannot name a host)`);
153 const label = value["label"];
154 if (label !== undefined && (typeof label !== "string" || label.length > 80))
155 return invalid(`machines.${name}.label must be short text`);
156 return ok({
157 name,
158 label: typeof label === "string" && label !== "" ? label : name,
159 tools: [...new Set(tools as string[])],
160 reach: [...new Set(reach as Reach[])],
161 });
162 }
163
164 /** The machine a role runs in: its choice, else the standard one. */
165 export function machineFor(config: MachineConfig, role: CrewRole | undefined): Machine {
166 const name = role === undefined ? undefined : config.byRole[role];
167 return config.machines.find((m) => m.name === name) ?? STANDARD;
168 }
169
170 /**
171 * The repository's own app URLs a machine lets the sandbox allow: none for
172 * a machine without `apps` reach. Never adds a host.
173 */
174 export function reachableApps(machine: Machine, appUrls: readonly string[]): string[] {
175 return machine.reach.includes("apps") ? [...appUrls] : [];
176 }
177
178 /** The run's environment for its machine: names only, nothing secret. */
179 export function machineEnv(machine: Machine): Record<string, string> {
180 return {
181 FORGE_MACHINE: machine.name,
182 FORGE_MACHINE_TOOLS: machine.tools.join(","),
183 };
184 }
185
186 /** One line for the brief: where the shell runs and what it was given. */
187 export function machineNote(machine: Machine): string | undefined {
188 if (machine.tools.length === 0) return undefined;
189 return (
190 `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): ` +
191 machine.tools.map((t) => `${t} (${TOOLS[t]})`).join("; ") +
192 "."
193 );
194 }
src/crew/machines_read.ts added +21
@@ -0,0 +1,21 @@
1 /**
2 * A repository's machines as the default branch declares them (#9418).
3 * Read at the default branch's tip, never at the agent's own branch: a run
4 * cannot widen the machine it runs in by editing `forge.json`.
5 */
6 import type { App } from "../app.ts";
7 import { readFileAt } from "../apps/checkout.ts";
8 import type { Repo } from "../core/repos.ts";
9 import { resolveOid } from "../git/merge.ts";
10 import { repoDir } from "../git/repos.ts";
11 import { NO_CONFIG, parseMachines, type MachineConfig } from "./machines.ts";
12
13 /** The declared machines, or the built-ins when there are none or they are invalid. */
14 export async function repoMachines(app: App, repo: Repo): Promise<MachineConfig> {
15 const dir = repoDir(app.reposRoot, repo.ownerName, repo.name);
16 const tip = await resolveOid(dir, `refs/heads/${repo.defaultBranch}`);

The key claim (read at the default tip, not the agent's branch) has no test. Add one with two branches that declare different machines.

17 if (tip === undefined) return NO_CONFIG;
18 const parsed = parseMachines(await readFileAt(dir, tip, "forge.json"));
19 // A broken declaration must not stop the crew: the standard sandbox runs.
20 return parsed.ok ? parsed.value : NO_CONFIG;
21 }
src/crew/runner.ts +9 −2
@@ -35,6 +35,8 @@ import { previewEnv } from "../apps/data.ts";
3535 import { appsRuntime } from "../apps/runtime.ts";
3636 import { chooseModel, modelFacts } from "./model_choice.ts";
3737 import { ownApp, ownAppUrls } from "./app_hosts.ts";
38 import { machineEnv, machineFor, reachableApps } from "./machines.ts";
39 import { repoMachines } from "./machines_read.ts";
3840 import { authorsInRepoPull } from "./authors_pull.ts";
3941 import { crewRole, getAgent, type Agent } from "../core/agents.ts";
4042 import { getAppByRepo, type AppRecord } from "../core/apps.ts";
@@ -1090,9 +1092,13 @@ async function work(
10901092 : undefined;
10911093 const policy =
10921094 record === undefined ? undefined : await policyBrief(app, repo);
1095 // The machine this seat runs in, from the default branch's forge.json.
1096 const role = crewRole(app.db, repo.id, agent.user.id);
1097 const machine = machineFor(await repoMachines(app, repo), role);

The env and the reach narrowing are wired here with no test that would fail if this wiring were removed.

10931098 const brief = {
10941099 agent,
10951100 repo,
1101 machine,
10961102 issue,
10971103 pull,
10981104 branch,
@@ -1107,7 +1113,7 @@ async function work(
11071113 !reviewing &&
11081114 previous?.sessionId !== undefined &&
11091115 previous.sessionId !== "",
1110 role: crewRole(app.db, repo.id, agent.user.id),
1116 role,
11111117 };
11121118 const failedCheck =
11131119 reason === "check_failed" && pull
@@ -1320,6 +1326,7 @@ async function work(
13201326 ...passportEnv(app.config.dataDir),
13211327 ...credentialEnv(picked),
13221328 ...grant.env,
1329 ...machineEnv(machine),
13231330 ...(options.configDir === undefined
13241331 ? {}
13251332 : { CLAUDE_CONFIG_DIR: options.configDir }),
@@ -1339,7 +1346,7 @@ async function work(
13391346 dataDir: app.config.dataDir,
13401347 agentName: agent.user.username,
13411348 urls: [options.localUrl, options.forgeUrl],
1342 appUrls: ownAppUrls(ownApp(app, repo)),
1349 appUrls: reachableApps(machine, ownAppUrls(ownApp(app, repo))),
13431350 extraHosts: [
13441351 ...(options.allowHosts ?? []),
13451352 ...hosts,
test/crew-machines-wiring.test.ts added +85
@@ -0,0 +1,85 @@
1 /**
2 * Where machines meet the rest of the crew (#9418): the brief names the
3 * machine's tools, and a repository's machines are read at the default
4 * branch's tip, never at a side branch an agent could have edited.
5 */
6 import { afterAll, expect, test } from "bun:test";
7 import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import type { App } from "../src/app.ts";
11 import { systemPrompt, type BriefInput } from "../src/crew/brief.ts";
12 import { machineFor, SECURITY, STANDARD } from "../src/crew/machines.ts";
13 import { repoMachines } from "../src/crew/machines_read.ts";
14 import type { Repo } from "../src/core/repos.ts";
15 import { gitClientEnv } from "./helpers/git-client.ts";
16
17 const prompt = (machine: BriefInput["machine"]) =>
18 systemPrompt({
19 agent: { user: { username: "bot", displayName: "" }, brief: "" },
20 repo: { ownerName: "alice", name: "notes", defaultBranch: "main", autonomyDirection: "" },
21 issue: { number: 7, title: "t", body: "", authorName: "alice" },
22 branch: "bot/7",
23 forgeUrl: "http://forge.test",
24 isApp: true,
25 resuming: false,
26 machine,
27 } as unknown as BriefInput);
28
29 test("the system prompt names a machine's tools, and the standard machine adds nothing", () => {
30 expect(prompt(SECURITY)).toContain("Your shell runs in the `security` machine");
31 expect(prompt(SECURITY)).toContain("gitleaks");
32 expect(prompt(STANDARD)).not.toContain("Your shell runs in");
33 expect(prompt(undefined)).toBe(prompt(STANDARD));
34 });
35
36 const env = gitClientEnv({
37 GIT_AUTHOR_NAME: "A",
38 GIT_AUTHOR_EMAIL: "[email protected]",
39 GIT_COMMITTER_NAME: "A",
40 GIT_COMMITTER_EMAIL: "[email protected]",
41 });
42 const roots: string[] = [];
43 afterAll(() => Promise.all(roots.map((d) => rm(d, { recursive: true, force: true }))));
44
45 async function git(args: string[], cwd: string): Promise<void> {
46 const p = Bun.spawn(["git", ...args], { cwd, env, stdout: "pipe", stderr: "pipe" });
47 const [err, code] = await Promise.all([new Response(p.stderr).text(), p.exited]);
48 if (code !== 0) throw new Error(`git ${args.join(" ")}: ${err}`);
49 }
50
51 test("a repository's machines are read at the default branch's tip, not a side branch's", async () => {
52 const root = await mkdtemp(join(tmpdir(), "forge-machines-"));
53 roots.push(root);
54 const dir = join(root, "alice", "notes.git");
55 await mkdir(dir, { recursive: true });
56 await git(["init", "-q", "-b", "main", dir], dir);
57 // A bare repository has no work tree; build commits with plumbing.
58 const commit = async (ref: string, text: string) => {
59 await writeFile(join(root, "forge.json"), text);
60 const blob = await hashObject(dir, join(root, "forge.json"));
61 const tree = await plumb(dir, ["mktree"], `100644 blob ${blob}\tforge.json\n`);
62 const oid = await plumb(dir, ["commit-tree", tree, "-m", ref], "");
63 await git(["update-ref", `refs/heads/${ref}`, oid], dir);
64 };
65 await commit("main", JSON.stringify({ machines: { shut: { reach: [] } }, machineFor: { reviewer: "shut" } }));
66 await commit("bot/7", JSON.stringify({ machines: { wide: { tools: ["nmap"] } }, machineFor: { reviewer: "wide" } }));
67
68 const app = { reposRoot: root } as unknown as App;
69 const repo = { ownerName: "alice", name: "notes", defaultBranch: "main" } as unknown as Repo;
70 const config = await repoMachines(app, repo);
71 expect(machineFor(config, "reviewer").name).toBe("shut");
72 expect(config.machines.some((m) => m.name === "wide")).toBe(false);
73
74 // And the default branch moving is what changes the answer.
75 await git(["update-ref", "refs/heads/main", (await plumb(dir, ["rev-parse", "refs/heads/bot/7"], "")) ], dir);
76 expect(machineFor(await repoMachines(app, repo), "reviewer").name).toBe("wide");
77 });
78
79 async function plumb(dir: string, args: string[], stdin: string): Promise<string> {
80 const p = Bun.spawn(["git", ...args], { cwd: dir, env, stdin: new TextEncoder().encode(stdin), stdout: "pipe", stderr: "pipe" });
81 const [out, err, code] = await Promise.all([new Response(p.stdout).text(), new Response(p.stderr).text(), p.exited]);
82 if (code !== 0) throw new Error(`git ${args.join(" ")}: ${err}`);
83 return out.trim();
84 }
85 const hashObject = (dir: string, file: string) => plumb(dir, ["hash-object", "-w", file], "");
test/crew-machines.test.ts added +88
@@ -0,0 +1,88 @@
1 import { describe, expect, test } from "bun:test";
2 import {
3 BUILT_IN,
4 machineEnv,
5 machineFor,
6 machineNote,
7 NO_CONFIG,
8 parseMachines,
9 reachableApps,
10 SECURITY,
11 STANDARD,
12 TOOLS,
13 } from "../src/crew/machines.ts";
14
15 const config = (root: unknown) => {
16 const parsed = parseMachines(JSON.stringify(root));
17 if (!parsed.ok) throw new Error(parsed.error);
18 return parsed.value;
19 };
20 const refusal = (root: unknown) => {
21 const parsed = parseMachines(JSON.stringify(root));
22 return parsed.ok ? undefined : parsed.error;
23 };
24
25 describe("machines", () => {
26 test("no forge.json, or none that declares machines, is the built-ins", () => {
27 expect(parseMachines(undefined)).toEqual({ ok: true, value: NO_CONFIG });
28 expect(config({ app: {} }).machines).toEqual(BUILT_IN);
29 expect(parseMachines("{not json")).toEqual({ ok: true, value: NO_CONFIG });
30 });
31
32 test("a role with no choice runs in the standard machine, which is today's sandbox", () => {
33 expect(machineFor(NO_CONFIG, "reviewer")).toBe(STANDARD);
34 expect(machineFor(NO_CONFIG, undefined)).toBe(STANDARD);
35 expect(STANDARD.tools).toEqual([]);
36 expect(STANDARD.reach).toEqual(["apps"]);
37 });
38
39 test("a repository picks a machine for a role from the list", () => {
40 const c = config({
41 machines: { audit: { tools: ["gitleaks", "gitleaks", "nmap"], reach: [] } },
42 machineFor: { reviewer: "audit", planner: "security" },
43 });
44 const audit = machineFor(c, "reviewer");
45 expect(audit.name).toBe("audit");
46 expect(audit.tools).toEqual(["gitleaks", "nmap"]);
47 expect(audit.reach).toEqual([]);
48 expect(machineFor(c, "planner")).toBe(SECURITY);
49 expect(machineFor(c, "builder")).toBe(STANDARD);
50 });
51
52 test("the security machine ships with the investigation tools from the research", () => {
53 expect([...SECURITY.tools].sort()).toEqual(
54 ["gitleaks", "trivy", "osv-scanner", "nuclei", "zap", "testssl.sh", "nmap"].sort(),
55 );
56 for (const tool of SECURITY.tools) expect(TOOLS[tool]).toBeTruthy();
57 });
58
59 test("a machine cannot name a host, a tool off the list, or a role that is not one", () => {
60 expect(refusal({ machines: { a: { reach: ["evil.example.com"] } } })).toContain("cannot name a host");
61 expect(refusal({ machines: { a: { reach: ["forge"] } } })).toContain("reach");
62 expect(refusal({ machines: { a: { tools: ["rm -rf /"] } } })).toContain("pick from");
63 expect(refusal({ machines: { a: { tools: ["__proto__"] } } })).toContain("pick from");
64 expect(refusal({ machines: { "A b": {} } })).toContain("lowercase");
65 expect(refusal({ machines: [] })).toContain("object");
66 expect(refusal({ machineFor: { admin: "standard" } })).toContain("not a crew role");
67 expect(refusal({ machineFor: { builder: "nope" } })).toContain("not declared");
68 });
69
70 test("reach only ever narrows the app URLs the sandbox allows", () => {
71 const urls = ["https://notes.example", "https://notes--3.example"];
72 expect(reachableApps(STANDARD, urls)).toEqual(urls);
73 const closed = config({ machines: { shut: { reach: [] } } }).machines.find((m) => m.name === "shut")!;
74 expect(reachableApps(closed, urls)).toEqual([]);
75 });
76
77 test("a machine's environment carries names, never a secret", () => {
78 expect(machineEnv(SECURITY).FORGE_MACHINE).toBe("security");
79 expect(machineEnv(SECURITY).FORGE_MACHINE_TOOLS).toContain("gitleaks");
80 expect(Object.keys(machineEnv(SECURITY)).sort()).toEqual(["FORGE_MACHINE", "FORGE_MACHINE_TOOLS"]);
81 });
82
83 test("the brief names the tools a machine carries, and says nothing for the standard one", () => {
84 expect(machineNote(STANDARD)).toBeUndefined();
85 expect(machineNote(SECURITY)).toContain("gitleaks");
86 expect(machineNote(SECURITY)).toContain("forge's own copies");
87 });
88 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs