docs(adr): options memo for audit_log retention levers (BACKLOG #1421) - #933
Open
wshallwshall wants to merge 1 commit into
Open
docs(adr): options memo for audit_log retention levers (BACKLOG #1421)#933wshallwshall wants to merge 1 commit into
wshallwshall wants to merge 1 commit into
Conversation
ADR 0185, Proposed. #1421's last limb is one owner ruling: which retention lever exists for a table that must stay tamper-evident. This lays the options out and picks none. Mechanism re-read at HEAD, not inherited: the row id is not in the hashed payload, and the walk chains from the STORED hash. A verifier exists three ways and ships off. Eight shapes driven with both controls firing, plus a full walk beside the shipped one, which reports only the first divergent row. Three findings are new: a break is local and does not spread; an interior delete leaves the anchor head byte-identical; a tombstone keeping row_hash still breaks at that row. Crux: delete-then-reseal verifies clean, so only a held off-box anchor tells a purge from an attack. Docs only. No purge, no config field, no default moved, row stays open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
BACKLOG #1421's remaining limb is one owner ruling: does deleting an audit row break the
tamper-evident chain, and therefore which retention lever exists at all for an
audit_logthatwould grow by one commit per authenticated read on a first deployment.
This ships ADR 0185 as an options memo,
Status: Proposed. It lays the levers out with theircosts so the owner can rule. It picks nothing, and it changes no engine behaviour -- no purge, no
new config field, no default moved.
[retention].audit_daysis still accepted and unenforced.Row 1421 stays OPEN. Its banner records that the memo exists and that the ruling is outstanding.
Read with
parse_itemsafter the edit:is_open True, 1 open banner, 0 closed.The mechanism, re-read at HEAD rather than inherited
Row 1421's cited line numbers are from
fd44b0f17and have drifted. These are current.audit_row_hash(store/store.py:1009) digests[prev_hash, ts, actor, action, channel_id, detail]at:1057, plusclientas a conditional seventh element. The rowidis not in thepayload.
record_audit(:7603) reads the head under the store lock immediately before the insert, sochain order is
idorder and nothing else.row_hashis a plain nullableTEXTcolumn (:1782).:7998), not the recomputed one. That one line decidesmost of the question.
Storeprotocol (store/base.py:1543), the CLI(
__main__.py:617,:646), and[integrity].audit_verify_on_start, which shipsFalse(
config/settings.py:3370). It returns one boolean and names only the first divergent row(
:8008).What was measured
Eight shapes driven at
c57903c2c: a throwaway SQLite store in a temp directory, six rows writtenthrough the real
MessageStore.record_audit, each shape applied out-of-band,verify_audit_chainre-run. Synthetic actors only, no PHI, nothing written into the repository. Both controls fired --
a clean store verified, an ordinary interior edit broke. A second full walk ran beside the shipped
verifier, because the shipped one reports only the first break.
Cases A, B and D reproduce row 1421's existing results. Three findings are new:
and keep their evidentiary value.
catches that shape by count and not by hash.
row_hashstill breaks the walk at the tombstoned row. Sopreserving the link bounds the damage without removing it.
The crux for the ruling: delete-then-reseal verifies clean. That is what an attacker who can
write the table would produce, so a re-sealing purge would leave a held off-box anchor as the only
working control.
What the three merged PRs already settled
Checked by content against
origin/main, not bygit merge-base --is-ancestor-- this repositorysquash-merges and deletes branches, so ancestry returns false for work that landed.
api/security.py:175-188carries the replacement reason. Note the word"inventing" is still in the file, quoted as the retired reasoning it corrects, so a bare grep for it
reads as "the fix did not land", which is the wrong answer.
docs/CONFIGURATION.md'saudit_daysrow is the source ofrecord;
settings.py:1675-1683andPHI.mdlink to it.Severity
Conditional per CLAUDE.md section 0. Zero deployments, so nothing is growing, and no operator is
affected today. A first deployment would grow
audit_logunbounded at one row per authenticatedread, with no configured window able to stop it.
Checks
ruff check .ruff format --check .mypy messagefoundry(strict)pytestdocs/ledger slicetest_backlog_status_check,test_ledger_check,test_backlog_citation_check,test_adr_analyze,test_backlog_score_advisory,test_backlog_hygiene_claim_extraction,test_doc_guards_lane,test_backlog_dependency_census,test_docs_cite_no_refused_config_keyspre-commitat commitSkipped: the full suite. This change is docs only -- three files, all under
docs/-- so noengine path is exercised by it. The two warnings in the docs slice are pre-existing baseline rows for
master-test-plan documents absent from this checkout, unrelated to this change. Hosted-runner-only
legs were not seen from this session and must be read on the PR.
Notes for the reader
alloc.ps1to this worktree, and its index row is in the samecommit. No number is cited that was not allocated.
In one line: make the anchor operational first, take a write-time bound as the primary lever, use
archive-first with the case C1 contract if the table itself must be bounded, and reject both hard
delete and re-seal.
Generated with Claude Code (https://claude.com/claude-code)