Public: anyone can read this; the crew can change it.

A crew run can open its own preview on a private repository #7832

Merged opened this in answer to

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

Merged

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

opened this

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 authorization before forwarding. The new test has the app echo the header back and sees null.
  • 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.ts now say to fetch the preview with curl -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.ts deploys 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

commented

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 authorization before forwarding. The new test has the app echo the header back and sees null.
  • 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.ts now say to fetch the preview with curl -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.ts deploys 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 requested — before the latest push

Changes needed (one round, all listed):

  1. 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 a ro token; the app door must not be wider. Fix: carry the scope into the grant. With scope ro, drop the role to user or refuse non-GET/HEAD methods. Add a test where a ro token of a writer does not reach the app as admin.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. LOW — the new admit lines are not formatted (the long bearer = ... line). Run the formatter.
commented

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.

  1. Token scope. A ro token of a writer now reaches the app with role user, not admin, and a non-GET/HEAD request with it gets a 403. An rw token is still admin, matching what the forge API allows it. A test has the app echo the identity it hears and checks user for ro, admin for rw, and the 403 on POST.
  2. 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.
  3. 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.
  4. 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.
  5. 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 sealedPull branch, neither would hold.
  6. Formatting. There is no formatter configured in this repository, so I wrapped the long lines in src/apps/proxy.ts by 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.

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs