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

Real-cell proof, cell half: the template build walk in a 2 GiB, 1-core cell, and a real memory breach restarting the child #12002

Merged opened this

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

Merged

scout/forge:scout/12001-real-cell-proof-cell-half-the-template-b → main 2 commits fork 2 threads · 2 unresolved

  • Merged. scout/12001-real-cell-proof-cell-half-the-template-b was merged into main as c82e2df.

Review

Sign in to review this change.
3 files changed +31 −4
docs/features/real-cell-proof.md +5 −1
@@ -3,4 +3,8 @@
33 - `test/helpers/build-standin.ts` (its work in `build-standin-builder.ts`) is a deterministic stand-in for the model: it adds a `/reading` route and its test to the template's app (`READING-V1` in the prompt), or changes what it says (`READING-V2`), runs the app's real `bun test`, and pushes only when that passes.
44 - `test/build-journey.test.ts` walks a first ask to production in one process: `newApp` copies the template, the crew's stand-in builds, the pull's preview run says its tests passed, a person lands it, production serves `/reading`, and a second change lands the same way. About 8 s.
55
6 Not proved here, and tracked in #12001: the same walk inside a 2 GiB, 1-core cell, and a real memory breach restarting a child.
6 ## The cell half, begun (#12001)
7
8 `childEnvFor` (src/spawn/cell.ts, over `cellEnv`) now passes the parent's `FORGE_CLAUDE` to a cell child, so the child's crew can run a stand-in written under the child's own directory (the one place both sides see: the cell hides the parent's data and `/tmp`). Production children read it too (`main.ts`), so a parent with Claude off `PATH` has children that find it. Unit-tested in `test/spawn-cell.test.ts`.
9
10 Still not proved, and still open in #12001: the walk itself in a real 2 GiB, 1-core cell, and a real memory breach restarting a child. Both need bwrap and socat with user namespaces, which the sandbox this was written in refuses.
src/spawn/cell.ts +15 −2
@@ -224,7 +224,7 @@ export function cellSpecFor(input: {
224224 * its cgroup's directory (when the kernel holds the caps) let it read its own
225225 * use against its size (`selfUse`, `spawn/usage.ts`; #10853).
226226 */
227 export function cellEnv(input: { port: number; dataDir: string; url: string; parentPort?: number; size?: Size; cgroupDir?: string }): Record<string, string> {
227 export function cellEnv(input: { port: number; dataDir: string; url: string; parentPort?: number; size?: Size; cgroupDir?: string; claude?: string }): Record<string, string> {
228228 const env = childEnv({
229229 PORT: String(input.port),
230230 FORGE_HOST: "127.0.0.1",
@@ -234,12 +234,25 @@ export function cellEnv(input: { port: number; dataDir: string; url: string; par
234234 ...(input.size === undefined ? {} : { [SIZE_ENV]: input.size }),
235235 ...(input.cgroupDir === undefined ? {} : { [CGROUP_DIR_ENV]: input.cgroupDir }),
236236 ...(input.parentPort === undefined ? {} : { FORGE_PARENT_LOCAL: parentLocal(input.parentPort) }),
237 // Which Claude Code its crew runs: the parent's own choice, a path and not

Forwarded to every production child but nothing consumes it yet; land with its consumer, and test the call site in makeRegistry, not only cellEnv.

238 // a secret, so a test can put a stand-in in the cell (#12001).
239 ...(input.claude === undefined || input.claude === "" ? {} : { FORGE_CLAUDE: input.claude }),
237240 });
238241 delete env["USER"];
239242 delete env["LOGNAME"];
240243 return env;
241244 }
242245
246 /**
247 * What a child is started with: `cellEnv`, plus the parent's `FORGE_CLAUDE`
248 * when it has one. The child's crew reads it (`main.ts`), so a parent run with
249 * Claude Code somewhere off `PATH` has children that find it too.
250 */
251 export function childEnvFor(input: Parameters<typeof cellEnv>[0], parentEnv: Record<string, string | undefined> = process.env): Record<string, string> {
252 const claude = parentEnv["FORGE_CLAUDE"];
253 return cellEnv({ ...input, ...(claude === undefined ? {} : { claude }) });
254 }
255
243256 export interface CellChild {
244257 readonly bodyId: string;
245258 readonly name: string;
@@ -397,7 +410,7 @@ export function makeRegistry(parent: App, launcher?: Launcher): CellChildren {
397410 const command = `exec ${JSON.stringify(process.execPath)} ${JSON.stringify(source.main)}`;
398411 const proc = Bun.spawn(cell.argv(command), {
399412 cwd: dirname(source.main),
400 env: cell.env(cellEnv({ port, dataDir: dir, url, parentPort, size: input.size, ...(cell.cgroupDir === undefined ? {} : { cgroupDir: cell.cgroupDir }) })),
413 env: cell.env(childEnvFor({ port, dataDir: dir, url, parentPort, size: input.size, ...(cell.cgroupDir === undefined ? {} : { cgroupDir: cell.cgroupDir }) })),
401414 stdin: "ignore",
402415 stdout: "pipe",
403416 stderr: "pipe",
test/spawn-cell.test.ts +11 −1
@@ -10,7 +10,7 @@ import { createRun, queueDepth } from "../src/core/runs.ts";
1010 import { createApp } from "../src/app.ts";
1111 import { childEnv } from "../src/apps/process.ts";
1212 import { watchedLimits } from "../src/apps/contain.ts";
13 import { childLog, plainLine, cellChildren, crewAtWork, nextRunDue, type CellChild, stopCellChildren, stopGently, cellEnv, cellLimits, cellPlacement, cellSpecFor, forward, launchSpecFor, ownsBody } from "../src/spawn/cell.ts";
13 import { childLog, plainLine, cellChildren, crewAtWork, nextRunDue, type CellChild, stopCellChildren, stopGently, cellEnv, childEnvFor, cellLimits, cellPlacement, cellSpecFor, forward, launchSpecFor, ownsBody } from "../src/spawn/cell.ts";
1414 import { fallbackPlacement, hostedPlacement, memoryBodies } from "../src/spawn/placement.ts";
1515 import type { SpawnPlan } from "../src/spawn/plan.ts";
1616 import { SIZES } from "../src/spawn/sizes.ts";
@@ -311,6 +311,16 @@ describe("a child reaching its parent from inside its cell", () => {
311311 }
312312 });
313313
314 test("cellEnv carries FORGE_CLAUDE only when the parent names one (#12001)", () => {
315 const base = { port: 4100, dataDir: "/d/c", url: "https://kid--f.test" };
316 expect(cellEnv({ ...base, claude: "/d/c/build-claude" })["FORGE_CLAUDE"]).toBe("/d/c/build-claude");
317 expect(cellEnv(base)["FORGE_CLAUDE"]).toBeUndefined();
318 expect(cellEnv({ ...base, claude: "" })["FORGE_CLAUDE"]).toBeUndefined();
319 // The call site `startOn` uses: the parent's own environment decides.
320 expect(childEnvFor(base, { FORGE_CLAUDE: "/opt/claude" })["FORGE_CLAUDE"]).toBe("/opt/claude");
321 expect(childEnvFor(base, {})["FORGE_CLAUDE"]).toBeUndefined();
322 });
323
314324 test("a heartbeat goes to the relayed port when one is named, and to the public address otherwise", async () => {
315325 const dataDir = join(tmp, "beat-child");
316326 mkdirSync(dataDir, { recursive: true });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs