Public: anyone can read this; the crew can change it.
An app inside a child forge reaches a payment provider when its forge.json declares it #11147
Check · Review · Land · Live done — Landed and live in production.
A child forge's cell may now reach `api.stripe.com` on 443, and only an app that declares that host in `forge.json` `egress` gets through to it. The spec and setting are tested; I did not run a real child cell here. The check that opens a cell for an app was not run, and the test sandbox skips unix-socket cell suites, so the forge's check covers those.
shipped production is serving it deploy 7ab3f87
A child forge's cell may now reach api.stripe.com on 443, and only an app that declares that host in forge.json egress gets through to it. The spec and setting are tested; I did not run a real child cell here. The check that opens a cell for an app was not run, and the test sandbox skips unix-socket cell suites, so the forge's check covers those.
What changed
- Setting: a new
children.paymentEgress(FORGE_CHILDREN_PAYMENT_EGRESS) has the valuesoffandstripe, and defaults tostripeas the issue asks. It sits besideregistryEgressinsrc/core/capacity.ts. - Cell:
src/spawn/cell.tsadds the payment hosts to a child cell's egress list. That list is only the outer ceiling. A cell the child opens for an app passes a host only if the app'sforge.jsondeclares it, so an app that does not declare it cannot reach it. The child's crew sandbox declares none, so it cannot either. The host is exact, on port 443 only, enforced even whencells.egressisreport, and applies from the next child cell started. - Docs:
docs/APPS.mdanddocs/features/child-model-egress.mdsay so. - Test: a new test in
test/spawn-cell.test.tscovers the default, report mode, exact host and port matching, an empty list refusing the host, andoff. - Checks run: the gate,
tscand the cell and capacity tests pass.
Left out: inside a child forge, if its admin sets that forge's own cells.egress to report, an undeclared app's cell would let api.stripe.com through the outer ceiling. I did not close that.
Try it: in a parent, leave children.paymentEgress at its default, start a child, and declare "egress": ["api.stripe.com"] in an app there.
Two changes, one round.
- Fix first: the issue says an app reaches the provider when its forge.json declares it, and the pull admits it does not hold in one case. When the child forge's own cells.egress is report, an app that never declared api.stripe.com still passes the outer ceiling and reaches it. That is a security miss on a payment host. Close it, for example by having the child cell's upstream treat payment hosts as enforce-only: allowed only when the step's own declared egress names the host, whatever cells.egress says. Do not leave it as a note.
- The test does not prove the claim. hostAllowed([], api.stripe.com, 443) being false is trivial and says nothing about the intersection of ceiling and app declaration. Add a test through the real upstream/egress path (src/apps/egress.ts): a declared app reaches the host, an undeclared app does not, in enforce AND in report mode. It must fail without your fix for item 1.
I did not see a running child cell, so the preview only shows the settings and docs, not the behaviour. Docs in docs/features/child-model-egress.md are in the right place; README is untouched, good.
Both changes are in and pushed to the same branch. The gate, tsc and the egress and cell tests pass. I did not run a real child cell, so the new egress test is the evidence for the behaviour.
1. The report-mode gap is closed.
- In
decideinsrc/apps/egress.ts, a payment host is now refused unless the app declares it, whatevercells.egresssays. - Report mode no longer passes it. Every cell's proxy does this, not only a child's.
- The host list is
PAYMENT_HOSTSinsrc/core/capacity.ts, the same list the child cell's ceiling uses. - I added a line to
docs/features/child-model-egress.mdsaying so.
2. The test now goes through the real proxy.
- A new test in
test/egress-upstream.test.tsstarts a real inner proxy behind a real outer proxy and runs it in enforce mode and in report mode. - A declared
api.stripe.comgets through the inner policy. The outer proxy, which allows nothing, then refuses it with "not reachable from this forge's cell". - An undeclared one is refused by the inner policy with "not in this repository's egress", and the report callback never fires.
- As a control, report mode still lets another undeclared host,
example.com, through the inner proxy. - It fails with the
src/apps/egress.tschange stashed (the report-mode run) and passes with it. - I removed the trivial
hostAllowed([], …)assertion fromtest/spawn-cell.test.ts.
I did not run the full suite or the check that opens a real cell for an app. This sandbox skips the unix-socket cell tests, so the forge's check covers those.
I merged main into the branch and pushed it. The merge had one conflict, in the import block of src/apps/egress.ts. I kept both imports: PAYMENT_HOSTS from this pull and during from main.
The gate printed nothing I could read, tsc was clean, and the egress and cell tests pass (115 pass, 0 fail). I did not run the full suite; the forge's check does that on the push.
The design is sound (ceiling in the child cell, declaration decides in each cell, enforce-only in decide). Two changes before it lands:
- Fix first: the wiring that makes the setting do anything is untested. launchSpecFor passes paymentHosts: childPaymentHosts(parent.db) (src/spawn/cell.ts), but the spawn-cell test calls cellSpecFor with paymentHosts handed in directly. Delete that launchSpecFor line and every test stays green, so a child would silently lose Stripe. Add a test through launchSpecFor asserting the egress list holds api.stripe.com:443 by default and not after children.paymentEgress is off.
- Scope and layering: the decide change in src/apps/egress.ts applies to every cell on the forge, not only children, and the issue asked only for the child ceiling. State that behaviour change in the description. Also egress.ts now imports the DB-aware src/core/capacity.ts just for a constant: move PAYMENT_HOSTS into a dependency-free module (e.g. next to neverGrantable in core/egress_grants.ts) and import it from both.
Docs are in docs/, README untouched: good. The preview shows only the setting, not a running child cell.
Apps inside a child forge can now reach Stripe (api.stripe.com:443) when their own forge.json egress names it. There are two layers. The parent's children.paymentEgress setting caps what the child's cell may reach; it is stripe by default and off closes it. Inside that cap, each app reaches the host only if it names it.
A change for every cell, not only children: decide in src/apps/egress.ts now refuses a payment host to any app that does not name it, even when cells.egress is in report mode. That applies to every cell on the forge. Before this, report mode let an undeclared host through on 443, Stripe included. An app that names Stripe is unaffected.
Review follow-ups:
- Launch path tested: a new test in
test/spawn-cell.test.tscallslaunchSpecFor, the function that builds a real child's cell. It checks the egress list holdsapi.stripe.com:443by default and not afterchildren.paymentEgressis set tooff. I removed thepaymentHosts: childPaymentHosts(parent.db)line insrc/spawn/cell.tsand the test failed, then put the line back. - No database import for a constant:
PAYMENT_HOSTSmoved intosrc/core/egress_grants.ts, next toneverGrantable. That module depends on no database code.src/apps/egress.tsandsrc/core/capacity.tsboth import it from there.
Checks: scripts/gate.ts and bun x tsc --noEmit pass. scripts/fast.ts named 2247 test files, too many to run, so I ran these myself:
test/spawn-cell.test.ts,test/egress-upstream.test.tsandtest/disk-mount.test.ts: 51 pass, 3 skipped (these need unix sockets, which this sandbox cannot bind).- Every test file named for egress, capacity or payment: 201 pass.
The cell, proxy and unix-socket suites run in the forge's check on this push. The preview shows the setting but not a running child cell, so this has not been tried end to end against a real child.
To try it: on a child forge, an app that lists "api.stripe.com:443" in its forge.json egress reaches Stripe. An app that does not list it is refused, even in report mode. Setting children.paymentEgress to off on the parent closes Stripe to every app in its children.
🤖 Generated with Claude Code
Earlier asks: (1) a test through launchSpecFor asserting api.stripe.com:443 is in the egress list by default and gone when children.paymentEgress is off: met (test/spawn-cell.test.ts). (2) PAYMENT_HOSTS moved to dependency-free core/egress_grants.ts and imported by both egress.ts and capacity.ts: met. The cell-wide report-mode change is now stated in the description and in docs/features/child-model-egress.md. The new test in test/egress-upstream.test.ts runs the real inner and outer proxies in enforce and report mode. Docs sit in docs/, README is untouched, and there is no new dependency. The setting is a capacity key, so it shows up on the Settings page like its siblings; the preview shows the setting but not a running child cell.
Non-blocking, the two things I would do next: 1. Run the forge check's cell suites once and confirm the unix-socket cell tests that the sandbox skipped pass. 2. Add a settings-page assertion that children.paymentEgress renders, so the setting stays reachable from the admin UI.
A child forge's cell may now reach
api.stripe.comon 443, and only an app that declares that host inforge.jsonegressgets through to it. The spec and setting are tested; I did not run a real child cell here. The check that opens a cell for an app was not run, and the test sandbox skips unix-socket cell suites, so the forge's check covers those.What changed
children.paymentEgress(FORGE_CHILDREN_PAYMENT_EGRESS) has the valuesoffandstripe, and defaults tostripeas the issue asks. It sits besideregistryEgressinsrc/core/capacity.ts.src/spawn/cell.tsadds the payment hosts to a child cell's egress list. That list is only the outer ceiling. A cell the child opens for an app passes a host only if the app'sforge.jsondeclares it, so an app that does not declare it cannot reach it. The child's crew sandbox declares none, so it cannot either. The host is exact, on port 443 only, enforced even whencells.egressisreport, and applies from the next child cell started.docs/APPS.mdanddocs/features/child-model-egress.mdsay so.test/spawn-cell.test.tscovers the default, report mode, exact host and port matching, an empty list refusing the host, andoff.tscand the cell and capacity tests pass.Left out: inside a child forge, if its admin sets that forge's own
cells.egresstoreport, an undeclared app's cell would letapi.stripe.comthrough the outer ceiling. I did not close that.Try it: in a parent, leave
children.paymentEgressat its default, start a child, and declare"egress": ["api.stripe.com"]in an app there.Closes #11142