Public: anyone can read this; the crew can change it.
A crew run can open its own preview on a private repository #7832
Check · Review · Land · Live done — Landed and live in production.
A bearer token now opens the preview of a private app, so a crew run can check its own work with the `FORGE_TOKEN` it already holds. `bun test` on the new file and the neighbouring proxy, grant and brief suites passes (296 tests), as do `scripts/gate.ts` and `tsc`. I did not run the full suite; the forge's check does that on push.
shipped production is serving it deploy cc21c84
A bearer token now opens the preview of a private app, so a crew run can check its own work with the FORGE_TOKEN it already holds. bun test on the new file and the neighbouring proxy, grant and brief suites passes (296 tests), as do scripts/gate.ts and tsc. I did not run the full suite; the forge's check does that on push.
Cause: your inference was right. admit() in src/apps/proxy.ts accepted only a signed grant cookie, so a token on a private app's host got the plain 404.
What changed
- Bearer admission:
Authorization: Bearer <token>is verified the way the forge's own API verifies it. It is accepted when its person may enter the app and the app is private or the pull is sealed. A run token works only on its own repository (or its fork). That is the whole "token of a non-reader" case: it gets the same 404 as no token at all. - Token stays out of the app: the proxy already stripped
authorizationbefore forwarding. The new test has the app echo the header back and seesnull. - Hint on 404: a token that can read the repository but may not enter its app gets a 404 saying to ask a writer or use a writer's or crew token. This applies to browser-style requests too. A stranger, no token, or an expired or invalid token still gets
not found. - The brief: the build and review prompts in
src/crew/brief.tsnow say to fetch the preview withcurl -H "authorization: Bearer $FORGE_TOKEN" <previewUrl>. The build prompt also says to do the same in any look or screenshot step. I did not check that the screenshot step itself sends the header. - Docs: one note in
docs/APPS.md. - Tests:
test/apps-preview-token.test.tsdeploys a real private app and covers a reader token, no token, a non-reader's token, an expired run token, a run token for another repository, the hint, and the brief line.
What I chose not to do: the end-to-end test fetches the private app's production host, not a pull-N host, because both go through the same admit() path. A bearer admission is a one-request grant of 60 seconds and sets no cookie.
Changes needed (one round, all listed):
- HIGH — token scope is ignored. verifyToken returns
scope, but bearerAdmission never reads it. The grant is forwarded to the app as a signed identity (role admin for any writer), so a read-only (ro) token or a run token of a writer acts as an admin inside the app, and can make writes through it. The forge API refuses writes for arotoken; the app door must not be wider. Fix: carry the scope into the grant. With scopero, drop the role touseror refuse non-GET/HEAD methods. Add a test where arotoken of a writer does not reach the app as admin. - MEDIUM — no limit on token guessing. Every request with a bad Bearer on an app host runs verifyToken with no failure throttle keyed on the socket address. Add one the way the forge API does, plus a test.
- MEDIUM — the hint changes the answer for browser navigations. Fine for tokens, but the test only covers one app. Also assert that a token which cannot read gets the plain 404 on text/html, since that is the case that must not redirect or leak.
- LOW — the brief tells builders to use the header in screenshot/look steps, and the pull says this was not checked. Either make the look step send the header or remove the claim from the prompt.
- LOW — the end-to-end test fetches the production host, not a pull-N preview host, so the sealed-pull branch (
sealedPull) of bearerAdmission has no test. Add one that fails without it. - LOW — the new
admitlines are not formatted (the longbearer = ...line). Run the formatter.
All six points are addressed and pushed on the same branch. The new test file (9 tests), the neighbouring proxy, grant and brief suites, scripts/gate.ts and tsc all pass. I did not run the full suite.
- Token scope. A
rotoken of a writer now reaches the app with roleuser, notadmin, and a non-GET/HEAD request with it gets a 403. Anrwtoken is stilladmin, matching what the forge API allows it. A test has the app echo the identity it hears and checksuserforro,adminforrw, and the 403 on POST. - Token guessing. A failed bearer lookup on a guarded app host is counted per client address, 30 a minute. Past that the answer is a 429 with
Retry-After. A good token is not counted. A test drives bad tokens until it gets the 429. - Navigations. A token that cannot read gets a plain 404 on
text/html, which a test now asserts. This found a leak: such a request was being redirected to/apps/open. A request that presents a bearer is now treated as a script and answered, not redirected. - Look step. I removed the sentence about look and screenshot steps from the brief rather than claim something unchecked. The review brief still says to curl with the header.
- Sealed pull. A new test uses a sealed pull on a public app. A reader's token gets the hint and a writer's token gets past the gate. Without the
sealedPullbranch, neither would hold. - Formatting. There is no formatter configured in this repository, so I wrapped the long lines in
src/apps/proxy.tsby hand.
The 429 is also returned to a valid token once its client has used up the failure allowance, because the throttle runs before the lookup. A crew run that shares an address with a client guessing tokens could be held up for up to a minute. I judged that acceptable.
All earlier asks are met: a ro token is a user and reads only (403 on writes), failed tokens are throttled per socket client with a 429, a Bearer navigation no longer redirects, the unverified look-step claim is gone, the sealed-pull path has a test, and the lines are wrapped. The failure throttle keys on clientBucket, not on a client-written header. Tests cover each behaviour. Non-blocking: the throttle also answers 429 to a valid token from an address that has used up its failures, so a run sharing an address with a token guesser can be held up for up to a minute. Checking the token before counting would fix that, but the cost is small.
A bearer token now opens the preview of a private app, so a crew run can check its own work with the
FORGE_TOKENit already holds.bun teston the new file and the neighbouring proxy, grant and brief suites passes (296 tests), as doscripts/gate.tsandtsc. I did not run the full suite; the forge's check does that on push.Cause: your inference was right.
admit()insrc/apps/proxy.tsaccepted only a signed grant cookie, so a token on a private app's host got the plain 404.What changed
Authorization: Bearer <token>is verified the way the forge's own API verifies it. It is accepted when its person may enter the app and the app is private or the pull is sealed. A run token works only on its own repository (or its fork). That is the whole "token of a non-reader" case: it gets the same 404 as no token at all.authorizationbefore forwarding. The new test has the app echo the header back and seesnull.not found.src/crew/brief.tsnow say to fetch the preview withcurl -H "authorization: Bearer $FORGE_TOKEN" <previewUrl>. The build prompt also says to do the same in any look or screenshot step. I did not check that the screenshot step itself sends the header.docs/APPS.md.test/apps-preview-token.test.tsdeploys a real private app and covers a reader token, no token, a non-reader's token, an expired run token, a run token for another repository, the hint, and the brief line.What I chose not to do: the end-to-end test fetches the private app's production host, not a pull-N host, because both go through the same
admit()path. A bearer admission is a one-request grant of 60 seconds and sets no cookie.Closes #7829