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

Merged opened this

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

Merged

`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

opened this

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.

The four suites pass: bun test test/credentials.test.ts test/subscriptions.test.ts test/subscription-budget.test.ts test/spawn-keys.test.ts gave 43 pass, 0 fail. bun x tsc --noEmit printed nothing, so it's clean. It first failed with a missing bun type definition until I ran bun install.

Changes

  • Pick: src/core/subscriptions.ts adds the none variant. It looks up the owner's is_admin only when no grant matched, so the common path has no extra query.
  • Runner: src/crew/runner.ts already treats anything that isn't ready as the forge's own token, so none runs as env. I only added a comment saying so. No run changes behaviour.
  • New test: test/credentials.test.ts has a new test. A non-admin owner with no grant gets none, and gets the subscription once it's granted. An admin-owned scope gets env.

Existing assertion changed: test/subscriptions.test.ts, in "picks the nearest grant whole, an agent's before everyone's, and rotates". It expected env for 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 expects none. The assertions in test/credentials.test.ts that expect env use alice, the first user and therefore an admin, so they stay as they were.

Left out: no caller acts on none yet. The runner fallback itself goes in a later part of #1144.

To try it, call pickSubscription with a non-admin ownerId and no grants. The new test in test/credentials.test.ts does exactly that.

🤖 Generated with Claude Code

Closes #2095

Changes requested — before the latest push

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".

commented

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 the resting case 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 calls credentialEnv, never records PLATFORM, and never starts the agent. I removed the "for now" comment.
  • Test: test/crew-nocredential.test.ts runs the stand-in crew with the owner demoted to non-admin and ANTHROPIC_API_KEY set in the forge's process. The run ends failed with the summary and log line above, and the stand-in agent was never launched, so it was handed no credential. I did not check the PLATFORM bookkeeping directly, because Run has no subscriptionId field; the early return is what keeps it out.
  • Ran: tsc --noEmit is clean. The new test, credentials.test.ts and crew-pause.test.ts pass. 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.
Approved

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:

  1. 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.
  2. 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.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs