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

Merged opened this in answer to

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

Merged

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

opened this

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 MCP forges tool (reach), and in the result of POST /api/v1/forges and MCP spawn (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. forwardSocket in src/spawn/cell.ts carries upgrades to a cell's home and apps. It opens the cell's side before answering the visitor. test/spawn-cell-socket.test.ts covers it.
  • End-to-end test. test/spawn-cell-e2e.test.ts spawns 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.md describes all of this, and docs/MCP.md has 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.

Closes #7959

commented

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 MCP forges tool (reach), and in the result of POST /api/v1/forges and MCP spawn (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. forwardSocket in src/spawn/cell.ts carries upgrades to a cell's home and apps. It opens the cell's side before answering the visitor. test/spawn-cell-socket.test.ts covers it.
  • End-to-end test. test/spawn-cell-e2e.test.ts spawns 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.md describes all of this, and docs/MCP.md has 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.

commented

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 requested — before the latest push

Changes needed (first round, all in one):

  1. HIGH, SSRF from child-controlled data. slugsOf in src/spawn/reach.ts reads app.slug out of the child's own forge.db and appUrlOf interpolates it unvalidated into https://<slug>--<child>--<parent>/, which the parent then fetches. A child admin who writes a slug like evil.example# or u@host steers the parent's request to an arbitrary host. Fix: accept only slugs matching the forge's slug rule (reuse the validator behind slugFor in 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.
  1. 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: null to a fixture. Add a test in the TUI forges suite that a failing reach renders 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.
  1. 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.
  1. 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.
  1. Clean. FORGE_NO_REACH is a test-only switch read in production code (off); inject via useReach default in the test preload instead of an env var in src. Test file spawn-reach.test.ts has a module-level STANDBY Response duplicating standby(); 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.

commented

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.

  1. SSRF. slugsOf now keeps only slugs that pass isSlug, the rule behind slugFor in core/apps.ts. A hostile slug in a child's forge.db (evil.example#, u@host, a/b, UP) is dropped before any address is built. The new test in test/spawn-reach.test.ts asserts that no request goes to such a slug, and that every probed host is …--kid--forge.test or the child's home.
  2. 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.ts checks both the "Running, not reachable: …" line and the per-address lines. docs/features/child-reach.md says how to open it (ctrl-k, "Forges you sent").
  3. Design. The hand-rolled <ul> on /forges is gone. Each address is now a row() 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 in docs/design/areas/lists.md.
  4. 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, the src/http/server.ts cell door and test/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.
  5. Clean. FORGE_NO_REACH is gone from src. useReachByDefault({ off: true }) in test/helpers/preload.ts replaces it, and an injected fetch still turns proving back on. The duplicate STANDBY is removed from test/spawn-reach.test.ts.
Approved

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.

commented

The full-suite proof of this head is waiting: no check slot is spare. It starts when one frees.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs