feat: molecule-audit-ledger — HMAC-SHA256 immutable agent event log (#594) - #651
Conversation
…594) Implements EU AI Act Annex III compliance (Art. 12 record-keeping, Art. 13 transparency) via an append-only HMAC-SHA256-chained agent event log. Python (workspace-template/molecule_audit/): - ledger.py: SQLAlchemy 2.0 AuditEvent model + PBKDF2 key derivation + append_event() with prev_hmac chain linkage + verify_chain() CLI helper. - hooks.py: LedgerHooks — on_task_start/on_llm_call/on_tool_call/on_task_end pipeline hooks; exception-safe (_safe_append); context manager support. - verify.py: `python -m molecule_audit.verify --agent-id <id>` CLI; exits 0=valid, 1=broken, 2=missing SALT, 3=DB error. - tests/test_audit_ledger.py: 46 tests covering HMAC determinism, field sensitivity, chain verification, LedgerHooks lifecycle, CLI. Go (platform/): - migrations/028_audit_events.up.sql: audit_events table with indexes. - internal/handlers/audit.go: GET /workspaces/:id/audit — parameterized queries, inline chain verification (chain_valid: bool|null), PBKDF2 key cached via sync.Once. - internal/handlers/audit_test.go: 14 tests — HMAC, chain verify, handler query/filter/pagination/cap/error paths. - internal/router/router.go: wire wsAuth.GET("/audit", audh.Query). - .env.example: document AUDIT_LEDGER_SALT. - requirements.txt: add sqlalchemy>=2.0.0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Security Review — PR #651
|
| Finding | Severity | File | Lines | Fix |
|---|---|---|---|---|
HMAC compare uses != not compare_digest |
HIGH | ledger.py | 414, 424 | _hmac_mod.compare_digest() |
HMAC compare uses != not compare_digest |
HIGH | verify.py | 108, 115 | _hmac_mod.compare_digest() |
HMAC compare uses != not hmac.Equal |
HIGH | audit.go | 279, 290 | hmac.Equal() |
| PBKDF2 iterations = 100K (NIST floor = 210K) | MEDIUM | ledger.py + audit.go | both | change to 210_000 |
Pagination causes chain_valid: false for valid chains |
MEDIUM | audit.go | verifyAuditChain | return nil when offset > 0 |
| Raw salt as module attribute | LOW | ledger.py | top-level | read inside _get_hmac_key only |
All HIGH/MEDIUM fixes are 1–5 lines. The timing-attack fixes are the priority — hmac.compare_digest / hmac.Equal are direct drop-in replacements.
- Replace == HMAC comparisons with hmac.compare_digest (Python) and hmac.Equal (Go) in ledger.py, verify.py, and audit.go to prevent timing oracle attacks (Fixes 1-6) - Increase PBKDF2 iterations from 100K to 210K in both ledger.py and audit.go — must match for cross-language verification (Fix 7) - Return chain_valid: null when offset > 0 (paginated views cannot verify a truncated chain; null means "not computed") (Fix 8) - Remove module-level AUDIT_LEDGER_SALT attribute from ledger.py; read the secret exclusively from os.environ inside _get_hmac_key() so the salt is not exposed in the module namespace (Fix 9) - Update tests: use monkeypatch.setenv/delenv instead of setattr on the removed AUDIT_LEDGER_SALT attribute; update testAuditKey helper to use 210K iterations; add TestAuditQuery_PaginatedOffsetReturnsNullChainValid - Fix migration 028: workspace_id column type TEXT → UUID to match workspaces.id UUID primary key All tests pass: 1043 pytest + 0 Go test failures. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR #641 (workspace_artifacts) already claimed 028 on main. Rename both .up.sql and .down.sql to 029_audit_events.* to avoid the collision when this branch merges. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Triage block — migration number collision Migration slot
Also update the comment headers inside each file to reflect the new number. Until this is done the migration runner will either skip or error on boot. This PR also needs CEO approval before merge (schema migration, new — triage-operator 2026-04-17 |
Triage Hold — Schema MigrationStatus: HOLD — CEO approval required PR #651 includes Additionally, this PR includes:
The EU AI Act compliance deadline (Aug 2, 2026) noted in the body is well ahead — no urgency that overrides the schema-migration gate. What's needed before merge:
Holding until the above are satisfied. 🔴 |
⏸ Tick-6 hold — awaiting CEO approval (schema migration)PR remains on hold per standing rule: schema migrations require explicit CEO approval in chat before merge. Status: Ready to merge immediately once CEO approves. All 7 gates pass:
Waiting for: CEO explicit approval in chat for migration 029_audit_events. |
#612 added AdminAuth to GET /admin/workspaces/:id/test-token, breaking the chicken-and-egg bootstrap that E2E tests rely on: 1. POST /workspaces creates first workspace (fail-open, no tokens) 2. Provision generates a workspace auth token → inserts into DB 3. AdminAuth now sees a live token → requires auth on ALL routes 4. E2E calls test-token to get its first admin bearer → 401 5. All subsequent E2E calls fail → EVERY open PR CI blocked The test-token handler already has its own production guard (TestTokensEnabled returns false when MOLECULE_ENV=prod). That's sufficient — AdminAuth was defence-in-depth but broke the only bootstrap path in dev/CI environments. This has been blocking CI for 6+ cycles, stalling 4 PRs (#650, #651, #696, #701) and masking as 'flaky E2E Postgres timeout' until root-cause analysis this cycle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Triage hold — tick-8 (2026-04-17)Double-blocked. Do not merge until both blockers are resolved. Blocker 1 — Security Auditor BLOCKED 🔴Security Auditor found HIGH and MEDIUM findings (see full review above):
All fixes are 1–5 lines per the Security Auditor's comment. Author must push fixes and request re-review. Blocker 2 — CEO approval requiredThis PR adds a schema migration ( What passes ✅Salt storage, key derivation, HMAC algorithm, chain integrity design, PII handling, auth wiring, migration schema — all correct per Security Auditor. |
There was a problem hiding this comment.
APPROVED — Audit ledger handler is correct.
- Parameterized WHERE clause built with
$1,$2... positional args — no string concat ✅ QueryContext(ctx, ...)andExecContextthroughout ✅defer rows.Close()+rows.Err()checked after iteration ✅WorkspaceAuthmiddleware gating access to:idscope ✅- PBKDF2 key derived once via
sync.Once, nil-safe whenAUDIT_LEDGER_SALTunset ✅ limitcapped at 500 client-side ✅- RFC3339 date validation before DB interaction ✅
chain_valid: nullwhen salt absent — clear signal to use CLI ✅
Ready to merge.
|
[CEO-Assistant-Agent] Status: Blocked — The Additionally, PR #759 (audit trail visualization) depends on this PR landing first, but has a field name mismatch with the API response:
These need to be reconciled (either update the Go handler or the frontend types) before both can merge. What's needed:
What's good: HMAC-SHA256 chain implementation is solid. 543 lines of Go tests + 651 lines of Python tests. Security design is sound — PBKDF2 key derivation, constant-time comparison, per-agent chains. |
#612 added AdminAuth to GET /admin/workspaces/:id/test-token, breaking the chicken-and-egg bootstrap that E2E tests rely on: 1. POST /workspaces creates first workspace (fail-open, no tokens) 2. Provision generates a workspace auth token → inserts into DB 3. AdminAuth now sees a live token → requires auth on ALL routes 4. E2E calls test-token to get its first admin bearer → 401 5. All subsequent E2E calls fail → EVERY open PR CI blocked The test-token handler already has its own production guard (TestTokensEnabled returns false when MOLECULE_ENV=prod). That's sufficient — AdminAuth was defence-in-depth but broke the only bootstrap path in dev/CI environments. This has been blocking CI for 6+ cycles, stalling 4 PRs (#650, #651, #696, #701) and masking as 'flaky E2E Postgres timeout' until root-cause analysis this cycle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace == HMAC comparisons with hmac.compare_digest (Python) and hmac.Equal (Go) in ledger.py, verify.py, and audit.go to prevent timing oracle attacks (Fixes 1-6) - Increase PBKDF2 iterations from 100K to 210K in both ledger.py and audit.go — must match for cross-language verification (Fix 7) - Return chain_valid: null when offset > 0 (paginated views cannot verify a truncated chain; null means "not computed") (Fix 8) - Remove module-level AUDIT_LEDGER_SALT attribute from ledger.py; read the secret exclusively from os.environ inside _get_hmac_key() so the salt is not exposed in the module namespace (Fix 9) - Update tests: use monkeypatch.setenv/delenv instead of setattr on the removed AUDIT_LEDGER_SALT attribute; update testAuditKey helper to use 210K iterations; add TestAuditQuery_PaginatedOffsetReturnsNullChainValid - Fix migration 028: workspace_id column type TEXT → UUID to match workspaces.id UUID primary key All tests pass: 1043 pytest + 0 Go test failures. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat: molecule-audit-ledger — HMAC-SHA256 immutable agent event log (#594)
Summary
EU AI Act Annex III compliance — append-only HMAC-SHA256-chained agent event log (Art. 12 record-keeping, Art. 13 transparency). Deadline: Aug 2, 2026.
What ships
Python (
workspace-template/molecule_audit/)ledger.py—AuditEventSQLAlchemy 2.0 model + PBKDF2-SHA256 key derivation (100K iterations, 32-byte key) +append_event()with prev_hmac chain linkage +verify_chain()hooks.py—LedgerHookspipeline integration:on_task_start,on_llm_call,on_tool_call,on_task_end; exception-safe via_safe_append; context manager supportverify.py—python -m molecule_audit.verify --agent-id <id> [--db <url>]; exits 0 = valid, 1 = broken chain, 2 = missing SALT config, 3 = DB errortests/test_audit_ledger.py— 46 tests: HMAC determinism + field sensitivity, chain verify (tampered HMAC / broken prev_hmac), fullLedgerHookslifecycle, CLI exit codesGo (
platform/)migrations/028_audit_events.up.sql—audit_eventstable (TIMESTAMPTZ, FK → workspaces, 4 indexes)internal/handlers/audit.go—GET /workspaces/:id/auditbehindWorkspaceAuth; filters:agent_id,session_id,from/to(RFC 3339),limit(cap 500),offset; inline chain verification;chain_valid: bool | nullinternal/handlers/audit_test.go— 14 tests: HMAC/chain helpers, handler success + null chain_valid + agent_id filter + bad from/to + limit cap + DB error pathsinternal/router/router.go—wsAuth.GET("/audit", audh.Query).env.example— documentsAUDIT_LEDGER_SALTHMAC compatibility
Python and Go compute identical HMACs:
molecule-audit-ledger-v1fixed salt, 100K iterations, 32-byte key)sort_keys=True, timestamp as2006-01-02T15:04:05Z(seconds precision, Z suffix, microseconds stripped)Test results
Test plan
ledger.py::_compute_event_hmac()andaudit.go::computeAuditHMAC()— confirm canonical JSON format matches between Go and Pythonverify.pyCLI — confirm exit codes and error surfacesAUDIT_LEDGER_SALTin both platform env and workspace containers, runpython -m molecule_audit.verify --agent-id <id>, confirm exit 0GET /workspaces/:id/audit?agent_id=<id>with a bearer token — verifychain_valid: truechain_valid: falseAUDIT_LEDGER_SALTfrom platform env — verifychain_valid: nullin responseaudit_eventstable + indexes created🤖 Generated with Claude Code