Skip to content

AuthzTrace: an authorization decision carries one prose reason, not the rule-by-rule trace the directive asks for #3289

Description

@macanderson

The maintainer directive on #2716 puts auditable second only to "the best agent loop":

every tool call's authorization decision is a value with a rule-by-rule trace (Decision + Trace), reconstructable after the fact from the journal

PR #3281 landed the decision as a value. The trace is not there. AuthzDecision::Deny { reason: String } carries one prose sentence, so "why was this denied" is answerable only if the gate author remembered to write a good sentence — and "which rules were evaluated, which matched, and which one decided" is not recoverable at all.

It was left out of #3281 on purpose rather than stubbed: a Trace type with nothing reading it is the unwired-field expedient CLAUDE.md forbids. This issue is where it gets a consumer.

What to build

  • An AuthzTrace — an ordered list of evaluated rules, each with its identifier, whether it matched, and what it contributed. resolve.ts's Trace shape in oxagen-platform is the reference; copy the shape, not its defects.
  • AuthzGate::check returns the trace alongside the decision. Keep the Result<_, AuthzEvalError> split exactly as it is — the trace is not an excuse to soften an evaluation failure into a partial answer.
  • A consumer, or this issue is not done. The natural one is the journal: emit the (principal, contract, decision, trace) triple so a host can attribute every effect after the fact. That means an AgentEvent variant, which under invariant fix(config): drop duplicate vertex/bedrock providers (unbreak cargo test) #10 must declare what reads it in crates/stella-protocol/src/event/consumers.rs — the row is part of the work, not a follow-up.

Constraint that shapes the design

Invariant #2: no I/O in the engine. Policy resolvers must stay pure functions over prefetched data, returning the decision plus the trace as a value. That is what makes "why was this denied" a thing a test asserts on rather than a log line somebody greps.

Invariant #3: the trace must not become an egress hole. If it ever rides a telemetry path, rule identifiers are fine and tool inputs are not — crates/stella-store/src/content_free.rs is the reviewed allowlist and a new key there needs a human to answer "is this content?".

Witness

A gate with three rules, of which the second denies: the emitted trace names all three, marks the second as deciding, and survives a serde round trip.

Refs #2716, #2701, #3281.

Metadata

Metadata

Assignees

No one assigned

    Labels

    triageUntyped request — convert by adding bug / feature / epic

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions