Skip to content

Safety pipeline

Held to a higher coverage bar than the rest of Gaby: 100% line + branch coverage, enforced in CI via scripts/cov-safety.sh.

The four stages

authz.evaluate → scopes.check → redact.scrub → audit.append

Each stage is independently testable and has hypothesis property tests guarding its invariants:

  1. deny-by-default — unknown scope = deny.
  2. deny-beats-allow — explicit deny overrides any allow.
  3. hash-chain integrity — audit log entries reference the prior entry's hash.
  4. tamper detection — recomputing the chain detects a single-byte mutation.
  5. redaction idempotenceredact(redact(x)) == redact(x).

Decision matrix

The authz pipeline branches on (actor_kind, tool_scope, autonomy_level):

actor_kind tool_scope=read tool_scope=write
agent allow autonomy_level decides
operator_ask allow always deny (Iter 16 — no writes from Ask)
replay_reviewer allow always deny

For agent + write:

autonomy behavior
off deny
investigate deny (investigate is read-only by design)
propose emit NeedsApproval → park investigation in WAITING_APPROVAL
act allow if connector's scope policy permits, else deny

Why structural, not flag-driven

  • Read-only connectors (Redis, Sentry) literally don't declare write tools — there's nothing for the model to call.
  • Ask Gaby engine ships tools=[] to the provider — no tool surface at all.
  • Stripe execute path raises NotImplementedError in v0.3 — code can't run.

Source: backend/src/gaby/safety/