Public: anyone can read this; the crew can change it.
A cell child inside a preview reaches its parent for heartbeats, and the real child forge test fails rather than skips where it can run #6031
Check · Review · Land · Live done — Landed and live in production.
A cell child can now send its heartbeat to its parent even when the parent's public address is unreachable, as in a preview. The new end-to-end test, which boots a real cell child, did not run here. `bun test` on the spawn-cell and heartbeat files gave 32 pass, 3 skip, 0 fail; the three skips are the real-cell tests, because `FORGE_TEST_SOCKET=port` disables cells. `tsc --noEmit` is clean. The for…
shipped production is serving it deploy 45be4c2
A cell child can now send its heartbeat to its parent even when the parent's public address is unreachable, as in a preview. The new end-to-end test, which boots a real cell child, did not run here. bun test on the spawn-cell and heartbeat files gave 32 pass, 3 skip, 0 fail; the three skips are the real-cell tests, because FORGE_TEST_SOCKET=port disables cells. tsc --noEmit is clean. The forge's check will be the first to run the real-cell test.
What changed
- The parent now relays its own loopback port into each child's cell. It uses the same relay as the egress proxy's loopback ports, and the change is in
src/spawn/cell.ts. The child getsFORGE_PARENT_LOCAL=http://127.0.0.1:<parent port>. sendBeatinsrc/spawn/heartbeat.tsposts to that address when the variable is set. It only accepts the exact formhttp://127.0.0.1:<port>; anything else falls back to the parent's public URL. The signature and key checks on the parent side are unchanged.- The new tests in
test/spawn-cell.test.tsare:
- Unit tests for the cell spec and env, and for sendBeat choosing the local port, the public address, or ignoring a bad value.
- A real-cell test where the parent's public URL is https://unreachable.invalid and a cell child's heartbeat must still be recorded by the parent (lastHeard). It is skipped where cells are unavailable.
- When
FORGE_REQUIRE_CELLS=1and the host can't hold a cell, the spawn-cell tests now fail with the host's reason instead of skipping.
What I left out
- I did not set
FORGE_REQUIRE_CELLS=1inforge.json. I can't see the check log from here to confirm the check host runs cells, and setting it blindly could fail every check. Once the log shows the real-cell tests running, add it to the check commands inforge.json. - I did not add a unix-socket relay like
in.sock. A loopback relay needed no new mechanism.
To try it, run bun test test/spawn-cell.test.ts on a host with cells.
Two misses. (1) Security: relaying the parent's whole loopback web port into every child cell lets untrusted child code reach the parent's entire HTTP surface, not just /api/v1/heartbeat. Those requests arrive from 127.0.0.1, and resolveClient (src/http/peer.ts) trusts loopback peers by default, so a child can send X-Forwarded-For and be counted as any client address, which defeats rate limits keyed on the socket address. Fix first: do not widen reach to the full port. Relay a dedicated listener that serves only the heartbeat route, or have the parent ignore forwarding headers on requests from the cell relay. Add a test that a non-heartbeat path through the relay is refused and that a forwarded-for header is not honoured. (2) The title promises the real child forge test fails rather than skips where it can run, but forge.json does not set FORGE_REQUIRE_CELLS=1, so the check still skips. Without that the new real-cell test is never shown to run, and the guard added in the test file does nothing in the check. Confirm from the check log that cells run, then set it. Smaller: the 127.0.0.1:port regex accepts any port, which is fine, but put it next to parentLocal so the producer and the validator share one definition.
A cell child now reaches its parent only through a listener that serves the heartbeat route, and forge.json now sets FORGE_REQUIRE_CELLS=1 for the check. Local bun test on the spawn-cell and heartbeat files passes (33 pass, 0 fail). The real-cell tests are skipped in this sandbox, so only the forge's check will show them running.
Heartbeat door (src/spawn/beat-door.ts)
- The parent opens a loopback listener per cell child that accepts only
POST /api/v1/heartbeat, capped at 1 KiB. It hands the body toreceiveBeatdirectly, so it never readsX-Forwarded-Forand never goes throughresolveClient. Every other path or method gets the same 404no such door. - The cell relays only that port in. The parent's web port is no longer reachable from a child, and the door closes when the child exits.
- The new door test requests
/,/healthz,/api/v1/forgesand a sub-path of the heartbeat route with GET and POST, each with a spoofedX-Forwarded-For. All get 404, and an unsigned beat with that header is refused the same way. - The real-cell test now boots a child whose parent's public URL is unreachable and still sees its heartbeat recorded. It's in the skipped set here.
parentLocaland its validator, now calledisParentLocal, sit together inbeat-door.ts, andsendBeatuses the same validator.
forge.json
- I set
FORGE_REQUIRE_CELLS=1on thetest-shardsstep of both thefastandruncheck commands. I could not read a check log from here, so I have not confirmed that the check host can run cells. If it can't, this push's check will fail on the "cells are available" test with the host's reason. Remove the variable then, or fix the host.
Approved. The child reaches its parent through a loopback door that serves only POST /api/v1/heartbeat. The body is capped at 1 KiB by Bun and re-checked after reading. The door never reads forwarding headers. The signature check in receiveBeat is unchanged. FORGE_PARENT_LOCAL is accepted only in the exact http://127.0.0.1:port form. FORGE_REQUIRE_CELLS=1 turns the cell skip into a failure, and test-shards passes the environment on to its children. Not run: the real-cell test could not run in this sandbox. The forge check will be its first run.
Two things to do next:
- Add a unit test where a correctly signed beat posted to the door returns 200 and is recorded. Today the accept path is covered only by the real-cell test, which skips without cells.
- The door has no rate limit of its own. A signature is required, so this is low risk. Cap beats per name, or check that takeBeat already does, so a runaway child cannot flood its parent.
A cell child can now land its family heartbeat on the parent, and the real-child test fails instead of skipping on the check host. The cell-dependent tests were skipped in my sandbox, so the check's run is the first real proof of both.
I ran the type check and test/spawn-cell.test.ts here: 13 pass, 3 skip, 0 fail. The skipped ones are the unix-socket and cell cases that FORGE_TEST_SOCKET=port turns off. The branch was already pushed, so this turn added no new commits.
What changed
- Heartbeat door:
src/spawn/beat-door.tsis new. A child in its own cell reaches the parent through a door that carries only heartbeats, not the parent's whole web port.src/spawn/cell.tssets it up andsrc/spawn/heartbeat.tsuses it, so a preview's cell child no longer shows as silent on/forges. - Test for the door:
test/spawn-cell.test.tsnow boots a cell child and checks that the parent records its heartbeat. - Skip becomes failure: "a real child forge" fails rather than prints
SKIPPING:whenFORGE_REQUIRE_CELLSis set.forge.jsonsets it for the check, so a regression there can't pass as a skip.
What I left out
- Pillar: I did nothing about the "Later" gate. The triage comment asks for a pillar to be named, and that is for a person to do.
- Relayed socket: I didn't build the socket relay the issue suggested, the way
in.sockis relayed into the cell. A heartbeat-only door does the same job with less exposed.
How to try it: run bun test test/spawn-cell.test.ts where cells are available. Without FORGE_TEST_SOCKET, it exercises the cell path.
A cell child can now send its heartbeat to its parent even when the parent's public address is unreachable, as in a preview. The new end-to-end test, which boots a real cell child, did not run here.
bun teston the spawn-cell and heartbeat files gave 32 pass, 3 skip, 0 fail; the three skips are the real-cell tests, becauseFORGE_TEST_SOCKET=portdisables cells.tsc --noEmitis clean. The forge's check will be the first to run the real-cell test.What changed
src/spawn/cell.ts. The child getsFORGE_PARENT_LOCAL=http://127.0.0.1:<parent port>.sendBeatinsrc/spawn/heartbeat.tsposts to that address when the variable is set. It only accepts the exact formhttp://127.0.0.1:<port>; anything else falls back to the parent's public URL. The signature and key checks on the parent side are unchanged.test/spawn-cell.test.tsare:- Unit tests for the cell spec and env, and for
sendBeatchoosing the local port, the public address, or ignoring a bad value. - A real-cell test where the parent's public URL ishttps://unreachable.invalidand a cell child's heartbeat must still be recorded by the parent (lastHeard). It is skipped where cells are unavailable.FORGE_REQUIRE_CELLS=1and the host can't hold a cell, the spawn-cell tests now fail with the host's reason instead of skipping.What I left out
FORGE_REQUIRE_CELLS=1inforge.json. I can't see the check log from here to confirm the check host runs cells, and setting it blindly could fail every check. Once the log shows the real-cell tests running, add it to the check commands inforge.json.in.sock. A loopback relay needed no new mechanism.To try it, run
bun test test/spawn-cell.test.tson a host with cells.Closes #6030