feat(runtime): validate account tokens on add + per-key usage; auto-disable a bad token (v0.282.0) - #508
Merged
Merged
Conversation
…uto-disable a bad token (v0.282.0) A mis-pasted Claude subscription token was vaulted as-is and then silently sent every future session to /login (invalid CLAUDE_CODE_OAUTH_TOKEN) — the outage seen on the instapods pool. Three changes close that: - Add-time validation: POST /api/runtime-accounts probes the token against Claude's own GET /api/oauth/usage (the source Claude Code's status line uses; no quota consumed) and rejects a definitive 401 with a clear message instead of storing it. A transient 429/network is added and badged "could not verify", never blocked. New src/edge/runtime-account-check.ts. - Per-key usage in Settings → Runtime accounts: a Usage column (weekly 7d + session 5h utilization, amber ≥80% / red ≥100%, reset on hover), a Refresh action, and the add form echoing the result. Usage populates for login credential-dirs (user:profile scope); a setup-token is validated the same way but can't report usage (Anthropic scopes it out) — shown honestly as "valid · usage n/a". New runtime_accounts columns last_checked_at/check_ok/check_note/usage_json; POST …/:runtime/:name/check. - Launch-time backstop: when an unattended run's pane shows a credential-rejection banner (not a usage limit), the account it launched under is auto-disabled (markInvalid) — an invalid token won't self-heal at a reset — so the launcher rotates to another account / the box default instead of looping into /login. A Refresh that re-authenticates re-enables it. Verified against the live oauth-usage endpoint (401 invalid / 403-scope valid / 200 usage), the store + migration on a scratch DB, and the full HTTP add/reject/refresh flow with an owner session. 130/130 governance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…validate # Conflicts: # CHANGELOG.md # package-lock.json # package.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
An owner added a Claude subscription token to the instapods runtime-account pool, but every session kept hitting
/login. Root cause: the enabled account's token value was not a valid setup-token (Z3ug…, notsk-ant-oat01-…); it was vaulted as-is with no validation, injected asCLAUDE_CODE_OAUTH_TOKEN, and Claude rejected it (401 Invalid bearer token) — so rotation overrode the working box default with a broken token on every launch. Nothing caught it: no add-time check, and teardown only reacted to usage-limits, not auth failures.What
1. Add-time validation (the core fix).
POST /api/runtime-accountsnow probes a token against Claude's ownGET /api/oauth/usage(the endpoint Claude Code's status line uses; read-only, no quota consumed) before storing it:permission_error(scope) or 200 → valid, stored.2. Per-key usage in Settings → Runtime accounts. New Usage column (weekly 7d + session 5h utilization, amber ≥80% / red ≥100%, reset on hover), a Refresh action, and the add form echoing the result. Usage populates for interactive-login credential-dirs (which carry the
user:profilescope). Aclaude setup-tokenis validated the same way but can't report usage (Anthropic scopes it out of the profile endpoint) — surfaced honestly asvalid · usage n/a (setup-token lacks the user:profile scope).3. Launch-time backstop. When an unattended run's pane shows a credential-rejection banner (not a usage-limit), the account it launched under is auto-disabled (
markInvalid) — an invalid token won't self-heal at a reset the way a usage-limit does — so the launcher rotates to another account / the box default instead of looping into/login. ARefreshthat re-authenticates re-enables it.Schema
New idempotent columns on
runtime_accounts:last_checked_at,check_ok,check_note,usage_json. New routePOST /api/runtime-accounts/:runtime/:name/check.Verification
oauth-usageendpoint across all outcomes: bad token → 401 (rejected), valid setup-token → 403-scope (valid, usage n/a), login token → 200 with real weekly/session %.markInvaliddisables + drops frompick/enabledCount;recordCheck(null)doesn't clobber a known-good badge;pickskips invalid/limited.npm run build, web build,npm run typecheck, 130/130 governance.Note on the live instapods box
Already hot-patched separately (disabled the broken
tools2account → pool inert → sessions fall back to the working box default). This PR prevents the class of failure going forward. The fleet uses setup-tokens, which validate here but can't show usage — to get live usage numbers an account must be added as a login credential-dir.🤖 Generated with Claude Code