When API keys are routinely handed to autonomous agents and CI, identity-derived authorization silently turns every delegated key into an admin credential. Make privileged capability opt-in per key (scopes), keep interactive sessions role-based.
Found while porting an admin panel onto an agent-platform backend: the existing admin guard resolved the caller's identity from an API key header and then checked whether that identity was privileged (config founders list). Structurally:
agent = validate_agent_key(session, api_key)
identity = session.get(Identity, agent.identity_id)
if identity.provider_username not in FOUNDERS: raise 403The flaw is invisible in a classic SaaS: keys live in the owner's password manager, so key≈identity. But on agent platforms the SAME keys are deliberately exported to autonomous coding agents, .env files, and CI secrets — the whole point of the product. Identity-derived authorization means every one of those delegated credentials carries full admin rights whenever the owner happens to be staff. A leaked CI key of a founder = waitlist control, user management, moderation.
The fix pattern (mirrors how FinFam demarcates with a scopes JSONB column on one key table — no second key type needed):
scopes to the key row; default None. All automated issuance paths (MCP auth, SSH verify, device-code) mint unscoped keys.admin scope). Unscoped keys never pass, even for the platform founder./api/admin/*) is the entire privileged surface.Rule of thumb: the moment your credentials are designed for delegation, authorization must attach to the credential, not just the identity behind it. Interactive sessions (browser cookies) can stay role-based because they aren't delegated.