/** * AgentAuthorizationPropagator — action-layer security for agent delegation. * * Implements P1 of ADR-144 (ruvnet/ruflo#2248): scope-envelope on SendMessage * + per-action authorization checks. P2-P5 wire this into the comms layer, * MCP dispatcher, MCP auth validator, and provenance log respectively. * * Threat model * ------------ * When agent A delegates a task to agent B via SendMessage, B can today * escalate the granted scope by calling tools A was never authorized to * invoke. RBAC/ABAC on agent roles do not solve this — roles don't compose * under dynamic LLM delegation (arXiv:2605.05440, Grade A formal analysis). * * The fix is scope-based propagation: every SendMessage carries an * `AuthScope` that is *monotonically reducing* — each hop can drop tools or * servers from the granted set but never add them. This is the same shape * as OAuth scope reduction; adding back a tool requires talking to the * original principal out-of-band. * * Scope * ----- * - P1 (this file): the component, the envelope, the type-level invariants. * No call sites yet — adding the wrapping/enforcement in P2-P4. * - All operations synchronous, pure, allocation-light. Targets < 1 ms p99 * for `wrapOutbound` + `checkToolCall` so it can sit on every SendMessage. * * Backwards compatibility * ----------------------- * `scope` on the envelope is optional in v1. Agents without scope set * operate in legacy permissive mode (all tools allowed, depth unlimited, * server auth unchecked). `CLAUDE_FLOW_STRICT_AUTH=true` enables enforcement. * * Reference: ADR-144, arXiv:2605.05440 (Authorization Propagation), * arXiv:2605.28914 (AIRGuard), arXiv:2605.22333 (MCP auth survey). */ /** Scope granted to an agent at a delegation hop. Monotonically reducing. */ export interface AuthScope { /** Stable identifier of the originating principal (agent or user). */ readonly principalId: string; /** MCP tool IDs this scope is allowed to call. */ readonly grantedTools: ReadonlyArray; /** MCP server IDs whose tool responses this scope will accept. */ readonly grantedServers: ReadonlyArray; /** Max remaining delegation hops. Decrements on every `wrapOutbound`. */ readonly delegationDepth: number; /** Unix ms after which this scope is no longer valid. */ readonly expiresAt: number; /** * Optional opaque ID for cross-referencing with ADR-144 P5 provenance log * and ADR-146 telemetry events. Callers should treat as a correlation * handle, not a security claim. */ readonly scopeId?: string; } /** Envelope shape attached to every SendMessage when strict-auth is on. */ export interface SendMessageEnvelope { /** Authorization scope governing what the receiver may do. */ readonly scope: AuthScope; /** The original SendMessage payload, untouched. */ readonly payload: T; } /** Decision returned by `checkToolCall`. Never throws. */ export interface ToolCallDecision { readonly allowed: boolean; /** Human-readable reason; stable enough to match in tests/telemetry. */ readonly reason?: 'tool-not-in-scope' | 'server-not-in-scope' | 'scope-expired' | 'delegation-depth-exhausted' | 'principal-mismatch'; } /** * Wrap-failure reasons surfaced to callers as a typed error. We use a * discriminated union rather than throwing so the comms layer (P2) can * decide whether to drop the SendMessage or surface to the user. */ export declare class AuthorizationPropagationError extends Error { readonly code: 'scope-cannot-grow' | 'depth-underflow' | 'scope-expired' | 'principal-mismatch'; constructor(code: 'scope-cannot-grow' | 'depth-underflow' | 'scope-expired' | 'principal-mismatch', message: string); } /** * Construct a default permissive scope. Used in legacy mode when no scope * was attached upstream. Callers SHOULD migrate to explicit scopes; this is * here so the propagator never has to special-case "no scope". */ export declare function makeLegacyPermissiveScope(principalId?: string): AuthScope; /** * `AgentAuthorizationPropagator` — the load-bearing component for ADR-144. * * Construction is intentionally cheap (no I/O, no async). Callers can build * a fresh instance per task without overhead, or share one — it holds no * mutable state beyond the optional provenance buffer. */ export declare class AgentAuthorizationPropagator { private readonly opts; /** * In-memory provenance buffer. P5 will flush this to the structured * telemetry sink (ADR-146 GuardrailEvent shape). For P1 we just retain * the last N events for inspection/tests. */ private readonly provenance; constructor(opts?: { provenanceBufferMax?: number; }); /** * Attach a reduced scope to an outbound SendMessage. * * Invariants enforced (throws `AuthorizationPropagationError` on violation): * - newly granted tools MUST be a subset of `currentScope.grantedTools` * - newly granted servers MUST be a subset of `currentScope.grantedServers` * - delegationDepth MUST decrement by ≥ 1 (must remain ≥ 0) * - principalId is propagated unchanged * - expiresAt cannot be extended; copied from the holder */ wrapOutbound(payload: T, currentScope: AuthScope, requested?: { tools?: ReadonlyArray; servers?: ReadonlyArray; }): SendMessageEnvelope; /** * Validate a single tool call against a scope. Pure; never throws. Use the * result's `allowed` to make the dispatch decision. */ checkToolCall(toolId: string, scope: AuthScope, opts?: { serverId?: string; now?: number; }): ToolCallDecision; /** * Verify an MCP server presented a valid credential before its response is * consumed. P1 ships a permissive default (any non-empty credential is * accepted) so the API is stable; P4 wires this to a real validator. * * Returns `false` on missing/empty credential; callers MUST treat `false` * as a hard reject (same rule as ADR-131 reject findings). */ verifyServerAuth(serverId: string, credential: unknown): boolean; /** * Record an action in the provenance buffer. P5 will route to telemetry; * P1 keeps the last N events for test inspection and post-incident audit. */ recordAction(agentId: string, toolId: string, scope: AuthScope, outcome: 'allowed' | 'denied', reason?: string): void; /** Read-only view of recorded provenance for tests + audit CLI (P5). */ getProvenance(): ReadonlyArray<{ agentId: string; toolId: string; scope: AuthScope; outcome: 'allowed' | 'denied'; reason?: string; ts: number; }>; } //# sourceMappingURL=propagator.d.ts.map