`docs/TOOL-CALL-ARCHITECTURE.md` §3 calls deploying the MCP door "config, not code". It is not, and the reason is authentication. `/mcp/skills` authenticates with `AuthService::authenticate`, which returns a full `AuthedUser` carrying the user's role. There is no narrower credential in the system. So pointing a mission container at the door means writing a bearer token into a file inside that container — and mission agents run arbitrary `Bash` with egress and no read gate, which is this platform's own documented security posture. An owner-scoped token there turns "the agent runs commands in a sandbox" into "the agent drives the whole ClawMates API as the owner". Checked before building this rather than assumed: no such credential is in a mission container today. The runtime's config.toml has no `[mcp.servers]` block and no bearer, so the door would have been a NEW exposure, not an existing one. So: `auth_sessions.scope`, defaulting to `full`. `authenticate` now delegates to `authenticate_scoped(token, SCOPE_FULL)`, which means **every existing caller rejects a narrow token** and a route must opt in by naming the scope it accepts. `/mcp/skills` is the only opt-in. Fail closed on purpose. The likely mistake here is adding a scope and forgetting to wire its check; this way that mistake grants nothing rather than granting everything. `mint_scoped` refuses to mint a `full` token — a caller reaching for it wants a narrow credential, and handing back a full one because an argument was wrong is exactly the failure the column exists to prevent, and it would be invisible because the token would work. The test that matters is not that the door accepts the token, it is that nothing else does. Negative-controlled: removing the scope comparison fails `a_scoped_token_is_refused_by_every_unscoped_caller`. `.sqlx` regenerated — `authenticate` is a compile-checked query and CI builds with SQLX_OFFLINE=true. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018i9Ten1LU4jUr5d7TAWda9
38 lines
2.1 KiB
SQL
38 lines
2.1 KiB
SQL
-- Give a session a SCOPE, so a credential can be handed to something that is
|
|
-- not a person.
|
|
--
|
|
-- `AuthService::authenticate` returns a full `AuthedUser` carrying the user's
|
|
-- role. There is no narrower credential in the system, so any component that
|
|
-- needs to call the ClawMates API must be given one that can do everything the
|
|
-- user can.
|
|
--
|
|
-- That is the blocker on deploying the MCP door to mission agents
|
|
-- (`docs/TOOL-CALL-ARCHITECTURE.md` §3, which calls it "config, not code").
|
|
-- Reaching `/mcp/skills` from a mission container means putting a bearer token
|
|
-- in a file inside that container — and mission agents run arbitrary `Bash`
|
|
-- with egress and no read gate, which is the platform's own documented
|
|
-- security posture. An owner-scoped token there turns "the agent runs commands
|
|
-- in a sandbox" into "the agent drives the whole API as the owner".
|
|
--
|
|
-- Verified before building this: no such credential is in a mission container
|
|
-- today. The runtime's config.toml has no `[mcp.servers]` block and no bearer,
|
|
-- so this would be a NEW exposure rather than an existing one.
|
|
--
|
|
-- FAIL CLOSED. The default is 'full', so every existing row and every existing
|
|
-- caller behaves exactly as before; `authenticate` REJECTS anything else, and a
|
|
-- route must opt in by asking for the scope it accepts. A scope added later and
|
|
-- wired nowhere therefore grants nothing, which is the safe direction for the
|
|
-- mistake most likely to be made here.
|
|
ALTER TABLE auth_sessions
|
|
ADD COLUMN IF NOT EXISTS scope TEXT NOT NULL DEFAULT 'full';
|
|
|
|
COMMENT ON COLUMN auth_sessions.scope IS
|
|
'full = a person''s session, accepted everywhere. Anything else is a narrow credential accepted only by routes that name that scope (see AuthService::authenticate_scoped). Never widen a token in place; mint a new one.';
|
|
|
|
-- The lookup is by token_hash and already indexed; this supports auditing and
|
|
-- revoking a whole class of narrow credential at once (e.g. every skills token
|
|
-- for a workspace after a leak).
|
|
CREATE INDEX IF NOT EXISTS auth_sessions_scope_idx
|
|
ON auth_sessions (scope)
|
|
WHERE scope <> 'full';
|