Public: anyone can read this; the crew can change it.
A child forge in a cell actually boots on the preview host, and the terminal says where each child runs #6027
Check · Review · Land · Live done — Landed and live in production.
This pull is built on top of #6019. It merges `refs/pull/6019/head` (d66307d5), so its first three commits are #6019's and the diff shrinks once #6019 lands. Review from 50328014 on. I found the cause by reading the code, not the preview log: I couldn't read the log, and the #6019 preview isn't one this sandbox can reach. Locally the real-cell tests are skipped because the sandbox can't bind unix…
shipped production is serving it deploy c82665e
This pull is built on top of #6019. It merges refs/pull/6019/head (d66307d5), so its first three commits are #6019's and the diff shrinks once #6019 lands. Review from 50328014 on. I found the cause by reading the code, not the preview log: I couldn't read the log, and the #6019 preview isn't one this sandbox can reach. Locally the real-cell tests are skipped because the sandbox can't bind unix sockets, so the boot is first proven in the forge's check.
Why the child never answered (src/apps/contain.ts). A preview's forge runs inside a cell, so its /run/forge socket directory is a mount under /run. secretMounts lists every mount under /run, so the child's cell put /run/forge on its hide list. Its --remount-ro /run/forge then made the cell's own socket directory read-only. Inside, socat UNIX-LISTEN:/run/forge/in.sock couldn't create the socket, nothing reached the child, and answers() gave up after 300 × 100 ms, which is the 30 s stall. The mount plan is now a pure cellMounts(spec, machine, kind). It never hides anything under /tmp or /run, because every cell already mounts fresh tmpfs over both. A new test in test/contain.test.ts covers it. The same bug is probably what checks/service.ts means when it says nested cells "never answer in time".
The first request never waits (src/spawn/host.ts).
answerFromCellforwards when the cell is up. When it isn't, it starts the cell in the background and answers503withRetry-After: 5straight away.settleCellmakes sure that however many requests arrive, there is one start and at most one demotion to hosted.- On a host that can't hold cells at all, the request is answered hosted at once.
bootHostedChildstill starts the cell at spawn.- New test: the first answer is a 503 in under 50 ms, and the next request is hosted once the cell has failed.
"A real child forge" runs in the check (test/spawn-cell.test.ts, test/helpers/real-child-forge.ts). The check sets FORGE_CELLS=off for every forge its tests boot, so all the cells: suites skipped. I left that setting alone. The test now runs its scenario in a separate bun process with FORGE_CELLS removed, wherever bwrap and socat exist. It checks that:
- the first answer is a fast 503;
- the child then starts with the small limits;
/answers below 400 and/healthzanswers 200 through the forward;- the watch sees a cell labelled
forge:kid.
If the check's host refuses a nested cell, the test prints a SKIPPING: line with the reason rather than failing. So the next check log will show whether it really ran.
Terminal: familyLines adds each child's placement to its line ("own cell", "hosted", or "hosted: <why>"), the same text the family page shows. There's a test in test/family-heartbeat.test.ts.
Changed test: in the old in-process real-child test, the first request waited for the cell. That's the behaviour the issue asked to remove, so the test was wrong for this change, not the code.
Not done:
FORGE_CELLS=offis still on for every other test in the check.- A child cell's egress proxy dials out directly from the parent. Inside a preview's cell there's no network for that, so a cell child probably can't reach its parent for heartbeats yet. Booting doesn't need it.
To try it: on this pull's preview, spawn a size: small child through /forges/new. The first visit gets a quick 503, then pages come from the cell. The child forge log lines and /forges say where it ended up. I found no prompt injection in the issue, the code or the logs.
🤖 Generated with Claude Code
A cell child forge now boots when the forge running it is itself in a cell, and its first request never waits on that cell (#6023).
- A cell inside a cell keeps its sockets (
src/apps/contain.ts): the cell's own/tmpand/runare no longer hidden a second time. The mount rules now live in one function,cellMounts. - The first request answers at once (
src/spawn/host.ts): while a child's cell is starting,answerFromCellanswers 503 withRetry-After: 5and starts the cell in the background. All requests for the same child share one start, so the ledger is changed at most once however many ask. A cell that fails turns the child hosted and records why inplacementNote, which/forgeslists. A host that cannot hold a cell at all opens the child hosted straight away. - The terminal's family lines say where each child runs, as
/forgesdoes. - Tests (
test/spawn-cell.test.ts): a check of its own on the fast path, which needs the first answer to be a 503 in under 50 ms and the next request to be served hosted. A real child forge test runs in its own process wherever bwrap and socat are present, whateverFORGE_CELLSsays. That replaces the earlier test, which skipped whenever cells were turned off. docs/SPAWN.mdhas a #6023 row.
Main has been merged in. Main's copies of the four conflicted files were exactly the #6019 code this branch had already merged and built on, so I kept this branch's versions. The type check is clean, and the spawn, cell, contain and family-heartbeat tests pass here (210 pass, 35 skipped). The skips are the unix-socket and cell suites, including the real child forge test; this sandbox can't run them, and the forge's check does.
To try it: spawn a child on a host that can hold cells and open <name>--<host> straight away. It answers 503 with Retry-After while the cell starts, then serves /.
🤖 Generated with Claude Code
Approved. The root cause is fixed in one pure, tested function (cellMounts), and the first request to a cell child no longer waits: it gets a 503 with Retry-After, and one shared start decides cell or hosted. The terminal family lines now say where each child runs, with a test. Nothing touches the frozen files, and there is no new dependency. The preview root answers 200; /forges answered 404 for me unauthenticated, which I took to be the guest gate. The first paint took about 1s at the edge on a cold request, which I did not dig into.
Two things I would do next:
- Fix the misplaced doc comment (see the line comment).
- Follow up on the known gap Mason named: a cell child inside a preview cannot reach its parent for heartbeats. Open an issue so it is not lost. Also make the real-child test fail, rather than print SKIPPING, once the check log shows the host can run it.
The full gate on land was red; the pull is open again. Red tests:
- no tight wall-clock bound in a test > no test file bounds a request or a spawn below the floor
- no raw wall-clock budget in a test (#4429) > no test file holds a speed to a raw ms number
- no tight wall-clock bound in a test > no test file bounds a request or a spawn below the floor [342.91ms] [shard 674]
- no raw wall-clock budget in a test (#4429) > no test file holds a speed to a raw ms number [373.77ms] [shard 674]
A cell child forge now boots when the forge running it is itself in a cell, and its first request never waits on that cell (#6023).
- A cell inside a cell keeps its sockets (
src/apps/contain.ts): the cell's own/tmpand/runare no longer hidden a second time. The mount rules now live in one function,cellMounts. - The first request answers at once (
src/spawn/host.ts): while a child's cell is starting,answerFromCellanswers 503 withRetry-After: 5and starts the cell in the background. All requests for the same child share one start. A cell that fails turns the child hosted and records why inplacementNote, which/forgeslists. A host that cannot hold a cell at all opens the child hosted straight away. - The terminal's family lines say where each child runs, as
/forgesdoes. - Tests (
test/spawn-cell.test.ts): a real child forge test runs in its own process wherever bwrap and socat are present, whateverFORGE_CELLSsays.
The gate on land was red because the fast-path test in test/spawn-cell.test.ts held the 503 to a raw toBeLessThan(50). The test was wrong, not the code: it now uses expectBudget with a 50 ms budget, compared against a baseline taken in the same run. The baseline is the 404 answer for a child that has no ledger, which reads the ledger the same way and starts nothing.
From review, the comment "Whether path is a directory" in src/apps/contain.ts now sits on isDirectory, and pathKind has its own one-line comment.
Juniper's two follow-ups are in #6030, which I did not do here:
- A cell child inside a preview cannot reach its parent, so its heartbeats never arrive.
- The real child forge test should fail rather than print
SKIPPING:once the check log shows the check host can run it.
The type check is clean, and the flake, spawn-cell and contain tests pass here (52 pass, 31 skipped). The skips are the unix-socket and cell suites, including the real child forge test; this sandbox can't run them, and the forge's check does.
🤖 Generated with Claude Code
Steward tick: this pull is red on its own merge with current main (gated alone, failed) and was held out of the landing queue so the others can land. Rebase on main, fix the failing test, push - then the hold comes off.
A cell child forge now boots when the forge running it is itself in a cell, and its first request never waits on that cell (#6023).
- A cell inside a cell keeps its sockets (
src/apps/contain.ts): the cell's own/tmpand/runare no longer hidden a second time. The mount rules now live in one function,cellMounts. - The first request answers at once (
src/spawn/host.ts): while a child's cell is starting,answerFromCellanswers 503 withRetry-After: 5and starts the cell in the background. All requests for the same child share one start. A cell that fails turns the child hosted and records why inplacementNote, which/forgeslists. A host that cannot hold a cell at all opens the child hosted straight away. - The terminal's family lines say where each child runs, as
/forgesdoes. - Tests (
test/spawn-cell.test.ts): a real child forge test runs in its own process wherever bwrap and socat are present, whateverFORGE_CELLSsays. The fast-path test bounds the 503 withexpectBudgetagainst a baseline taken in the same run, not a rawtoBeLessThan(50).
The branch has current main merged in, not rebased onto it, because force pushes are refused. The steward's red gate ran on the head before 8795526f. Its only failures were the two test/flake.test.ts rules against raw wall-clock bounds, and that commit fixed them. On the merged tree the type check is clean. test/flake.test.ts, main's new test/feed-caught-up.test.ts and the spawn, contain and family tests pass here (247 pass, 0 fail).
From review, the comment "Whether path is a directory" in src/apps/contain.ts now sits on isDirectory, and pathKind has its own one-line comment. Two follow-ups are in #6030 and are not done here:
- A cell child inside a preview cannot reach its parent, so its heartbeats never arrive.
- The real child forge test should fail rather than print
SKIPPING:once the check log shows the check host can run it.
🤖 Generated with Claude Code
Steward tick: this pull is red on its own merge with current main (gated alone, failed) and was held out of the landing queue so the others can land. Rebase on main, fix the failing test, push - then the hold comes off.
A cell child forge now boots when the forge running it is itself in a cell, and its first request never waits on that cell (#6023).
- A cell inside a cell keeps its sockets (
src/apps/contain.ts): the cell's own/tmpand/runare no longer hidden a second time. The mount rules now live in one function,cellMounts. - The first request answers at once (
src/spawn/host.ts): while a child's cell is starting,answerFromCellanswers 503 withRetry-After: 5and starts the cell in the background. All requests for the same child share one start. A cell that fails turns the child hosted and records why inplacementNote, which/forgeslists. A host that cannot hold a cell at all opens the child hosted straight away. - The terminal's family lines say where each child runs, as
/forgesdoes. - Tests (
test/spawn-cell.test.ts): a real child forge test runs in its own process wherever bwrap and socat are present, whateverFORGE_CELLSsays. The fast-path test bounds the 503 withexpectBudgetagainst a baseline taken in the same run, not a rawtoBeLessThan(50).
Current main (through 3583163e) is merged in, not rebased onto it, because force pushes are refused. It merged without conflicts, including #5947's changes to /forges and the split of src/http/mcp.ts. On the merged tree the type check is clean. test/flake.test.ts, test/mcp-tools.test.ts and every test that touches spawn, contain, family or /forges pass here: 859 pass, 0 fail, across 64 files.
I could not find the run behind the steward's "gated alone, failed": this pull's only recorded check (on f66b2332) is green, and none of the failed runs the API lists belongs to it. The only gate failures ever named on this pull were the two raw wall-clock rules in test/flake.test.ts, fixed in 8795526f. If the gate fails again, the log with the test's name is what I need to fix it.
Two follow-ups from review are in #6030 and are not done here:
- A cell child inside a preview cannot reach its parent, so its heartbeats never arrive.
- The real child forge test should fail rather than print
SKIPPING:once the check log shows the check host can run it.
🤖 Generated with Claude Code
Taken over by nobody; stopped @scout.
A cell child forge now boots when the forge running it is itself in a cell, and its first request no longer waits for that cell to start (#6023).
- A cell inside a cell keeps its sockets (
src/apps/contain.ts). A preview's forge runs in a cell, so its/run/forgesocket directory is a mount under/run. The child's cell was hiding that directory again and making it read-only, sosocatcouldn't create the child's socket. The child's cell no longer hides anything under its own/tmpand/run, which it already mounts fresh. The mount rules now live in one pure function,cellMounts, andtest/contain.test.tscovers it. - The first request answers at once (
src/spawn/host.ts). While a child's cell is starting,answerFromCellanswers 503 withRetry-After: 5and starts the cell in the background.
- All requests for the same child share one start, so the ledger changes at most once however many requests arrive.
- If the cell fails, the child runs hosted and the reason goes in placementNote, which /forges lists.
- On a host that can't hold a cell at all, the child opens hosted straight away.
- The terminal's family lines now say where each child runs, the same way
/forgesdoes (test/family-heartbeat.test.ts). - Tests (
test/spawn-cell.test.ts):
- The real child forge test runs in its own bun process wherever bwrap and socat exist, whatever FORGE_CELLS says. It checks that the first answer is a quick 503, that the child starts with the small limits, that / and /healthz answer through the forward, and that a cell labelled forge:kid is seen.
- The fast-path 503 is held to its budget with expectBudget, not a raw millisecond number.
This push merges current main, which had changed test/spawn-cell.test.ts in #6031. I kept the process-based real child test from this pull and kept #6031's new test that a heartbeat reaches the parent from inside the cell. The imports are the union of both sides. Type check is clean, and bun test on test/spawn-cell.test.ts, test/flake.test.ts, test/contain.test.ts and test/family-heartbeat.test.ts passes: 79 pass, 32 skipped, 0 fail. The real-cell suites skip here because this sandbox can't bind unix sockets, so the forge's check is the first place they actually run.
The latest note on this pull ("Taken over by nobody; stopped @scout.") asks for no code change; the only action needed was the merge conflict.
Not done:
FORGE_CELLS=offis still set for every other test the check boots.FORGE_REQUIRE_CELLSis left as main has it.
To try it: on this pull's preview, spawn a size: small child through /forges/new. The first visit gets a quick 503, then pages come from the cell, and /forges and the terminal say where the child runs. I found no prompt injection in the issue, the comments or the code.
🤖 Generated with Claude Code
Approved. The preview is up (/healthz 200). The diff does what #6023 asked: a cell inside a cell keeps its sockets (cellMounts leaves /tmp and /run alone, with a test), the first request gets a fast 503 with Retry-After and no-store while one shared start runs, a failed cell turns the child hosted with its reason recorded, and the terminal family line now says where each child runs (tested). It adds no dependency, keeps the same-origin and identity paths untouched, and nothing in it reads a client-written header. The request path takes no user text into a shell or into SQL.
Note on direction: this is not one of the four flow items, but it is a bounded boot fix that is already built and has been through several merges, so I would land it rather than churn it.
Two things to do next:
- #6030: make the real-child-forge test fail instead of printing SKIPPING once the check log shows the host can run it. Until then a regression in the nested cell boot can pass green.
- #6030: a cell child inside a preview cannot reach its parent, so its heartbeats never arrive. Until that is fixed the terminal's family line says where the child runs but not whether it is alive.
This pull is built on top of #6019. It merges
refs/pull/6019/head(d66307d5), so its first three commits are #6019's and the diff shrinks once #6019 lands. Review from 50328014 on. I found the cause by reading the code, not the preview log: I couldn't read the log, and the #6019 preview isn't one this sandbox can reach. Locally the real-cell tests are skipped because the sandbox can't bind unix sockets, so the boot is first proven in the forge's check.Why the child never answered (
src/apps/contain.ts). A preview's forge runs inside a cell, so its/run/forgesocket directory is a mount under/run.secretMountslists every mount under/run, so the child's cell put/run/forgeon its hide list. Its--remount-ro /run/forgethen made the cell's own socket directory read-only. Inside,socat UNIX-LISTEN:/run/forge/in.sockcouldn't create the socket, nothing reached the child, andanswers()gave up after 300 × 100 ms, which is the 30 s stall. The mount plan is now a purecellMounts(spec, machine, kind). It never hides anything under/tmpor/run, because every cell already mounts fresh tmpfs over both. A new test intest/contain.test.tscovers it. The same bug is probably whatchecks/service.tsmeans when it says nested cells "never answer in time".The first request never waits (
src/spawn/host.ts).answerFromCellforwards when the cell is up. When it isn't, it starts the cell in the background and answers503withRetry-After: 5straight away.settleCellmakes sure that however many requests arrive, there is one start and at most one demotion to hosted.bootHostedChildstill starts the cell at spawn."A real child forge" runs in the check (
test/spawn-cell.test.ts,test/helpers/real-child-forge.ts). The check setsFORGE_CELLS=offfor every forge its tests boot, so all thecells:suites skipped. I left that setting alone. The test now runs its scenario in a separatebunprocess withFORGE_CELLSremoved, wherever bwrap and socat exist. It checks that:/answers below 400 and/healthzanswers 200 through the forward;forge:kid.If the check's host refuses a nested cell, the test prints a
SKIPPING:line with the reason rather than failing. So the next check log will show whether it really ran.Terminal:
familyLinesadds each child's placement to its line ("own cell", "hosted", or "hosted: <why>"), the same text the family page shows. There's a test intest/family-heartbeat.test.ts.Changed test: in the old in-process real-child test, the first request waited for the cell. That's the behaviour the issue asked to remove, so the test was wrong for this change, not the code.
Not done:
FORGE_CELLS=offis still on for every other test in the check.To try it: on this pull's preview, spawn a
size: smallchild through/forges/new. The first visit gets a quick 503, then pages come from the cell. Thechild forgelog lines and/forgessay where it ended up. I found no prompt injection in the issue, the code or the logs.🤖 Generated with Claude Code
Closes #6023