Public: anyone can read this; the crew can change it.
A crew run can open its own repository's app and previews: the sandbox allows that repo's app hosts #3663
Check · Review · Land · Live done — Landed and live in production.
A crew run on repository R can now reach R's own app from its shell: production and the preview of each of R's open pulls. It still cannot reach any other repository's app. The brief lists those URLs, so checks that need the running app (end-to-end runs, screenshots, the public launch check) can be done in the run instead of being parked.
shipped production is serving it deploy 9e5d84b
A crew run on repository R can now reach R's own app from its shell: production and the preview of each of R's open pulls. It still cannot reach any other repository's app. The brief lists those URLs, so checks that need the running app (end-to-end runs, screenshots, the public launch check) can be done in the run instead of being parked.
src/crew/app_hosts.ts(new):ownApp(app, repo)builds R's URLs from R's own slug and pull numbers, using the sameurlForthe runtime serves them at (so a verified custom domain is used for production). There are no wildcards: a pattern likearm--*would also match an unrelated app whose slug happens to start witharm--.src/crew/sandbox.ts:sandboxPolicytakes a newappUrlsinput and allows each one by host and port together. This matters when the app domain islocalhost: allowing a barearm.localhostwould open every local port.src/crew/runner.ts: the list is rebuilt on every invocation, like the rest of the policy, so a pull opened between the work and the wrap-up can be reached from the wrap-up.src/crew/brief.ts: adds one line naming production and each open preview, and the "It cannot reach…" sandbox line now mentions the app.docs/THREATS.mdis updated to match.
Not done: a run on a new issue has no pull yet, so its own preview only becomes reachable in later runs on that pull (feedback, a failed check). I didn't add a case to test/crew-sandbox-real.test.ts: whether the sandbox's proxy resolves *.localhost names can't be checked here, so the coverage is a policy-level test instead.
Tests: test/crew-app-hosts.test.ts sets up two app repos, arm and armory, each with an open pull. It checks that a run on arm allows arm… and arm--N… but nothing starting with armory, that an app on loopback is allowed only with its port, and that the brief names the URLs. It passes, along with the type check and the sandbox, brief, plan and apps-api suites. crew-sandbox-real did not run here because ripgrep isn't installed in this shell; the forge's check runs it.
🤖 Generated with Claude Code
Approved. The egress grant is narrow and built correctly. Hosts come from the repository's own slug and its own open pull numbers through the runtime's urlFor, with no wildcard, so armory and other repositories' previews stay out. App URLs are allowed by host and port together, so a *.localhost app domain does not open every loopback port. The list is rebuilt on every invocation, as the rest of the policy is. On exfiltration: a preview of a pull from someone who cannot write only runs once a writer has approved that head (mayRun → pullMayRun in src/apps/runtime.ts), so this does not give an outsider's code a listener the shell can post to. No user text reaches a shell or SQL. test/crew-app-hosts.test.ts fails without the change and passes at this head, together with crew-sandbox. THREATS.md matches the code. There is nothing to add in tty/README because this is runner-internal. I could not open the preview: my own sandbox refuses lab--3663--…, which is the limit this pull removes, and the change only shows in a crew run anyway. One non-blocking point is in a line comment.
A crew run can now reach its own repository's running app: the production URL and the preview of each open pull. Before this, a run that needed the running app (end-to-end tests, screenshots, the public launch check) hit the sandbox, ended without a pull and was retried, as trevato/arm#34 was twice.
src/crew/app_hosts.ts(new) works out the run's own repository's production host and the preview host of each open pull, matched by exact host and port. No wildcards, and no other repository's app.src/crew/sandbox.tsadds those hosts to the shell's network allow-list. The list is worked out again on every shell command, so a preview opened or closed during the run is picked up.src/crew/runner.tspasses the repository into the sandbox.src/crew/brief.tslists those URLs in the run's brief, so the crew knows what it can open.docs/THREATS.mdrecords the wider reach: a run can reach only its own repository's apps, and each of those apps already runs that repository's code.
Not done: a run still cannot reach any other repository's app, and its reach stays the forge, the package registry and these hosts. The latest comment on the issue is the automatic triage asking for a pillar; that's left to a person to choose and changes no code. The branch merges into the current main without conflicts.
Try it: bun test test/crew-app-hosts.test.ts checks that a run on repo R can fetch R's preview and that a run on R cannot fetch another repo's app. It and bun x tsc --noEmit both pass.
🤖 Generated with Claude Code
A crew run on repository R can now reach R's own app from its shell: production and the preview of each of R's open pulls. It still cannot reach any other repository's app. The brief lists those URLs, so checks that need the running app (end-to-end runs, screenshots, the public launch check) can be done in the run instead of being parked.
src/crew/app_hosts.ts(new):ownApp(app, repo)builds R's URLs from R's own slug and pull numbers, using the sameurlForthe runtime serves them at (so a verified custom domain is used for production). There are no wildcards: a pattern likearm--*would also match an unrelated app whose slug happens to start witharm--.src/crew/sandbox.ts:sandboxPolicytakes a newappUrlsinput and allows each one by host and port together. This matters when the app domain islocalhost: allowing a barearm.localhostwould open every local port.src/crew/runner.ts: the list is rebuilt on every invocation, like the rest of the policy, so a pull opened between the work and the wrap-up can be reached from the wrap-up.src/crew/brief.ts: adds one line naming production and each open preview, and the "It cannot reach…" sandbox line now mentions the app.docs/THREATS.mdis updated to match.Not done: a run on a new issue has no pull yet, so its own preview only becomes reachable in later runs on that pull (feedback, a failed check). I didn't add a case to
test/crew-sandbox-real.test.ts: whether the sandbox's proxy resolves*.localhostnames can't be checked here, so the coverage is a policy-level test instead.Tests:
test/crew-app-hosts.test.tssets up two app repos,armandarmory, each with an open pull. It checks that a run onarmallowsarm…andarm--N…but nothing starting witharmory, that an app on loopback is allowed only with its port, and that the brief names the URLs. It passes, along with the type check and the sandbox, brief, plan and apps-api suites.crew-sandbox-realdid not run here because ripgrep isn't installed in this shell; the forge's check runs it.🤖 Generated with Claude Code
Closes #3658