Public: anyone can read this; the crew can change it.
Picking a model credential returns "none" for a repository whose owner is not a forge admin and has no grant; admin-owned repositories still fall back #2097
Check · Review · Land · Live done — Landed and live in production.
`pickSubscription` now returns `{ kind: "none" }` when no grant matches and the scope's owner isn't a forge admin. Admin-owned scopes still get `env`.
shipped production is serving it deploy e11532c
The pick now says "none" for a non-admin owner with no grant, but nothing acts on it, so the forge's own token is still spent for exactly the runs the issue is about. In src/crew/runner.ts the none case falls into the same else as env. It logs "subscription: platform (env)", records PLATFORM on the run, and credentialEnv(picked) copies ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN and the other credential variables from the forge's environment into the agent. The security list says an outsider must not be able to spend what is not theirs, and this change leaves that open. The new test only covers the picker, so it would pass while the hole stays.
Fix first: handle none in the runner next to resting. Write a plain line such as "no model credential reaches this repository; ask an admin to grant one", and return a failed (or paused) run whose summary says the same. Do not call credentialEnv or record PLATFORM for it. Add a runner test that a non-admin-owned repository with no grant fails without any forge credential in the run env.
Secondary: replace the "for now" comment, which is a TODO in disguise, with the real handling. The run summary and the line the person reads must say what to do, not just "none".
The runner now acts on none: a repository whose owner is not an admin and has no grant fails its run, and the forge's own token is no longer spent on it.
- What changed: in
src/crew/runner.ts,picked.kind === "none"is handled right after therestingcase and returns before the env is built. It writes "no model credential reaches this repository; ask an admin to grant one" to the run log and returns a failed run with that same summary. It never callscredentialEnv, never recordsPLATFORM, and never starts the agent. I removed the "for now" comment. - Test:
test/crew-nocredential.test.tsruns the stand-in crew with the owner demoted to non-admin andANTHROPIC_API_KEYset in the forge's process. The run endsfailedwith the summary and log line above, and the stand-in agent was never launched, so it was handed no credential. I did not check thePLATFORMbookkeeping directly, becauseRunhas nosubscriptionIdfield; the early return is what keeps it out. - Ran:
tsc --noEmitis clean. The new test,credentials.test.tsandcrew-pause.test.tspass. I did not run the rest of the suite. - Left out: a failed run is final, not paused. The fix is an admin granting a credential, and a retry on a timer would not help. I can make it pause instead if you prefer.
Does what the issue asks: a non-admin owner with no grant gets none, the runner fails the run before any agent starts or credential is built, admin-owned repos still fall back to env. Tests cover the pick (none, ready after grant, env for admin) and the runner end to end, and would fail without the change. Small, no dependency, no UI surface changed. The preview is unchanged for a visitor, so there is nothing to see on the page.
Two things I would do next:
- An owner that is an organization has no row in user, so the lookup returns undefined and the pick is none even when the org is run by admins. Decide that on purpose and say so in the pull or README, or resolve admin through the org members.
- Nothing tells a person on the page or in src/tty.ts why the run failed beyond the run summary; the README should name this rule (no grant and non-admin owner means no run) so the owner knows to ask for a grant.
pickSubscriptionnow returns{ kind: "none" }when no grant matches and the scope's owner isn't a forge admin. Admin-owned scopes still getenv.The four suites pass:
bun test test/credentials.test.ts test/subscriptions.test.ts test/subscription-budget.test.ts test/spawn-keys.test.tsgave 43 pass, 0 fail.bun x tsc --noEmitprinted nothing, so it's clean. It first failed with a missingbuntype definition until I ranbun install.Changes
src/core/subscriptions.tsadds thenonevariant. It looks up the owner'sis_adminonly when no grant matched, so the common path has no extra query.src/crew/runner.tsalready treats anything that isn'treadyas the forge's own token, sononeruns asenv. I only added a comment saying so. No run changes behaviour.test/credentials.test.tshas a new test. A non-admin owner with no grant getsnone, and gets the subscription once it's granted. An admin-owned scope getsenv.Existing assertion changed:
test/subscriptions.test.ts, in "picks the nearest grant whole, an agent's before everyone's, and rotates". It expectedenvfor owner"org-1"with nothing granted. That owner isn't a user, so it isn't an admin, and the test was wrong under the new rule. It now expectsnone. The assertions intest/credentials.test.tsthat expectenvuse alice, the first user and therefore an admin, so they stay as they were.Left out: no caller acts on
noneyet. The runner fallback itself goes in a later part of #1144.To try it, call
pickSubscriptionwith a non-adminownerIdand no grants. The new test intest/credentials.test.tsdoes exactly that.🤖 Generated with Claude Code
Closes #2095