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

Merged opened this

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

Merged

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

opened this

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 gets FORGE_PARENT_LOCAL=http://127.0.0.1:<parent port>.
  • sendBeat in src/spawn/heartbeat.ts posts to that address when the variable is set. It only accepts the exact form http://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.ts are:

- 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=1 and 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=1 in forge.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 in forge.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.

Closes #6030

commented

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 gets FORGE_PARENT_LOCAL=http://127.0.0.1:<parent port>.
  • sendBeat in src/spawn/heartbeat.ts posts to that address when the variable is set. It only accepts the exact form http://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.ts are:

- 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=1 and 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=1 in forge.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 in forge.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.

Changes requested — before the latest push

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.

commented

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 to receiveBeat directly, so it never reads X-Forwarded-For and never goes through resolveClient. Every other path or method gets the same 404 no 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/forges and a sub-path of the heartbeat route with GET and POST, each with a spoofed X-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.
  • parentLocal and its validator, now called isParentLocal, sit together in beat-door.ts, and sendBeat uses the same validator.

forge.json

  • I set FORGE_REQUIRE_CELLS=1 on the test-shards step of both the fast and run check 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

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:

  1. 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.
  2. 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.
commented

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.ts is 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.ts sets it up and src/spawn/heartbeat.ts uses it, so a preview's cell child no longer shows as silent on /forges.
  • Test for the door: test/spawn-cell.test.ts now boots a cell child and checks that the parent records its heartbeat.
  • Skip becomes failure: "a real child forge" fails rather than prints SKIPPING: when FORGE_REQUIRE_CELLS is set. forge.json sets 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.sock is 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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs