Back to Ruflo

ADR-G005: Proof Envelopes -- Cryptographic Run Auditing With Hash-Chained Events

v3/@claude-flow/guidance/docs/adrs/ADR-G005-proof-envelope.md

3.6.307.2 KB
Original Source

ADR-G005: Proof Envelopes -- Cryptographic Run Auditing With Hash-Chained Events

Status

Accepted

Date

2026-02-01

Context

Autonomous agent operations (swarms, daemon tasks, headless evaluation) execute tool calls without real-time human oversight. After the fact, teams need to:

  1. Verify what happened. Which rules were active? Which tools were called? Which files were modified? Were any violations detected?
  2. Detect tampering. If a run event is modified after the fact (by a compromised agent, a bug, or malicious code), the audit trail must signal the inconsistency.
  3. Attribute decisions. Each gate decision, violation, and outcome must trace back to the exact guidance version (constitution hash) and rule IDs that were active.

The RunEvent type in src/types.ts captures the minimum schema for a run: event ID (UUID), task ID, guidance hash, retrieved rule IDs, tools used, files touched, diff summary, test results, violations, outcome acceptance, rework lines, intent classification, timestamp, duration, and session ID.

The guidance hash in RunEvent.guidanceHash is the SHA-256 hash of the constitution text, binding each event to the exact rule set version that governed it.

Decision

Implement a proof envelope model for run auditing with the following properties:

1. Event Identity

Each RunEvent receives a UUID (eventId) generated by crypto.randomUUID() at creation time in RunLedger.createEvent(). This ID is immutable once assigned.

2. Guidance Binding

The guidanceHash field on every RunEvent stores the constitution.hash from the active PolicyBundle. This is a SHA-256 hash truncated to 16 hex characters, computed by GuidanceCompiler.hashContent():

typescript
private hashContent(content: string): string {
  return createHash('sha256').update(content).digest('hex').slice(0, 16);
}

If the constitution changes between runs, the hash changes, making it immediately visible in the ledger that different guidance versions were in effect.

3. Rule Traceability

RunEvent.retrievedRuleIds records the exact rule IDs that were active during the run. Each gate result (GateResult.triggeredRules) links back to specific rule IDs. Violations (Violation.ruleId) identify which rule was violated.

This three-level traceability (guidance hash -> retrieved rules -> triggered/violated rules) enables precise root-cause analysis: "Rule R042 was active because of guidance version abc123, was triggered by the secrets gate, and produced violation V-001."

4. Evaluator Chain

The RunLedger.evaluate() method runs all registered evaluators (IEvaluator implementations) against a finalized event. Each evaluator produces an EvaluatorResult with name, pass/fail, details, and an optional score. The evaluator chain is:

  1. TestsPassEvaluator -- Were tests run? Did they pass?
  2. ForbiddenCommandEvaluator -- Were any forbidden command patterns used?
  3. ForbiddenDependencyEvaluator -- Were forbidden packages introduced?
  4. ViolationRateEvaluator -- Did violations exceed the threshold?
  5. DiffQualityEvaluator -- Did rework exceed the acceptable ratio?

Custom evaluators can be added via RunLedger.addEvaluator().

5. Violation Ranking for Feedback

RunLedger.rankViolations() aggregates violations across events by rule ID, computing frequency, average cost (rework lines), and a combined score (frequency * cost). This ranked list feeds into the optimizer loop (ADR-G008).

6. Temporal Queries

The ledger supports temporal queries: getEventsInRange(startMs, endMs), getRecentEvents(count), and getEventsByTask(taskId). These enable windowed analysis for optimization cycles.

Consequences

Positive

  • Tamper evidence. Any modification to the guidance hash or rule IDs after the fact is detectable by comparing the stored hash against the current constitution. A chain break indicates that the guidance changed between the event's claimed version and the current version.
  • Full traceability. Every decision traces from the outcome back through the evaluators, violations, gate results, retrieved rules, and guidance version.
  • Metric computation. RunLedger.computeMetrics() derives optimization metrics (violation rate per 10 tasks, self-correction rate, rework lines, task count) directly from the event stream, enabling data-driven guidance evolution.
  • Export/import. RunLedger.exportEvents() and importEvents() allow ledger persistence and cross-session analysis.

Negative

  • In-memory storage. The current implementation stores events in an in-memory array. Mitigation: RunLedger accepts a maxEvents constructor parameter (default 0 = unlimited). When set, the oldest 10% of events are evicted in a batch splice when the limit is exceeded (see ADR-G026). Events can also be exported and cleared periodically via exportEvents() and clear().
  • No cryptographic chaining. The current implementation uses UUIDs and guidance hashes but does not implement full hash-chain linking (each event's hash includes the previous event's hash). This is a future enhancement.
  • Signing key is required. The createProofChain() factory requires { signingKey: string } as of ADR-G026. Callers must provide an explicit HMAC key; there is no fallback. The RuvBotBridgeConfig exposes a proofSigningKey field for this purpose.
  • Clock dependency. Timestamps rely on Date.now(), which can be manipulated on the host. Mitigation: in production, timestamps should be sourced from a trusted time service.

Alternatives Considered

1. No auditing, rely on git history

Use git log to reconstruct what happened. Rejected because git does not capture which rules were active, which gates fired, or whether violations were detected. Git captures the what (file changes) but not the why (guidance context).

2. Full blockchain-style merkle tree

Hash-chain every event into a merkle tree for cryptographic proof of ordering. Rejected as over-engineered for the current use case. The guidance hash binding provides sufficient tamper evidence without the complexity of merkle proofs. Can be added later if regulatory compliance requires it.

3. External audit service

Send events to an external audit logging service (e.g., AWS CloudTrail, Datadog). Rejected for the same reason as LLM-based classification: adds a network dependency, latency, and cost. The local ledger is sufficient for development workflows. External export can be layered on top via the exportEvents() API.

4. Log-only, no evaluators

Log events without running evaluators. Rejected because the evaluators are what make the ledger actionable. Without them, the ledger is a write-only log that requires manual analysis.

References

  • v3/@claude-flow/guidance/src/types.ts -- RunEvent, Violation, EvaluatorResult, ViolationRanking, OptimizationMetrics
  • v3/@claude-flow/guidance/src/ledger.ts -- RunLedger, all evaluator classes
  • v3/@claude-flow/guidance/src/compiler.ts -- hashContent() for guidance hash generation
  • v3/@claude-flow/guidance/src/index.ts -- GuidanceControlPlane.startRun(), finalizeRun()
  • ADR-G001 -- Why auditing is needed for autonomous agents
  • ADR-G008 -- How the optimizer consumes violation rankings