Skip to content

fix: derive the login scope set from ALL_SCOPES instead of hand-listing it - #303

Merged
saucam merged 2 commits into
mainfrom
fix/login-scope-drift
Aug 25, 2026
Merged

fix: derive the login scope set from ALL_SCOPES instead of hand-listing it#303
saucam merged 2 commits into
mainfrom
fix/login-scope-drift

Conversation

@saucam

@saucam saucam commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

The bug

codeoid login asked ZeroID to mint a hand-maintained literal scope list (src/cli.ts) that had drifted from ALL_SCOPES in both directions:

Never minted — real scopes the daemon enforces:

settings:read   settings:write   fleet:read
pipeline:create  pipeline:read  pipeline:answer  pipeline:manage

Not real scopes — requested but never defined in SCOPES:

tools:read  tools:write  tools:execute  tools:agent

codeoid enforces token scopes verbatimauth.ts: scopes: (identity.scopes ?? []) as Scope[], with no server-side expansion or default. So a scope missing from the mint request is unreachable for the life of the key. A login token structurally could not open the settings screen, the fleet board, or any pipeline verb.

Why it was hard to diagnose

The failure surfaces far from its cause — a bare Missing scope: settings:read from a daemon whose config is otherwise fine. Nothing an operator can do fixes it:

  • re-login — mints the same deficient set
  • restarting the daemon — irrelevant; the scope is fixed at mint time
  • upgrading ZeroID — ZeroID never sees these as anything but opaque strings. Verified on a live instance: credential_policies.allowed_scopes and service_keys.scopes were both empty (no ceiling), oauth_tokens had zero rows (stateless JWTs, nothing cached), and no codeoid scope string appears in the ZeroID binary at all. It grants what it's asked for — it was simply never asked.

The comment above the list read "The full scope set codeoid asks ZeroID to mint", which is precisely the claim that rotted when settings:* was added in fc149bd (2026-07-16).

The fix

const CODEOID_LOGIN_SCOPES = ALL_SCOPES_STRING;

ALL_SCOPES is the single source of truth. ALL_SCOPES_STRING is the same join terminal/client.ts and tui/ws.ts already use — only the login path hand-rolled its own.

Owner-tier question, flagged not buried

This makes the login token owner-tier: it now includes settings:write and pipeline:manage, which scopes.ts explicitly calls out as owner-tier ("the same trust class as settings:write").

I think that's right for the single-operator daemon codeoid targets — the key is minted by, and for, whoever runs it, and delegated access is already expressed by exchanging this token down (the conductor and watcher paths do exactly that) rather than by under-minting here. But it is a deliberate widening and worth a second opinion. The alternative is a named OWNER_SCOPES constant that's still derived, just narrower.

Verification

lint + typecheck clean, 2372 pass / 0 fail. Four new drift assertions: the derivation is checked structurally (so re-introducing a literal array fails), plus no-missing, no-unknown, and an explicit check on the seven scopes whose absence caused the original report.

After merge, one codeoid login <key> re-mint is needed for existing keys.

🤖 Generated with Claude Code

…ng it

`codeoid login` asked ZeroID to mint a hand-maintained literal list of scopes
that had drifted from the canonical set in BOTH directions:

  never minted (7)  settings:read, settings:write, fleet:read,
                    pipeline:create, pipeline:read, pipeline:answer,
                    pipeline:manage
  not real  (4)     tools:read, tools:write, tools:execute, tools:agent

codeoid enforces token scopes verbatim (auth.ts: `scopes: identity.scopes ??
[]`) — there is no server-side expansion or default — so a scope absent from
the mint request is unreachable for the life of the key. A login token
therefore could not open the settings screen, the fleet board, or any pipeline
verb.

The failure surfaces far from its cause: a bare "Missing scope: settings:read"
from a daemon whose config is otherwise correct. Nothing an operator can do
fixes it — not re-login, not restarting the daemon, not upgrading ZeroID
(which never sees these scopes as anything but opaque strings; its own scope
ceilings were empty and it grants what is requested).

`ALL_SCOPES` is the single source of truth. `ALL_SCOPES_STRING` is the same
join the terminal and TUI clients already use; only the login path hand-rolled
its own. The new drift test asserts the derivation structurally, so adding a
scope can no longer leave this behind.

This is an owner-tier token — it now includes `settings:write` and
`pipeline:manage`. That matches the single-operator daemon codeoid targets:
the key is minted by, and for, whoever runs it. Delegated access is expressed
by exchanging this token DOWN (as the conductor and watcher paths already do),
not by under-minting it here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@saucam
saucam merged commit 740b88c into main Aug 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants