Skip to content

feat(ledger): LedgerEmitter wrapper with write+fsync+verify-readback [W2.C.3]#58

Draft
TimothyVang wants to merge 3 commits into
feat/W2.C.2-tool-executorfrom
feat/W2.C.3-ledger-emitter
Draft

feat(ledger): LedgerEmitter wrapper with write+fsync+verify-readback [W2.C.3]#58
TimothyVang wants to merge 3 commits into
feat/W2.C.2-tool-executorfrom
feat/W2.C.3-ledger-emitter

Conversation

@TimothyVang

Copy link
Copy Markdown
Owner

Summary

  • Implements verdict/graph/wrappers/ledger_emitter.py — third wrapper in executor_work composition.
  • Durability: write() + fsync() + verify-readback; no buffered writes (CLAUDE.md §9).
  • HMAC chain integrity: prev_entry_hash = blake3(prev line bytes + newline).
  • Bidirectional Langfuse cross-link: langfuse_trace_id in both LedgerEntry fields and payload.
  • NIST SP 800-86 metadata: microsandbox_version, rootfs_sha256, tool_version, kernel_version.
  • Auth-field redaction before hash/sign (CLAUDE.md §3.9): redact_payload() in verdict/ledger/redaction.py.
  • Also adds prerequisites: verdict/schemas/ledger.py (W1.B.11 backport), verdict/schemas/mode.py, verdict/ledger/hmac_key.py (W1.G.6: TPM-backed + gpg-encrypted + in-process), verdict/ledger/writer.py (W2.G.1).
  • 20 tests GREEN; ruff clean.

TDD trace

  • RED commit: 89d49ee — test + prerequisite modules (no ledger_emitter yet)
  • GREEN commit: 6c09ff5 — LedgerEmitter implementation
  • FIX commit: 3721741 — ruff unused import cleanup

Test plan

  • pytest tests/graph/wrappers/test_ledger_emitter.py -v → 20 passed
  • ruff check verdict/graph/wrappers/ledger_emitter.py verdict/ledger/ verdict/schemas/ledger.py → All checks passed

TimothyVang added 3 commits May 2, 2026 08:42
Failing test: tests/graph/wrappers/test_ledger_emitter.py — import fails
because verdict/graph/wrappers/ledger_emitter.py does not exist yet.

Also adds prerequisite modules (not yet on W2.C.3 branch):
  - verdict/schemas/ledger.py — LedgerEntry (W1.B.11 backport)
  - verdict/schemas/mode.py — Mode enum
  - verdict/ledger/hmac_key.py — TPM-backed / gpg-encrypted HMAC (W1.G.6)
  - verdict/ledger/redaction.py — auth-field stripping before hash/sign
  - verdict/ledger/writer.py — write + fsync + verify-readback (W2.G.1)

Assertions:
- ledger.jsonl grows by one line per run().
- run() returns (ToolOutput, LedgerEntry) tuple.
- LedgerEntry carries NIST SP 800-86 metadata fields.
- prev_entry_hash of N+1 == blake3(line N + newline).
- HMAC sig present and verifiable with same key; fails with different key.
- langfuse_trace_id bidirectional cross-link in entry + payload.
- mode_at_case_init locked to construction value.
- Callable interface writes ledger and returns ToolOutput.
…[W2.C.3]

LedgerEmitter is the third wrapper in the DenyRuleWrapper → ToolExecutor →
LedgerEmitter composition (ARCHITECTURE.md §2).

Durability contract (CLAUDE.md §9):
  - write() + fsync() + verify-readback in LedgerWriter; no buffered writes.
  - Verify-readback re-reads last line, JSON-parses it, spot-checks
    entry_id + hmac_sig before advancing prev_entry_hash.

Chain integrity:
  - prev_entry_hash = blake3(prev line bytes including newline).
  - GENESIS_HASH ("0"×64) for the first entry.
  - LedgerWriter.build_entry() computes HMAC and sets prev_entry_hash.
  - Two sequential run() calls produce two chained JSONL lines.

HMAC (W1.G.6):
  - HMACKeyProvider Protocol: sign(bytes) → hex, verify(bytes, sig) → bool.
  - _TPMHMACProvider: /dev/tpmrm0 + tpm2-pytss (optional dep).
  - _GpgFileHMACProvider: ~/.verdict/key.gpg via system gpg.
  - _SoftwareHMACProvider: in-memory HMAC-SHA256 (gpg path + tests).
  - get_hmac_key_provider(): auto-selects TPM → gpg → error.
  - get_hmac_key_provider_from_bytes(): in-process key for tests.

Redaction (CLAUDE.md §3.9):
  - redact_payload() strips authorization, auth_user, api_key (9 fields)
    before hash/sign. Order: strip → hash → sign.

Langfuse cross-link:
  - LedgerEntry.langfuse_trace_id set from construction arg.
  - payload["langfuse_trace_id"] for Langfuse → ledger direction.
  - langfuse_session_id == case_id (one Langfuse session per case).

LedgerEmitter.run() returns (ToolOutput, LedgerEntry).
LedgerEmitter.__call__() returns ToolOutput only (executor contract).
- 20 tests GREEN; ruff clean.
Remove unused imports: hashlib, datetime, timezone, Path from
ledger_emitter.py; struct from hmac_key.py; unused re-export from writer.py.
All 20 tests still pass.
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