Audit chain 100% and HITL 100%: how two numbers map to mechanisms
Audit-chain completeness at 100% and HITL coverage at 100% are each backed by a mechanism: whether written audit records can be independently recomputed; whether high-risk actions passed a human gate, and whether that gate was the actor themselves or someone else.
Why logs alone do not support accountability
Section titled “Why logs alone do not support accountability”Ordinary audit records capture actor, time, and content. They help rebuild a timeline during incident response.
Accountability also requires that records can be independently recomputed after write. The runtime that writes logs can usually also change them; a row existing in storage only means it is queryable now, not that it has been unchanged since write.
Audit therefore needs a recomputable structure: the same fields that enter the chain must yield the same digest; if any record is altered, verification of the rest of the chain fails. Post-hoc rewrite and normal append become distinguishable by result.
Hash-chain write and verify
Section titled “Hash-chain write and verify”Each audit record carries two digest fields:
prev_hash: digest of the previous record; the chain head uses a fixed genesis value (a zero string of agreed length)hash: computed fromprev_hashand this record’s canonical body
hash = sha256(prev_hash + "\n" + canon(body))body includes tenant, session, event type, payload, time, and other fields that enter the chain. canon reduces the body to a stable, reproducible sequence so the same content does not produce different digests from serialization differences.
Writes append to the chain tail in time order. Verification recomputes in time order: the current record’s prev_hash must equal the previous hash, and the current hash must equal a recompute from the current body. Any step failure stops and returns the break record id.
The external verify API is GET /audit/verify. Example responses:
{ "ok": true, "checked": 50, "broken_at": null }{ "ok": false, "checked": 12, "broken_at": "aud_…" }checked is the number of recomputed records; a non-null broken_at points at the break. Deployment acceptance scripts call the same endpoint and require a full chain scan.
Capability boundary: the hash chain constrains rewrite of history after a successful write. If the write path itself is compromised, forged content can still be written with a matching digest. Given a trusted write path, later rewrite of old records is detected. This covers post-write integrity, not anti-forgery at the instant of write.
100% coverage on mutating paths
Section titled “100% coverage on mutating paths”Operations are layered by external side effects. Only mutating actions (those with external side effects) require audit, and may require approval by rule:
| Layer | Examples | Audit | Approval |
|---|---|---|---|
| Read | Read files, query logs | Retained | No |
| Write | Change workspace, produce patches | Retained | Per rule |
| mutating | Push, send email, production change, payment | Required | Forced per rule |
Forcing human approval on every read/query hurts usability; allowing push, change, or payment to skip recording leaves gaps in investigation. Gates therefore concentrate on mutating.
Here 100% means mutating paths must land on the chain continuously — intermittent gaps are not allowed. Audit coverage is binary: the path always lands, or there is a blank at a critical moment.
Verification criterion: checked from GET /audit/verify counts whether records already on the chain for the current tenant recompute completely. It is not the business count of all mutating actions that occurred. That count also depends on whether the action entered the landing path. Keep the two numbers separate.
Verify false positives: body shape mismatch
Section titled “Verify false positives: body shape mismatch”A common false positive: verify reports a broken chain, but storage was not manually altered. The root cause is a mismatch between the body shape used when computing the digest and the shape read back after persistence.
Key order. If the same content serializes with different key order, digests differ and verify misreports tampering. Normalize key order before hashing:
const keys = Object.keys(obj) .filter((k) => obj[k] !== undefined) .sort()Null-valued fields. In-memory representations may keep null fields; after writing JSON (or similar), those fields are often omitted. Computing the digest from the pre-write shape and verifying from the post-read shape means the body has changed and verify fails. Correct write order: first converge the body to the on-disk shape, then compute the digest, then insert.
These issues rarely show up in write-only tests; full verify surfaces them. After confirming a false positive from serialization, digests can be recomputed in time order and written back. That path is not for masking real content tampering; export and retain first.
Self-confirmation and third-party approval are separate flows
Section titled “Self-confirmation and third-party approval are separate flows”The audit chain answers whether records were rewritten. HITL answers whether a high-risk action passed a human gate, and who that gate was. They use different flows and events:
| Flow | Condition | Audit event | Enters approval inbox |
|---|---|---|---|
| Self-confirmation | Actor allows this operation in-session | permission.* | No |
| Third-party approval | Another role approves | approval.* (includes requester_id / decided_by) | Yes |
If both share one inbox table, HITL coverage stats count self-approval as human approval and the rate is wrong.
Rule: an actor cannot approve their own ticket in the approval center. Self-approval is not part of the approval flow. For accountability, search permission.* and approval.* separately.
In-session allow is existing permission capability; cross-person approval and recomputable audit are additional trust boundaries and must be measured and verified on their own.
Related: