feat(auth): the door that can delegate no longer needs a person's session

`/mcp` — `email_send`, `slack_post`, `delegate` — authenticated with
`authenticate`, which accepts only `full`. Nothing hands it a token today, so
this cost nothing yet; the moment something did, the only credential that
worked would have been an owner's session, held by an agent runtime.

`SCOPE_AGENT_DOOR` is that credential's narrow form. `full` still works, so
the UI and every human caller are unaffected, and the route now names what it
accepts rather than accepting everything by default.

The test that matters is not that each scope opens its own route: it is that
holding one grants nothing the other has. Both tokens live where an agent can
read them.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-25 07:34:30 -05:00
co-authored by Claude Opus 5
parent f52cff3e04
commit 22eeaa6f15
4 changed files with 73 additions and 2 deletions
+13
View File
@@ -21,6 +21,19 @@ pub const SCOPE_FULL: &str = "full";
/// that authenticates nowhere, which fails safely but silently.
pub const SCOPE_SKILLS_READ: &str = "skills:read";
/// Act through the §15 MCP door (`/mcp`), and nothing else.
///
/// The door is the actuator: `email_send`, `slack_post`, `delegate`. Reaching
/// it means a token an agent's runtime holds, and the same reasoning as
/// [`SCOPE_SKILLS_READ`] applies — a full session there is an owner-privileged
/// API key handed to a process whose whole purpose is to act on instructions
/// from a model.
///
/// Nothing mints one yet; the route accepts it so that whatever does will not
/// have to reach for a person's session to be understood. `full` still works,
/// so the UI and any human caller are unaffected.
pub const SCOPE_AGENT_DOOR: &str = "agent:door";
/// The authenticated caller attached to every API request: everything RBAC
/// decisions need, nothing more.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]