Skip to content

First-class decision & effect auditing (the audit seam) - #166

Draft
terylt wants to merge 8 commits into
devfrom
feat/audit-seam
Draft

First-class decision & effect auditing (the audit seam)#166
terylt wants to merge 8 commits into
devfrom
feat/audit-seam

Conversation

@terylt

@terylt terylt commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

CPEX could not audit its own enforcement. An observation-only plugin — the
reference audit-logger, or an OCSF emitter — only ever sees allowed
post-hook traffic; a blocked call, an approval rejection, a delegation failure,
or an injection-stop produced no audit record at all. And irreversible
external actions a plugin causes (a token mint, an approval grant) were not
recorded crash-safely — a process that died between "about to mint" and "minted"
left no trace.

This PR makes auditing first-class in the executor. The core owns a decision
record and emits it at every verdict; any audit sink consumes it. Irreversible
effects are recorded write-ahead, crash-safe, and reconcilable. Each decision
carries the provenance needed to reconstruct a causal graph (span + taint +
content hash). The OAuth delegator is wired as the first real consumer.

Everything is opt-in — no behavior changes unless the operator configures a
sink, an effect WAL, or content provenance.

What's included

Decision auditing.

  • A new AuditHook family, auto-attached by the PluginManager, fired at the
    executor's verdict return points (not a pipeline phase) — so allow, deny,
    and modify all produce a record. "Which phase, before or after which deny"
    stops being a question.
  • A DecisionLog — executor-owned, handed only to audit sinks, never placed
    on PluginContext (the thing that records must not be able to change what it
    records). Carries the ordered plugin steps and the terminal verdict.

Effect auditing (irreversible external actions).

  • A capability-gated, two-phase write-ahead protocol: a plugin holding
    emit_effect calls ext.begin_effect to durably record intent before the
    act (fail-closed — no durable record, no act) and ext.complete_effect
    to record the outcome (confirmed / rejected / unknown).
  • FileEffectLog — a durable WAL (append + fsync, serialized against
    concurrent writers, self-compacting past a configurable threshold).
  • Crash recovery: PluginManager::recover_effects compacts completed effects
    and reconciles crash-orphaned ones against the issuing participant via an
    EffectReconciler seam. The default (LogUnknownsReconciler) logs and leaves
    them unknown — correct for any participant with no lookup-by-key.
  • Extensions::perform_effect brackets the whole protocol so a caller cannot
    skip, reorder, or forget it.

Provenance on the decision node (for downstream causal-graph reconstruction).

  • Span / causal parentDecisionLog::span() carries a W3C
    trace_id/span_id/parent_span_id (child-span model: a fresh span whose
    parent is the request's span), set by the executor at pipeline entry.
  • Taint — the labels the request arrived with, captured at entry; the sink
    diffs them against the final labels to show the taint this node added.
  • Content hash (opt-in)PluginPayload::audit_bytes() (per-type opt-in
    via impl_plugin_payload!(_, audit_serialize)) feeds a sha256: content ref.
    The executor hashes the payload at entry behind capture_content_provenance;
    the sink hashes the output lazily. Only digests are kept, never content
    provenance without re-spilling the data a PII scanner exists to redact.

First real consumer.

  • cpex-plugin-delegator-oauth brackets both mint legs — the workload
    client_assertion base-token mint and the RFC 8693 exchange — with
    begin_effect/complete_effect, mapping a successful exchange to confirmed,
    a definitive IdP rejection to rejected, and a timeout/unreachable IdP to
    unknown (reconciled later, never assumed minted).

Reference sink.

  • audit-logger now renders the verdict, ordered steps, span, taint, and
    (when enabled) content hashes.

Notable design decisions

  • Fire on the verdict, not a phase. The two deny sites straddle the AUDIT
    phase; emitting at the executor's return points is complete by construction.
  • Isolation as a type contract. The DecisionLog reaching an audit handler
    but never PluginContext is a real security property; an AuditHook family
    (not a manager special-case) makes "sees verdicts, cannot influence them"
    type-level.
  • Err → unknown, not rejected. A failed mint may still have landed at the
    participant, so recovery reconciles it rather than assuming it didn't happen.
    The delegator, which knows a clean 4xx from a timeout, maps precisely.
  • The reconciler is generic, not plugin-specific. It reads the
    self-describing EffectRecord and does a keyed ledger lookup — participant-
    specific at most, and today no participant offers one, so the default suffices.

Opt-in / compatibility

  • No new source-level breaking changes. PluginPayload::audit_bytes() has a
    default (None); AuditHook/effect emitter/WAL/hashing all engage only when
    configured (effect_log_path, capture_content_provenance, emit_effect).
  • New config knobs on plugin_settings: effect_log_path,
    effect_log_compaction_threshold, capture_content_provenance — all default
    to off.

Testing

  • Unit + integration across cpex-core, audit-logger, and delegator-oauth:
    verdict-point emit, capability gating, WAL durability / concurrent-append
    integrity / recovery+compaction / reconciliation, perform_effect state
    mapping, span child-model, taint delta, content-hash gating, and the OAuth
    delegator emitting prepared → confirmed/rejected against a mock IdP.
  • cargo fmt clean, cargo clippy --workspace --all-targets clean, full
    workspace test green.

Out of scope (follow-ups)

  • Host-side trace-context propagation downstream so spans chain across hops.
  • ocsf-audit mapping span()/on_effect into OCSF trace/span and the
    Authentication class.
  • A real ledger reconciler (an issuance ledger with keyed lookup) — see
    docs/effect-ledger-integration-note.md; would make unknown truly resolvable
    and, if it sits in the mint path, deliver v2 structural enforcement too.

terylt and others added 8 commits August 4, 2026 14:28
…iting of denies.

Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Brings in the identity work (5 commits) before building effect auditing,
which touches delegation/identity. Only executor.rs conflicted: dev's
`payload_modified` flag and the audit seam's `decisions` log each appended a
trailing parameter to the phase functions — kept both, ordered decisions
then payload_modified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t.begin_effect, fail-closed durability.

Signed-off-by: Teryl Taylor <terylt@ibm.com>
… recovery + reconciliation seam.

Signed-off-by: Teryl Taylor <terylt@ibm.com>
…ction.

Signed-off-by: Teryl Taylor <terylt@ibm.com>
…mint effect-audit.

Signed-off-by: Teryl Taylor <terylt@ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant