Skip to main content

check_s1

Function check_s1 

Source
pub(super) fn check_s1(
    store: &Store,
    ctx: &RpcContext,
    req_agent_id: &str,
) -> Result<(), String>
Expand description

S1: the caller may only act on itself.

ctx.agent_id is stamped at bus:register and is the agent’s slug; req_agent_id is whatever the request named. Both are compared as canonical db_agents.ids when they are not already byte-identical, so a caller that authenticates with its slug and names itself by id (or by a differently-cased slug) is recognised as itself.

Both sides resolve, or neither does. Phase 4 of SPEC_CANONICAL_AGENT_ID_MIGRATION_2026_09_21.md §6 proposed stamping the resolved id into RpcContext at registration instead, and flags the hazard itself: resolving one side of this equality while the other stays a slug silently denies every caller, turning an authz boundary into a permanent outage that no test of the allow path would catch. Resolving both here, at the single point of comparison, has no such window — there is no deploy ordering in which one side has migrated and the other has not.

The byte-equality fast path is deliberate and load-bearing: it is the overwhelmingly common case, it costs no store lookup, and it makes this function’s behaviour for existing callers identical to the pre-migration version. Resolution only ever runs where the old code would already have returned FORBIDDEN, so this can admit calls that used to be rejected but can never reject one that used to be admitted.

Admitting more is safe here precisely because resolution is per-input and fails closed: two different inputs resolve to one id only when they name one agent, and an ambiguous slug (two agents, one slug) resolves to Err on both sides — so a collision denies rather than granting.

A failed resolution is reported as agent_id mismatch, never “no such agent”: whether some other agent exists is not something an unauthorised caller should be able to probe through this boundary. Takes the store rather than &AppState because the handlers do not agree on what they capture — some clone the whole state into the closure, others clone mstore alone — and a borrowed &AppState cannot escape into a 'static future.