Public: anyone can read this; the crew can change it.
A writer's shell reaches the repository's own production and previews when its machine says `apps`, and nothing else #9943
Check · Review · Land · Live done — Landed and live in production.
A person's shell now reaches the repository's own production and its previews when its machine says `apps`. Before this, the shell's cell could reach only the forge.
shipped production is serving it deploy f21b126
A person's shell now reaches the repository's own production and its previews when its machine says apps. Before this, the shell's cell could reach only the forge.
What changed
src/machines/shell.ts: a new pure function,shellReach(forgeUrl, machine, appUrls), decides what the cell may open: the forge, plusreachableApps(machine, ownAppUrls(ownApp(app, repo))). That is the same list of the repository's own app URLs the crew's runs get.shellCelltakes those URLs, andstart()passes them in. A machine withreach: []gets the forge alone.- The URLs go into the cell's
reach, whichopenCellturns into exact host-and-port origins in the egress proxy. No patterns are allowed. Theegresslist stays empty andhidestill covers the forge's data whatever the machine says.src/apps/egress.ts,src/apps/contain.tsandsrc/crew/machines_read.tsdid not need changes. docs/features/machines.md: the shell's "Where" bullet now says what it reaches, and the clause is gone from "Not done yet".
Two departures from the issue
standardgets apps reach. The issue says nothing should change forstandard, but the built-instandardmachine declaresreach: ["apps"](src/crew/machines.ts). I followed the issue's own rule that what the machine declares draws the boundary, so a shell onstandardnow reaches its repository's apps too. Ifstandardshells should stay forge-only,standard's declaration has to change. This PR doesn't change it.- Private addresses aren't checked for these hosts. As origins, the app hosts skip the egress proxy's check that refuses private addresses, just as the forge's own URL does. Hosts on the forge's apps domain can resolve to private addresses inside the forge's network, and the public-only check would refuse them. The cost: if a verified custom production domain pointed at a private address, the shell could reach that address on the domain's exact port.
Tests
- New tests in
test/crew-app-hosts.test.tsuse the existing two-app fixture (arm, plusarmorybelonging to another repository). They check the cell's reach forapps(the repository's own production and preview, nothing fromarmory, data still hidden), forreach: [](the forge alone) and forstandard. They fail on main becauseshellReachdoesn't exist there. - Passing:
bun scripts/gate.ts,bun x tsc --noEmit, and eight test files covering machines, the shell,containand features. - In that combined run, one test in
test/machine-shell.test.tsfailed once ("a reload's first size redraws the screen…"). The file passed 3 of 3 times on its own with this change. It's a timing test with nothing to do with reach, so I read it as a flake under load. - The cell suites are skipped under
FORGE_TEST_SOCKET, so the forge's check is what will run them. scripts/fast.tslisted 1,511 test files, far over its budget, so I ran only the files that cover this change.
To try it: on this pull's preview, sign in as a writer of an app repository and open /<owner>/<repo>/settings/machines. Press Open shell on standard and curl the production or a preview URL; it answers. Another repository's app is refused. In this sandbox the shell runs uncontained, so I couldn't check this end to end.
🤖 Generated with Claude Code
A person's shell can now reach the repository's own production and its previews when its machine says apps. Before this, the shell's cell could reach only the forge. This push also fixes a flaky test that failed the last check.
What changed
src/machines/shell.ts: a new pure function,shellReach(forgeUrl, machine, appUrls), decides what the cell may open: the forge, plusreachableApps(machine, ownAppUrls(ownApp(app, repo))). That is the same list of the repository's own app URLs the crew's runs get.shellCelltakes those URLs andstart()passes them in, so a machine withreach: []gets the forge alone. The URLs go into the cell'sreach, whichopenCellturns into exact host-and-port origins in the egress proxy, with no patterns.egressstays empty, andhidestill covers the forge's data whatever the machine says.docs/features/machines.md: the shell's "Where" bullet says what it reaches, and the clause is gone from "Not done yet".test/crew-app-hosts.test.ts: covers the forge alone forreach: [], the repository's own production and previews forapps, and never another repository's app.test/web-commits-check.test.ts: this file made seven anonymous requests for deep pages (/commits,/commit/…). Strangers get 30 of those per minute per address, cut to 7 while the event loop runs late (src/http/crawl.ts). In a loaded 40-file shard, the eighth request was refused, which broke "the list has no card markup…" and "a commit's page has no card…". The test was wrong, not the code: it now reads as the signed-in owner, and stays anonymous only where the private-repository test needs a stranger. Forcing the "behind" state throughsetCrawlClockmakes the old file fail those same two tests; the new file passes.- The branch merges the current
main.
Not done: the built-in standard machine declares reach: ["apps"] (src/crew/machines.ts), so its shells get apps reach too, although the issue says standard should not change. I followed the declaration rather than add a special case.
Checks: bun scripts/gate.ts and bun x tsc --noEmit pass, and both test files above pass (15 tests). scripts/fast.ts named 1512 files after the merge with main, too broad to run here, so the full suite is left to the forge's check.
To try it: open a shell on a machine whose reach includes apps and curl the repository's production or a preview URL. It answers; another repository's app and other hosts are refused.
🤖 Generated with Claude Code
Shell reach is a pure function (shellReach), fed by the same ownAppUrls the crew uses, exact origins only, egress stays empty, data stays hidden; docs updated in machines.md (not README). Tests fail on main. I did not exercise a contained shell: the cell suites skip here. Two next things: (1) decide on standard: it declares reach apps, so its shells now reach the repo apps, which contradicts box 1 ("standard: nothing changes"); either change standard or amend the issue, and add a test pinning the choice. (2) The web-commits-check change is an unrelated flake fix riding in this pull; split such fixes out next time. Also rewrap the header comment in shell.ts, which now has an over-long line.
Left #9934 open: 1 unticked item remain (When the default branch's forge.json gives the shell's role a machine with reach: ["apps"], the shell's cell can open its repository's production and preview hosts through the same egress allowance the crew's runs use (src/apps/egress.ts). With reach: [] or standard, nothing changes.).
A person's shell now reaches the repository's own production and its previews when its machine says
apps. Before this, the shell's cell could reach only the forge.What changed
src/machines/shell.ts: a new pure function,shellReach(forgeUrl, machine, appUrls), decides what the cell may open: the forge, plusreachableApps(machine, ownAppUrls(ownApp(app, repo))). That is the same list of the repository's own app URLs the crew's runs get.shellCelltakes those URLs, andstart()passes them in. A machine withreach: []gets the forge alone.reach, whichopenCellturns into exact host-and-port origins in the egress proxy. No patterns are allowed. Theegresslist stays empty andhidestill covers the forge's data whatever the machine says.src/apps/egress.ts,src/apps/contain.tsandsrc/crew/machines_read.tsdid not need changes.docs/features/machines.md: the shell's "Where" bullet now says what it reaches, and the clause is gone from "Not done yet".Two departures from the issue
standardgets apps reach. The issue says nothing should change forstandard, but the built-instandardmachine declaresreach: ["apps"](src/crew/machines.ts). I followed the issue's own rule that what the machine declares draws the boundary, so a shell onstandardnow reaches its repository's apps too. Ifstandardshells should stay forge-only,standard's declaration has to change. This PR doesn't change it.Tests
test/crew-app-hosts.test.tsuse the existing two-app fixture (arm, plusarmorybelonging to another repository). They check the cell's reach forapps(the repository's own production and preview, nothing fromarmory, data still hidden), forreach: [](the forge alone) and forstandard. They fail on main becauseshellReachdoesn't exist there.bun scripts/gate.ts,bun x tsc --noEmit, and eight test files covering machines, the shell,containand features.test/machine-shell.test.tsfailed once ("a reload's first size redraws the screen…"). The file passed 3 of 3 times on its own with this change. It's a timing test with nothing to do with reach, so I read it as a flake under load.FORGE_TEST_SOCKET, so the forge's check is what will run them.scripts/fast.tslisted 1,511 test files, far over its budget, so I ran only the files that cover this change.To try it: on this pull's preview, sign in as a writer of an app repository and open
/<owner>/<repo>/settings/machines. Press Open shell onstandardandcurlthe production or a preview URL; it answers. Another repository's app is refused. In this sandbox the shell runs uncontained, so I couldn't check this end to end.🤖 Generated with Claude Code
Closes #9934