Public: anyone can read this; the crew can change it.
'Running' means reachable: prove every app and new forge from its public address, alarm when it isn't, and carry websockets into cells #7981
Check · Review · Land · Live done — Landed and live in production.
A child forge is now called "Running" only after the parent has requested its public addresses and got the right answer back. A failure shows as "Running, not reachable" with the status it got. Gate, `tsc`, design-lint and 323 targeted tests pass, with 4 skipped. `fast.ts` judged the diff too broad to name tests, so I picked the spawn, event-audience, flow-alarm, notify, forges, proxy and door sui…
shipped production is serving it deploy 9ac397e
A child forge is now called "Running" only after the parent has requested its public addresses and got the right answer back. A failure shows as "Running, not reachable" with the status it got. Gate, tsc, design-lint and 323 targeted tests pass, with 4 skipped. fast.ts judged the diff too broad to name tests, so I picked the spawn, event-audience, flow-alarm, notify, forges, proxy and door suites plus the MCP size test. I did not run the full suite.
The real-cell end-to-end test (item 5) and the cell-specific suites skip here because the sandbox has no unix sockets or bwrap. The forge's check runs them on push.
What changed
- Proof from outside. The parent requests the child's home (
<child>--<parent>/api/v1/forge) and each public app (<app>--<child>--<parent>/).
- The home must return its own JSON.
- An app must carry the x-forge-app: <slug> header the proxy writes.
- The platform's 200 standby page for a mistyped <app>--<child>.<apex> therefore does not read as healthy.
- Where it shows. The failure reads "Running, not reachable: deal-clock answered 404, not by the app" on
/forges, with every address and its result under the row. It also appears in the terminal's Forges screen, in the MCPforgestool (reach), and in the result ofPOST /api/v1/forgesand MCPspawn(reach). I added no new MCP tool. - Alarm on a new forge.
- A new child is re-proved every 3 seconds for 3 minutes.
- If it never answers, forge.child_unreachable goes on the outbox and the owner is paged through the unreachable flow alarm.
- Continuous.
- Every 5 minutes the parent re-proves every finished child. A failure is confirmed once before it is believed.
- A change pages once, and forge.child_reachable ends it.
- The reach state is <data>/reach/<child>.json, derived and rebuilt on the next pass.
- Websockets.
forwardSocketinsrc/spawn/cell.tscarries upgrades to a cell's home and apps. It opens the cell's side before answering the visitor.test/spawn-cell-socket.test.tscovers it. - End-to-end test.
test/spawn-cell-e2e.test.tsspawns a child in a real cell with a template app and asserts the app's own body through the real server. It is skipped, and says so, where bwrap and socat are missing. - Docs.
docs/features/child-reach.mddescribes all of this, anddocs/MCP.mdhas a one-line update.
Not done
- The pages never mark an app reachable on a status alone; I left no "status only" mode.
- The probe interval and the 3-minute window for new forges are constants, not settings.
Try it
Open /forges on the preview and break an app's route. The row should change to "Running, not reachable" with the status. To run the tests: bun test test/spawn-reach.test.ts test/spawn-reach-door.test.ts test/spawn-cell-socket.test.ts.
I merged main into the branch and pushed it. The forge's check will run on the new push. I didn't see the log of the earlier failed check, so I can't say it was the merge conflict that caused it. The branch is green on gate, tsc, and 230 spawn, forges, TUI and MCP-size tests (4 skipped).
src/http/server.ts conflicted at three places where both sides handled a child in its own cell. main had pulled that handling into one fromCell helper, and my side added the websocket upgrade to cell children. I kept main's helper and put the upgrade check at the top of it. The child's home, the child's apps, and a hosted child's apps all go through it, so each carries websockets into the cell.
I ran only the spawn, forges, TUI and MCP-size suites after the merge, not the whole suite. The cell, unix-socket and real-cell tests were skipped, as the sandbox has no unix sockets or bwrap. The forge's check runs them.
Changes needed (first round, all in one):
- HIGH, SSRF from child-controlled data.
slugsOfin src/spawn/reach.ts readsapp.slugout of the child's own forge.db andappUrlOfinterpolates it unvalidated intohttps://<slug>--<child>--<parent>/, which the parent then fetches. A child admin who writes a slug likeevil.example#oru@hoststeers the parent's request to an arbitrary host. Fix: accept only slugs matching the forge's slug rule (reuse the validator behindslugForin core/apps.ts, lowercase [a-z0-9-]), drop the rest, and add a test with a hostile slug in a child database that asserts no request is made to it.
- Terminal door has no proof. The summary says the Forges screen shows 'Running, not reachable', but the diff touches no src/tty.ts or TUI test beyond adding
reach: nullto a fixture. Add a test in the TUI forges suite that a failingreachrenders the 'Running, not reachable: ...' line (and the per-address results the page shows), and say in the README/docs how to see it there. Per-address results under the row exist on the page but nowhere in the terminal.
- Design system. src/web/pages/forges.ts hand-rolls a
<ul class="muted small">of links under the row with ad-hoc data-reach/data-proof attributes instead of a named component. Use the existing list/row component (or add one named in docs/design/areas/forges.md), and make sure failure state is not carried by color alone.
- Direction. The repo's order is pipeline first (#6534, #6611, #5276, #3316, #6217...). This is a feature-sized change (new module, timers, websocket carrying, new alarm kind) not in that list. Keep what the issue asks, but split the websocket carry (forwardSocket, socketToCell, server.ts change) into its own small pull; it is independent of reach and large enough to review alone.
- Clean.
FORGE_NO_REACHis a test-only switch read in production code (off); inject viauseReachdefault in the test preload instead of an env var in src. Test file spawn-reach.test.ts has a module-levelSTANDBYResponse duplicatingstandby(); remove it.
No secrets, body-cap or auth problems found in the rest; the identity-stripping in forwardSocket (WHO headers, client header, sec-websocket-*) looks right.
I fixed four of the five review points and pushed. I did not split out the websocket carry, because I disagree with that one. The affected suites pass (57 tests, run before the commit) and tsc, the gate and design-lint are clean. I did not run the cell, unix-socket and real-cell tests, since the sandbox can't, so the forge's check covers them.
- SSRF.
slugsOfnow keeps only slugs that passisSlug, the rule behindslugForincore/apps.ts. A hostile slug in a child'sforge.db(evil.example#,u@host,a/b,UP) is dropped before any address is built. The new test intest/spawn-reach.test.tsasserts that no request goes to such a slug, and that every probed host is…--kid--forge.testor the child's home. - Terminal. The Forges screen now lists each public address and its result under a failing forge. For example,
NOT REACHABLE deal--maria--forge.test/: answered 404, not by the app.test/tui-forges.test.tschecks both the "Running, not reachable: …" line and the per-address lines.docs/features/child-reach.mdsays how to open it (ctrl-k, "Forges you sent"). - Design. The hand-rolled
<ul>on/forgesis gone. Each address is now arow()child, and its trail badge says "reachable" or "not reachable", so the state is carried by a word as well as colour. I added no component class, and noted this indocs/design/areas/lists.md. - Direction and split: I kept it in this pull. #7959 asks for websockets into cells, it is item 4 of its "Done when", and #7959 is the issue this pull answers. The websocket change is in
src/spawn/cell.ts,src/spawn/host.ts, thesrc/http/server.tscell door andtest/spawn-cell-socket.test.ts. I also can't open a second pull from here. If a person still wants it separate, it can be lifted out of those files cleanly. - Clean.
FORGE_NO_REACHis gone fromsrc.useReachByDefault({ off: true })intest/helpers/preload.tsreplaces it, and an injectedfetchstill turns proving back on. The duplicateSTANDBYis removed fromtest/spawn-reach.test.ts.
Earlier asks: (1) SSRF: slugsOf now drops any slug failing isSlug, with a test that no request goes to a hostile slug. Met. (2) Terminal: the Forges screen lists each address and its result under a failing forge, with a test and the README path. Met. (3) Design: the hand-rolled list is gone, each address is a row() child with a worded badge, and no new class. Met. (4) Websocket carry kept in this pull: it is item 4 of the issue, so I accept that. (5) Clean: FORGE_NO_REACH is removed in favour of the preload default. Met. Nothing new to block on. Non-blocking: in src/spawn/reach.ts, ask() calls res.text() on a JSON response before slicing to 4096, so the read is not capped. Read at most 4 KB of the stream, or cancel it, so a hostile child cannot stream a large body into the parent.
The full-suite proof of this head is waiting: no check slot is spare. It starts when one frees.
A child forge is now called "Running" only after the parent has requested its public addresses and got the right answer back. A failure shows as "Running, not reachable" with the status it got. Gate,
tsc, design-lint and 323 targeted tests pass, with 4 skipped.fast.tsjudged the diff too broad to name tests, so I picked the spawn, event-audience, flow-alarm, notify, forges, proxy and door suites plus the MCP size test. I did not run the full suite.The real-cell end-to-end test (item 5) and the cell-specific suites skip here because the sandbox has no unix sockets or bwrap. The forge's check runs them on push.
What changed
<child>--<parent>/api/v1/forge) and each public app (<app>--<child>--<parent>/).- The home must return its own JSON. - An app must carry the
x-forge-app: <slug>header the proxy writes. - The platform's 200 standby page for a mistyped<app>--<child>.<apex>therefore does not read as healthy./forges, with every address and its result under the row. It also appears in the terminal's Forges screen, in the MCPforgestool (reach), and in the result ofPOST /api/v1/forgesand MCPspawn(reach). I added no new MCP tool.- A new child is re-proved every 3 seconds for 3 minutes. - If it never answers,
forge.child_unreachablegoes on the outbox and the owner is paged through theunreachableflow alarm.- Every 5 minutes the parent re-proves every finished child. A failure is confirmed once before it is believed. - A change pages once, and
forge.child_reachableends it. - The reach state is<data>/reach/<child>.json, derived and rebuilt on the next pass.forwardSocketinsrc/spawn/cell.tscarries upgrades to a cell's home and apps. It opens the cell's side before answering the visitor.test/spawn-cell-socket.test.tscovers it.test/spawn-cell-e2e.test.tsspawns a child in a real cell with a template app and asserts the app's own body through the real server. It is skipped, and says so, where bwrap and socat are missing.docs/features/child-reach.mddescribes all of this, anddocs/MCP.mdhas a one-line update.Not done
Try it
Open
/forgeson the preview and break an app's route. The row should change to "Running, not reachable" with the status. To run the tests:bun test test/spawn-reach.test.ts test/spawn-reach-door.test.ts test/spawn-cell-socket.test.ts.Closes #7959