feat(maintenance): add immutable live-proof receipts - #3879
Conversation
Add the fixed read-only, candidate, and existing-apply receipt protocol for the reindex campaign. Receipts bind current archive, semantic, candidate, and input evidence while consumer seams reject stale or failed proof results. The maintenance command is deliberately evidence-only: it accepts only static proof ids and writes a new receipt outside the archive. Ref polylogue-x97cf. Co-Authored-By: Codex <noreply@openai.com>
|
@codex review |
|
Warning Review limit reached
Next review available in: 14 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThe change adds a read-only ChangesLive-proof evidence
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Operator
participant live_proof_command
participant collect_live_proof
participant Archive
participant ReceiptFile
Operator->>live_proof_command: invoke maintenance live-proof
live_proof_command->>collect_live_proof: pass proof route and inputs
collect_live_proof->>Archive: capture bindings and run read-only checks
Archive-->>collect_live_proof: return proof evidence
collect_live_proof-->>live_proof_command: return self-hashed receipt
live_proof_command->>ReceiptFile: write receipt as JSON
ReceiptFile-->>Operator: print formatted receipt
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dec47d1350
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| apply_result, digest = _validated_existing_apply_receipt(apply_receipt_path, bindings) | ||
| result = {"status": LiveProofStatus.PASSED.value, "apply_receipt": apply_result} |
There was a problem hiding this comment.
Reject unsuccessful existing-apply receipts
When a binding-current, self-hashed apply receipt reports a failed or blocked operation—or even has an empty result—this branch unconditionally wraps it with status: passed. Because the aggregate validators inspect only that outer status, an operation that never succeeded becomes acceptable terminal evidence; validate the operation-specific result and propagate unsuccessful outcomes instead.
Useful? React with 👍 / 👎.
| metadata.get("generation_id") != generation_id | ||
| or metadata.get("state") != "inactive" | ||
| or metadata.get("archive_root") != str(root) | ||
| or metadata.get("index_path") != str(index_path) | ||
| or not index_path.is_file() |
There was a problem hiding this comment.
Match candidates to their recorded source snapshot
If the durable source changes after an inactive candidate was created, generation.json still names the source snapshot from which that candidate was built, but this validation never compares that field with the current snapshot later placed in the receipt bindings. The command can therefore issue and subsequently validate a receipt that pairs an old candidate with the new source state, especially for semantic metadata changes that corpus-absences cannot detect; require the generation's recorded source_snapshot to equal the captured snapshot.
Useful? React with 👍 / 👎.
| candidate_generation_id=candidate_generation_id, | ||
| candidate_index_sha256=hash_file(candidate_index) if candidate_index is not None else None, |
There was a problem hiding this comment.
Bind active proofs to active index content
For the active read-only route, candidate_index_sha256 is always None, while the archive identity digest identifies the index file by inode rather than contents. If the daemon ingests or converges more data after collection, the same index.db inode and schema remain valid, so validation accepts stale count/check evidence without noticing the changed parsed tree; capture a coherent logical index snapshot or content binding for active proofs as well.
AGENTS.md reference: AGENTS.md:L163-L169
Useful? React with 👍 / 👎.
| if not is_json_document(payload.get("result")): | ||
| raise LiveProofError("existing apply receipt result is malformed") | ||
| _validate_private_path_references(receipt_bindings.get("private_paths")) | ||
| return require_json_document(payload["result"], context="existing apply receipt result"), digest |
There was a problem hiding this comment.
Redact paths copied from apply results
When an existing apply receipt's result contains a normal absolute artifact path, such as a backup, manifest, or quarantine location, this returns the result unchanged and it is embedded in the durable live-proof receipt. Only bindings.private_paths is validated, so paths elsewhere in the result bypass the advertised digest-plus-basename policy; recursively reject or redact private paths before returning the result.
Useful? React with 👍 / 👎.
| generation_root = root / ".index-generations" / generation_id | ||
| metadata_path = generation_root / "generation.json" |
There was a problem hiding this comment.
Resolve candidates from the canonical generation store
For archives whose configured index lives outside the archive root, IndexGenerationStore places .index-generations beside the canonical active-index pointer, but this code always searches under root/.index-generations. Consequently a valid inactive generation in a supported split-root or symlink topology is reported as unavailable; derive the generation root through ArchiveLocation/IndexGenerationStore rather than hard-coding it beneath the archive root.
Useful? React with 👍 / 👎.
| paths = { | ||
| "source": root / "source.db", | ||
| "index": candidate_index or root / "index.db", | ||
| "embeddings": root / "embeddings.db", | ||
| "user": root / "user.db", | ||
| } |
There was a problem hiding this comment.
Bind the ops-tier schema version
When ops.db is recreated or its schema version changes after receipt creation, validation still accepts the old receipt because this schema binding enumerates only four tiers. The archive-verification tier-schema check does inspect the ops tier, so omitting it allows an old successful result to remain current after one of its checked schemas changed; derive these bindings from the canonical tier registry and include ops.db.
AGENTS.md reference: AGENTS.md:L114-L122
Useful? React with 👍 / 👎.
| with os.fdopen(descriptor, "wb", closefd=False) as stream: | ||
| stream.write(encoded) | ||
| stream.flush() | ||
| os.fsync(stream.fileno()) | ||
| finally: |
There was a problem hiding this comment.
Make exclusive receipt writes failure-atomic
If writing or fsyncing the receipt fails—for example because of ENOSPC or process interruption—the final path has already been created and can contain truncated JSON. A retry then hits O_EXCL and refuses to replace that corrupt artifact, leaving the operator unable to produce the promised immutable receipt at that path; write and fsync a temporary sibling, then publish it atomically with exclusive-create semantics.
Useful? React with 👍 / 👎.
| parsed_residues.append( | ||
| LiveProofResidue(LiveProofResidueKind(cast(str, residue["kind"])), cast(str, residue["code"])) | ||
| ) |
There was a problem hiding this comment.
Validate residue codes as strings
When a self-hashed receipt contains a residue whose code is a JSON object, list, number, or null, cast(str, ...) performs no runtime validation and this parser accepts it into a supposedly typed LiveProofResidue. A malformed not_applicable residue can consequently pass _require_acceptable_result; explicitly require a nonempty string code before constructing the residue.
Useful? React with 👍 / 👎.
| if not is_json_document(result) or not isinstance(residues, list) or not isinstance(input_digests, list): | ||
| raise LiveProofError("live-proof receipt evidence is malformed") | ||
| try: | ||
| LiveProofStatus(cast(str, result["status"])) | ||
| except (KeyError, TypeError, ValueError) as exc: | ||
| raise LiveProofError("live-proof receipt result status is malformed") from exc |
There was a problem hiding this comment.
Require route-specific evidence before validation
A self-hashed receipt with current bindings, proof_id: archive-verification, and only result: {"status":"passed"} is accepted here even though it contains no archive-verification result and need not have run any registered check. The same generic validation permits mode-incompatible input-digest and evidence shapes, so aggregates cannot distinguish a producer-issued proof from a structurally incomplete document; validate the exact result, residue, and input-digest schema declared by each LiveProofSpec.
Useful? React with 👍 / 👎.
| if status is LiveProofStatus.PASSED: | ||
| return | ||
| if status is LiveProofStatus.NOT_APPLICABLE and any( | ||
| residue.kind is LiveProofResidueKind.NOT_APPLICABLE for residue in receipt.residues | ||
| ): |
There was a problem hiding this comment.
Reject passed receipts with blocking residues
When a receipt claims passed, this returns without checking whether its typed residues include blocked, unverified, or check_failed; similarly, not_applicable is accepted as soon as any one residue has that kind even if another residue is blocking. Such internally contradictory evidence therefore passes every aggregate boundary, so acceptance must enforce status/residue consistency and reject any blocking or unverified residue.
Useful? React with 👍 / 👎.
Problem: live-proof bindings omitted the canonical ops and audit tiers, so a receipt could remain valid while one of those schema files changed or was absent.\n\nWhat changed: include all six archive-tier schema versions in the binding and document the complete contract. Add a regression assertion for the tier vocabulary.\n\nCompatibility/migration: receipts produced before this change are stale because the binding shape is intentionally stronger.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7ec146b783
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| repository = Path(__file__).resolve().parents[2] | ||
| completed = subprocess.run( | ||
| ("git", "-C", str(repository), "rev-parse", "--verify", "HEAD"), | ||
| check=False, |
There was a problem hiding this comment.
Reject proofs collected from dirty Git worktrees
When collection runs from a checkout with tracked modifications, git rev-parse HEAD still returns the clean commit SHA, so the receipt claims that commit even though modified collector or verification code produced its evidence; a later clean checkout at the same HEAD then accepts the false binding. Refuse dirty worktrees or bind the receipt to a digest of the code actually executed.
Useful? React with 👍 / 👎.
| def validate_final_proof_receipts(receipts: Sequence[object], archive_root: Path) -> tuple[LiveProofReceipt, ...]: | ||
| """Consumer seam for final-proof aggregation, without emitting a terminal proof.""" | ||
|
|
||
| return validate_live_operation_aggregate(receipts, archive_root) |
There was a problem hiding this comment.
Require complete proof coverage at final aggregation
When a final consumer supplies only one otherwise valid receipt, this delegates to validate_live_operation_aggregate, whose only set-level requirement is that the sequence be nonempty. A lone archive-verification receipt therefore validates while candidate or apply-route evidence is omitted entirely; require the expected unique proof IDs, using typed not-applicable evidence for routes that legitimately do not apply.
Useful? React with 👍 / 👎.
| bead_id="polylogue-x97cf", | ||
| mode=LiveProofMode.CANDIDATE, | ||
| producer="archive_verification", | ||
| archive_checks=("corpus-absences",), |
There was a problem hiding this comment.
Run the canonical candidate acceptance profiles
When replay has populated every session but terminal derived-state work has not finished, corpus-absences can pass even though FTS, action pairs, delegation facts, planner statistics, or other required candidate invariants remain incomplete. The production rebuild runs the full REINDEX_ACCEPTANCE_CHECKS and REINDEX_CROSS_TIER_ACCEPTANCE_CHECKS profiles before promotion, but this advertised candidate-acceptance receipt runs only one cross-tier check and is then accepted by validate_candidate_proof_receipts; derive this route from the canonical profiles and apply strict acceptance.
Useful? React with 👍 / 👎.
| or metadata.get("state") != "inactive" | ||
| or metadata.get("archive_root") != str(root) | ||
| or metadata.get("index_path") != str(index_path) | ||
| or not index_path.is_file() |
There was a problem hiding this comment.
Reject symlinked candidate index files
When a corrupted or replaced generation contains index.db as a symlink, is_file() follows that link, so metadata that names the lexical candidate path still passes even if the link targets the active index or another generation. Collection and validation then hash and verify the target while issuing a receipt for the named inactive candidate; require a regular non-symlink file whose resolved path is owned by the generation and is distinct from the active index.
Useful? React with 👍 / 👎.
| target.relative_to(root) | ||
| except ValueError: | ||
| pass | ||
| else: | ||
| raise click.BadParameter("receipt output must be outside the archive root", param_hint="--output") |
There was a problem hiding this comment.
Protect external generation roots from receipt output
When .index-active-pointer selects an index generation outside the configured archive root, this containment check protects only root; an output path inside the external active generation or its .index-generations store is therefore accepted. The supposedly read-only command then mutates archive-owned storage, and the receipt can disappear when that generation is retired or reclaimed; derive every protected archive path from ArchiveLocation/IndexGenerationStore, not just the configured root.
AGENTS.md reference: AGENTS.md:L163-L166
Useful? React with 👍 / 👎.
Problem: the existing-apply live-proof route wrapped any input result as a passed proof, including blocked, failed, or unknown mutation outcomes.\n\nWhat changed: accept only applied and already_satisfied input outcomes and add a regression test for unknown results.\n\nCompatibility/migration: previously collected live-proof receipts remain bound to their original protocol version; new existing-apply inputs fail closed unless their mutation succeeded.
|
@codex review |
There was a problem hiding this comment.
Actionable comments posted: 12
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@polylogue/cli/commands/maintenance/_live_proof.py`:
- Around line 52-54: Update the exception handler around
write_live_proof_receipt to also catch OSError alongside LiveProofError,
translating both into click.ClickException while preserving exception chaining
and the existing error message behavior.
In `@polylogue/maintenance/live_proof.py`:
- Around line 566-584: Update write_live_proof_receipt to fsync the parent
directory after the receipt file is successfully written and closed, using
target.parent and an appropriate directory descriptor. Preserve exclusive
creation and ensure the directory descriptor is closed reliably, including when
writing or syncing the file fails.
- Around line 256-263: Unescaped filesystem paths are used in SQLite read-only
URI connections. Add a shared _read_only_uri helper that percent-encodes paths
safely, then use it in _schema_versions at
polylogue/maintenance/live_proof.py:256-263 and capture_live_proof_bindings at
polylogue/maintenance/live_proof.py:307-308, replacing both raw
f"file:{...}?mode=ro" constructions.
- Around line 338-341: Update collect_live_proof to resolve archive_root once
before invoking capture_live_proof_bindings, _candidate_index, or
verify_archive. Reuse that resolved path for candidate binding validation and
verification so relative, home-relative, and symlinked roots match the captured
metadata.
- Around line 574-584: Update the receipt-writing flow around os.open and the
descriptor-backed stream: translate all OSError failures from os.open, while
preserving the existing LiveProofError for FileExistsError, into LiveProofError
so the CLI’s existing handler can present a ClickException. Track
write/flush/fsync success and, when any stream operation fails, close the
descriptor and unlink target before propagating the translated error; preserve
the completed receipt and existing cleanup behavior on success.
- Around line 233-238: Update the subprocess.run call used for the git rev-parse
verification to include an explicit timeout, and catch subprocess.TimeoutExpired
in the surrounding live-proof flow, converting it into LiveProofError. Preserve
the existing successful-result handling and fixed command arguments.
In `@tests/unit/maintenance/test_live_proof.py`:
- Around line 171-182: Add direct unit coverage for _require_acceptable_result
through validate_live_operation_aggregate: verify a self-hashed not_applicable
result is accepted when a residue with kind not_applicable is present, and
raises LiveProofError with “not acceptable” when that residue is absent. Reuse
the existing receipt construction and hashing pattern from
test_aggregate_rejects_a_self_hashed_failed_proof_result.
- Around line 164-168: Update the test mutation around
validate_candidate_proof_receipts to modify the candidate index through a
supported SQLite connection and UPDATE statement rather than appending raw bytes
to index.db. Preserve the intended digest change while ensuring
capture_live_proof_bindings and _schema_versions can read the database, so the
assertion continues to target the “bindings are stale” LiveProofError.
- Line 80: Replace the non-callability assertion for spec.producer in the
LIVE_PROOF_SPECS test with an assertion that every producer matches one of the
allowed literal values: "archive_verification" or "existing_apply_receipt".
- Around line 35-40: Update the archive_root fixture to resolve the temporary
archive path once before initializing and returning it, ensuring the path
recorded by _candidate matches the resolved root used by
capture_live_proof_bindings and _candidate_index.
- Around line 140-151: Extend
test_existing_apply_receipt_rejects_non_successful_result to verify both
statuses in _ACCEPTED_APPLY_STATUSES: retain the existing applied-success
coverage and add an already_satisfied receipt that collect_live_proof accepts
without raising. Keep the unknown-status case asserting LiveProofError with the
existing failure message.
- Around line 57-58: Update the candidate index setup in the live-proof test to
preserve WAL-backed state: stop active writers and checkpoint the database
before copying index.db, or consistently copy index.db together with its -wal
and -shm sidecars. Ensure the candidate digest fully represents the database
state.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: c36160aa-ac53-4113-b9b2-19cfeb4d2fd4
📒 Files selected for processing (6)
docs/maintenance.mdpolylogue/cli/commands/maintenance/__init__.pypolylogue/cli/commands/maintenance/_live_proof.pypolylogue/maintenance/live_proof.pytests/unit/cli/test_maintenance_live_proof_cli.pytests/unit/maintenance/test_live_proof.py
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ecdc1966a0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| parser_fingerprints=parser_fingerprints, | ||
| lowering_fingerprint=lowering, | ||
| candidate_generation_id=candidate_generation_id, | ||
| candidate_index_sha256=hash_file(candidate_index) if candidate_index is not None else None, |
There was a problem hiding this comment.
Bind candidate receipts to WAL-visible content
When an inactive candidate has committed but uncheckpointed WAL frames—normal while a rebuild writer is active—verify_archive reads the logical database including those frames, but this binding hashes only index.db. Further commits can append to index.db-wal without changing that hash, so validation can accept a receipt whose corpus evidence predates the candidate's current contents. Require a quiescent completed generation with a clean checkpoint, or bind a coherent SQLite snapshot including WAL-visible state.
Useful? React with 👍 / 👎.
| def validate_live_operation_aggregate(receipts: Sequence[object], archive_root: Path) -> tuple[LiveProofReceipt, ...]: | ||
| """Consumer seam for the live-operation aggregate, without scheduling work.""" | ||
|
|
||
| validated = tuple(validate_live_proof_receipt(receipt, archive_root) for receipt in receipts) |
There was a problem hiding this comment.
Capture one binding snapshot for the whole aggregate
When the daemon ingests between these sequential validations, an older receipt can match the pre-ingest snapshot and a newer receipt can match the post-ingest snapshot, so both are accepted even though the returned aggregate combines evidence for different source states. Capture one coherent set of current bindings for the aggregate and require every receipt's shared bindings to match it rather than recapturing independently per item.
AGENTS.md reference: AGENTS.md:L163-L166
Useful? React with 👍 / 👎.
Problem: The live-proof protocol accepted incomplete archive evidence and could bind receipts to stale code, candidate metadata, or mutable SQLite files. Existing apply results also lost typed terminal status when converted into proof outcomes.\n\nWhat changed: Bind installed or clean-checkout code identity, canonical archive and generation paths, source snapshots, all tier schemas, active and candidate SQLite file sets, complete verification profiles, status/residue pairs, route coverage, and embedded apply evidence. Add atomic exclusive receipt publication and CLI write-error handling. Extend focused route tests and documentation.\n\nAlternatives rejected: The protocol remains a read-only evidence collector. It does not acquire, repair, promote, restart, or schedule archive work.\n\nCompatibility/migration: Existing receipts are intentionally stale unless they match the stricter bindings. No production archive was mutated. Ref polylogue-x97cf Co-Authored-By: Codex <noreply@openai.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93794158d1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| active_index_sha256 = _sqlite_file_set_digest(location.active_index_path, allow_symlink=True) | ||
| candidate_index_sha256 = _sqlite_file_set_digest(candidate_index) if candidate_index is not None else None | ||
| schema_versions = _schema_versions(location) |
There was a problem hiding this comment.
Bind every verified tier's contents
When a completed write changes user.db assertions, embedding references, or ops cursor/debt rows in place after collection, the inode-based archive identity and schema versions remain unchanged, as do the source snapshot and active-index digest. Validation therefore accepts the old receipt even though registered checks such as user-tier-refs, embeddings-refs-liveness, and convergence-freshness would now produce different evidence or fail; capture coherent content digests for every tier read by the selected profiles.
AGENTS.md reference: AGENTS.md:L120-L122
Useful? React with 👍 / 👎.
| return value | ||
| raise LiveProofError("archive verification emitted non-JSON evidence") | ||
|
|
||
| evidence = {key: value for key, value in document.items() if key not in {"archive_root", "generated_at"}} |
There was a problem hiding this comment.
Expire time-sensitive verification evidence
When an ingest cursor is behind but was updated just under the one-hour stall threshold, stalled-append-cursor-freshness reports ok; after the threshold passes with no database changes, the canonical check would report a warning. Removing generated_at and enforcing no age limit lets the earlier passed receipt retain identical bindings and remain acceptable indefinitely, so preserve collection time and reject or refresh receipts when a time-dependent predicate can have changed.
Useful? React with 👍 / 👎.
Problem: the live-proof CLI rejected receipt paths inside the configured archive root but accepted paths inside an externally pointed active generation or rebuild directory. Candidate proof mutation tests also changed SQLite bytes outside a supported database operation.\n\nWhat changed: move archive-owned root discovery into the maintenance layer, reject receipt paths under configured and externally selected lifecycle roots, replace raw candidate-byte mutation with a SQLite schema mutation, and add CLI coverage for an external active generation.\n\nCompatibility/migration: receipt output remains allowed in unrelated private directories. No archive files are mutated by the proof command.\n\nRef polylogue-x97cf\n\nCo-Authored-By: Codex <noreply@openai.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5b8d02e6c8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if any( | ||
| path.with_name(path.name + "-wal").exists() or path.with_name(path.name + "-wal").is_symlink() for path in paths | ||
| ): |
There was a problem hiding this comment.
Permit checkpointed WAL files during live collection
When polylogued has opened any archive tier, SQLite's normal WAL mode leaves a *-wal file present even when all frames are checkpointed, but this predicate rejects the file solely for existing (including zero-length WALs that _sqlite_file_state explicitly treats as absent). Because daemon connections are cached and configured for WAL, the advertised live-proof command cannot collect evidence from the normal running-daemon environment; distinguish nonempty/uncheckpointed WAL content from a harmless quiescent sidecar or acquire a coherent read snapshot instead.
AGENTS.md reference: AGENTS.md:L163-L166
Useful? React with 👍 / 👎.
| generation.generation_id != generation_id | ||
| or generation.state != "inactive" | ||
| or Path(generation.archive_root).resolve() != location.configured_root.resolve() |
There was a problem hiding this comment.
Require a ready rebuild transaction for candidate proofs
When a rebuild is running, paused, deferred, or has crashed before its terminal stages, its generation remains inactive; readiness is recorded separately by IndexRebuildTransaction.status == "ready". Accepting every inactive generation therefore allows a candidate proof to attest an in-progress snapshot, and bulk rebuilds use a non-WAL write profile so the WAL guard does not reliably exclude this case. Resolve the owning transaction and require its ready/completed state before treating the generation as a proof candidate.
Useful? React with 👍 / 👎.
| root = archive_root().resolve() | ||
| target = output.expanduser().resolve() | ||
| try: | ||
| owned_roots = archive_owned_storage_roots(root) |
There was a problem hiding this comment.
Translate invalid archive pointers into a CLI error
When .index-active-pointer is malformed, ArchiveLocation.resolve() raises ArchiveLocationError, which is a RuntimeError; this call occurs inside a handler that catches only LiveProofError and OSError. The maintenance command therefore escapes through Click with an uncaught exception instead of reporting a normal actionable failure, even though the later binding collector already normalizes archive-location failures. Catch and translate ArchiveLocationError here or have archive_owned_storage_roots() wrap it as LiveProofError.
Useful? React with 👍 / 👎.
Problem: live-proof collection accepted path aliases without normalizing the archive root once, and the aggregate path lacked a direct not-applicable residue test. What changed: normalize the archive root before binding and verification, and add an aggregate test proving not-applicable status requires its typed residue. Verification: devtools test tests/unit/maintenance/test_live_proof.py tests/unit/cli/test_maintenance_live_proof_cli.py; devtools verify --quick Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6068926946
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| _fsync_directory(target.parent) | ||
| temporary.unlink() | ||
| temporary = None |
There was a problem hiding this comment.
Remove the published target when directory sync fails
If either directory fsync fails after os.link publishes the receipt—for example because the filesystem reports an I/O error—the exception cleanup removes only the temporary name and leaves target in place. The command then reports that the write failed, but a retry is permanently rejected because the supposedly failed output already exists; track whether publication succeeded and unlink the target on this failure path.
Useful? React with 👍 / 👎.
| if _ABSOLUTE_PATH_RE.search(value): | ||
| return f"[private-text:{hash_text(value)}]" | ||
| return value |
There was a problem hiding this comment.
Redact colon-prefixed absolute paths
When archive evidence embeds an absolute path immediately after a colon, this regex deliberately fails to recognize it. The raw-quarantine-group-dedup check emits exactly this shape as group:{group.source_path} in details, so a quarantined group whose source path is /home/... persists that private path verbatim as group:/home/... in the durable receipt despite the documented redaction guarantee.
Useful? React with 👍 / 👎.
Problem: the live-proof root-normalization fix lacked a regression test for a symlinked archive path. What changed: collect a read-only receipt through an archive-root alias and compare its private binding with the canonical root reference. Verification: devtools test tests/unit/maintenance/test_live_proof.py tests/unit/cli/test_maintenance_live_proof_cli.py; devtools verify --quick Co-Authored-By: Claude <noreply@anthropic.com>
Problem: the archive-root alias regression covered capture bindings but did not exercise validation through the same alias.\n\nWhat changed: validate the captured receipt with the symlinked archive root so the capture and verification paths must normalize to the same canonical root.\n\nVerification: devtools test tests/unit/maintenance/test_live_proof.py tests/unit/cli/test_maintenance_live_proof_cli.py passed with 32 tests.\n\nRef polylogue-x97cf.\n\nCo-Authored-By: Codex <noreply@openai.com>
Problem: the archive-root alias test did not assert that the alias itself stayed out of serialized evidence.\n\nWhat changed: require the live-proof document to redact the symlink alias in addition to validating through that alias.\n\nVerification: devtools test tests/unit/maintenance/test_live_proof.py tests/unit/cli/test_maintenance_live_proof_cli.py passed with 32 tests.\n\nRef polylogue-x97cf.\n\nCo-Authored-By: Codex <noreply@openai.com>
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@polylogue/maintenance/live_proof.py`:
- Around line 637-639: Update the error message raised by the apply-status
validation in the live proof receipt handling to state that the status is not
recognized, rather than claiming it is not successful; update the corresponding
assertion in test_live_proof.py to expect the new wording.
- Around line 394-417: Close every SQLite connection explicitly in
_sqlite_file_set_digest, _schema_version, _schema_versions, and
capture_live_proof_bindings instead of relying on the connection context
manager; wrap each _open_readonly(path) result with contextlib.closing or close
it in finally, while preserving the existing transaction and read behavior.
- Around line 304-316: Update _require_quiescent_sqlite to reject paths with
either a SQLite -wal or -journal sidecar, checking both regular files and
symlinks. Keep the existing LiveProofError behavior and align the quiescence
gate with the sidecars already represented by _SQLITE_SIDECARS.
- Around line 1028-1044: Update the exception handling around the link operation
so the FileExistsError branch performs the same temporary-file cleanup as the
OSError branch, including unlinking temporary and syncing target.parent when
applicable. Preserve the existing LiveProofError messages and ensure cleanup
failures remain suppressed.
In `@tests/unit/maintenance/test_live_proof.py`:
- Around line 436-444: Extend test_readonly_uri_encodes_sqlite_metacharacters to
use a filename containing a literal percent sequence, such as “%2F”, alongside
the existing “?” and “#” cases. Assert that _readonly_uri preserves the stdlib
Path.as_uri escaping contract by encoding the percent character as “%25”, and
retain the SQLite read-only connection verification.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b1edc049-df08-4965-a4f9-3b52e3ed746b
📒 Files selected for processing (5)
docs/maintenance.mdpolylogue/cli/commands/maintenance/_live_proof.pypolylogue/maintenance/live_proof.pytests/unit/cli/test_maintenance_live_proof_cli.pytests/unit/maintenance/test_live_proof.py
| def _readonly_uri(path: Path) -> str: | ||
| return f"{path.resolve(strict=True).as_uri()}?mode=ro&immutable=1" | ||
|
|
||
|
|
||
| def _open_readonly(path: Path) -> sqlite3.Connection: | ||
| return sqlite3.connect(_readonly_uri(path), uri=True, timeout=2) | ||
|
|
||
|
|
||
| def _require_quiescent_sqlite(paths: Sequence[Path]) -> None: | ||
| if any( | ||
| path.with_name(path.name + "-wal").exists() or path.with_name(path.name + "-wal").is_symlink() for path in paths | ||
| ): | ||
| raise LiveProofError("live-proof requires a quiescent archive without SQLite WAL files") |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Reject a hot rollback journal, not only a WAL file.
_readonly_uri sets immutable=1. SQLite then assumes the database never changes, skips locking, and does not run hot-journal rollback or WAL recovery. _require_quiescent_sqlite only rejects a -wal file. A -journal file left by an interrupted writer therefore passes the quiescence gate, and every read in _sqlite_file_set_digest, _schema_version, and the raw_sessions query returns the un-rolled-back image. The receipt then binds evidence that does not match the committed database state. _SQLITE_SIDECARS already treats -journal as part of the bound file set, so the gate and the digest disagree.
Check both sidecars in _require_quiescent_sqlite.
🛡️ Proposed fix
def _require_quiescent_sqlite(paths: Sequence[Path]) -> None:
- if any(
- path.with_name(path.name + "-wal").exists() or path.with_name(path.name + "-wal").is_symlink() for path in paths
- ):
- raise LiveProofError("live-proof requires a quiescent archive without SQLite WAL files")
+ sidecars = (
+ path.with_name(path.name + suffix) for path in paths for suffix in _SQLITE_SIDECARS
+ )
+ if any(sidecar.exists() or sidecar.is_symlink() for sidecar in sidecars):
+ raise LiveProofError("live-proof requires a quiescent archive without SQLite WAL or journal files")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _readonly_uri(path: Path) -> str: | |
| return f"{path.resolve(strict=True).as_uri()}?mode=ro&immutable=1" | |
| def _open_readonly(path: Path) -> sqlite3.Connection: | |
| return sqlite3.connect(_readonly_uri(path), uri=True, timeout=2) | |
| def _require_quiescent_sqlite(paths: Sequence[Path]) -> None: | |
| if any( | |
| path.with_name(path.name + "-wal").exists() or path.with_name(path.name + "-wal").is_symlink() for path in paths | |
| ): | |
| raise LiveProofError("live-proof requires a quiescent archive without SQLite WAL files") | |
| def _readonly_uri(path: Path) -> str: | |
| return f"{path.resolve(strict=True).as_uri()}?mode=ro&immutable=1" | |
| def _open_readonly(path: Path) -> sqlite3.Connection: | |
| return sqlite3.connect(_readonly_uri(path), uri=True, timeout=2) | |
| def _require_quiescent_sqlite(paths: Sequence[Path]) -> None: | |
| sidecars = ( | |
| path.with_name(path.name + suffix) for path in paths for suffix in _SQLITE_SIDECARS | |
| ) | |
| if any(sidecar.exists() or sidecar.is_symlink() for sidecar in sidecars): | |
| raise LiveProofError("live-proof requires a quiescent archive without SQLite WAL or journal files") |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@polylogue/maintenance/live_proof.py` around lines 304 - 316, Update
_require_quiescent_sqlite to reject paths with either a SQLite -wal or -journal
sidecar, checking both regular files and symlinks. Keep the existing
LiveProofError behavior and align the quiescence gate with the sidecars already
represented by _SQLITE_SIDECARS.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1fbd1331b7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| name = check.get("name") | ||
| status = check.get("status") | ||
| if not isinstance(name, str) or not isinstance(status, str): | ||
| raise LiveProofError("live-proof receipt archive verification evidence is malformed") |
There was a problem hiding this comment.
Validate complete archive-check evidence
When a failed receipt is rewritten so every canonical check contains only {"name": ..., "status": "ok"} and is then self-hashed again, this validator accepts it and aggregate validation treats it as passing because no summary, count, breakdown, evidence, check class, waiver, or report-level fields are required. Fresh evidence for the earlier route-schema concern is that the new _validate_archive_result implementation validates only these two fields, so require the complete canonical check/report shape or producer-authenticated evidence before accepting the receipt.
Useful? React with 👍 / 👎.
| if not isinstance(payload.get("operation_id"), str) or not payload["operation_id"]: | ||
| raise LiveProofError("existing apply receipt operation binding is invalid") |
There was a problem hiding this comment.
Bind apply evidence to a registered operation
When an unrelated or invented apply receipt has current bindings and a terminal status, any nonempty operation_id passes here; validate_final_proof_receipts subsequently checks only that the generic existing-apply-receipt proof ID occurs once, so that unrelated receipt can satisfy the campaign's sole apply-evidence slot. Validate the operation ID against a closed route-specific operation/Bead mapping rather than accepting arbitrary text.
Useful? React with 👍 / 👎.
| def _code_sha() -> str: | ||
| configured = os.environ.get("POLYLOGUE_CODE_SHA", "").strip().lower() | ||
| if configured: | ||
| if not _CODE_SHA_RE.fullmatch(configured): | ||
| raise LiveProofError("POLYLOGUE_CODE_SHA must be an exact git commit SHA") | ||
| return configured |
There was a problem hiding this comment.
Check checkout cleanliness before honoring the SHA override
When POLYLOGUE_CODE_SHA is present while this runs from a Git checkout, the function returns before locating the repository or running git status, so modified collector or verification code is still attested as the configured clean commit. Fresh evidence for the earlier dirty-worktree concern is that the newly added cleanliness guard exists only in the fallback branch; check checkout cleanliness first, or permit this override only when no checkout is present.
Useful? React with 👍 / 👎.
| def _require_quiescent_sqlite(paths: Sequence[Path]) -> None: | ||
| if any( | ||
| path.with_name(path.name + "-wal").exists() or path.with_name(path.name + "-wal").is_symlink() for path in paths | ||
| ): | ||
| raise LiveProofError("live-proof requires a quiescent archive without SQLite WAL files") |
There was a problem hiding this comment.
Resolve symlinks before checking SQLite sidecars
After a normal index-generation promotion, the canonical active index.db is a symlink, but this checks for index.db-wal beside the symlink instead of beside its resolved target, where SQLite actually places the WAL. The later _sqlite_file_state resolves the symlink and merely hashes that target WAL, while immutable binding reads ignore it, so an actively changing promoted archive bypasses the advertised quiescence guard and can produce evidence from a different logical state than the schema binding; resolve each database path before testing its sidecars.
Useful? React with 👍 / 👎.
| validated = validate_live_operation_aggregate(receipts, archive_root) | ||
| proof_ids = [receipt.proof_id for receipt in validated] | ||
| if len(proof_ids) != len(LiveProofId) or set(proof_ids) != set(LiveProofId): | ||
| raise LiveProofError("final proof requires complete route coverage exactly once") |
There was a problem hiding this comment.
Preserve candidate evidence across promotion
When terminal proof validation runs after the accepted candidate has been promoted, this aggregate validation recaptures the candidate binding through _candidate_index, which requires that generation to remain inactive and distinct from the active index. Promotion makes it active, points the active index at it, and changes the active-index binding, so the required pre-promotion candidate receipt can no longer validate and a replacement candidate receipt cannot be collected; validate that historical evidence through the promotion receipt and retained generation identity instead of requiring a currently inactive candidate.
Useful? React with 👍 / 👎.
Problem: live-proof receipts could outlive time-sensitive checks, omit content changes in verified tiers, reject harmless checkpointed WAL files, attest candidates without a ready rebuild transaction, leak colon-prefixed paths, or leave a published receipt after directory sync failure.\n\nWhat changed: bind all active tier file sets, timestamp and expire receipts, read WAL-visible SQLite state, require the owning rebuild transaction to be ready, tighten path redaction, normalize archive pointer failures at the maintenance boundary, and remove published output on post-publication errors. Add focused regressions for each safety boundary.\n\nVerification: devtools test tests/unit/maintenance/test_live_proof.py tests/unit/cli/test_maintenance_live_proof_cli.py passed with 36 tests. devtools verify --quick passed all 24 steps.\n\nRef polylogue-x97cf.\n\nCo-Authored-By: Codex <noreply@openai.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f9089016bb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| commit = (VERSION_INFO.commit or "").lower() | ||
| if not _CODE_SHA_RE.fullmatch(commit): | ||
| raise LiveProofError("installed package has no exact build commit") | ||
| return commit |
There was a problem hiding this comment.
Reject dirty installed builds before returning their commit
For an installed wheel or container built from a dirty tree, this returns only the base commit even though polylogue/version.py:_get_embedded_build_info preserves BUILD_DIRTY in VERSION_INFO.dirty; a clean build of the same commit can therefore validate evidence produced by modified collector or verification code. Fresh evidence beyond the earlier checkout and SHA-override cases is that the installed-package branch has its own explicit dirty bit, so reject dirty builds or include that state in the code identity.
Useful? React with 👍 / 👎.
| location = ArchiveLocation.resolve(archive_root) | ||
| except RuntimeError as exc: | ||
| raise LiveProofError("archive-owned storage roots are unavailable") from exc | ||
| bases = {location.configured_root.resolve(), location.active_index_path.parent.resolve()} |
There was a problem hiding this comment.
Resolve the active index before protecting its parent
In the supported pointerless symlink-farm case (root/index.db -> /external/index.db), active_index_path.parent.resolve() resolves the configured /root, not /external, so an output such as /external/index.db-wal passes the CLI containment check; the receipt writer then puts JSON at SQLite's WAL pathname, and reopening the WAL-mode archive can fail with a disk I/O error. Fresh evidence beyond the earlier pointer-generation case is that this occurs specifically when no .index-active-pointer exists; include location.active_index.resolved_path.parent among the protected roots.
AGENTS.md reference: AGENTS.md:L163-L166
Useful? React with 👍 / 👎.
| except FileExistsError as exc: | ||
| raise LiveProofError("live-proof receipt output already exists") from exc |
There was a problem hiding this comment.
Remove the temporary receipt when the target exists
When the requested output already exists, os.link raises only after the temporary sibling has been fully written and fsynced, but this specialized FileExistsError branch bypasses the generic cleanup below. Every refused retry therefore leaves another hidden .target.* receipt in the destination directory; unlink and directory-sync the temporary file before reporting the exclusive-create conflict.
Useful? React with 👍 / 👎.
|
|
||
|
|
||
| def _open_readonly(path: Path) -> sqlite3.Connection: | ||
| return sqlite3.connect(_readonly_uri(path), uri=True, timeout=2) |
There was a problem hiding this comment.
Avoid creating SQLite sidecars during proof collection
When a quiescent WAL-mode tier has no -wal or -shm files, opening it with SQLite mode=ro can still create and retain a zero-length WAL plus a 32 KiB SHM file when the directory is writable. _sqlite_file_state normalizes the new empty WAL as absent and does not track SHM, so collection succeeds while this advertised read-only command has mutated every affected archive directory; use the existing immutable read path for a verified sidecar-free snapshot or otherwise prevent sidecar creation.
AGENTS.md reference: AGENTS.md:L163-L166
Useful? React with 👍 / 👎.
| else None | ||
| ) | ||
| identity = ArchiveIdentity.resolve_location(location) | ||
| active_index_sha256 = _sqlite_file_set_digest(location.active_index_path, allow_symlink=True) |
There was a problem hiding this comment.
Reuse the active index file-set digest
Every proof route hashes the complete active index.db file set here, then immediately hashes the same path again when _active_tier_paths(location) supplies the index entry for tier_file_set_digests. On the repository's tens-of-gigabytes production indexes this doubles the dominant binding I/O and lengthens the window in which live collection becomes stale; capture the index digest once and reuse it for both binding fields.
Useful? React with 👍 / 👎.
| tier_file_set_digests = tuple( | ||
| (name, _sqlite_file_set_digest(path, allow_symlink=name == "index")) | ||
| for name, path in _active_tier_paths(location) | ||
| ) |
There was a problem hiding this comment.
Bind receipts to the physical blob namespace
The selected archive profiles run blob-integrity and attachment-acquisition-debt against blob/, but these bindings cover only SQLite file sets, while source_snapshot re-verifies only blobs referenced as raw payloads. If a non-raw acquired attachment is deleted or corrupted, or an orphan blob is added, every recorded binding remains unchanged, so validation accepts the old passed receipt even though rerunning the canonical checks would now fail; include a coherent digest of the complete physical blob namespace, preferably produced by the existing full scan.
Useful? React with 👍 / 👎.
Problem Automated review found that live-proof validation still leaked open SQLite connections, accepted incomplete archive evidence, allowed arbitrary apply operation ids, and could not validate a candidate receipt after promotion. What changed Close read-only connections, validate the complete archive report shape, require the registered source-remediation operation id, keep receipt writes failure-atomic, reject dirty checkout SHA overrides, and validate retained candidate generations by their current source-side bindings and candidate file digest. Add regression coverage for each boundary and document the post-promotion route. Compatibility/migration Existing apply receipts must use the registered source-remediation operation id. No archive mutation or production operation was performed. Ref polylogue-x97cf. Co-Authored-By: Claude <noreply@anthropic.com>
Problem The live-proof protocol now rejects unregistered apply operation ids, but the maintenance guide described only a generic registered route. What changed Name the current source-remediation operation id in the route documentation so operators can construct an accepted apply receipt without guessing. Compatibility/migration Apply receipts using other operation ids remain rejected until a route is added to the closed registry. Ref polylogue-x97cf. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 02e3ac9868
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if configured: | ||
| if not _CODE_SHA_RE.fullmatch(configured): | ||
| raise LiveProofError("POLYLOGUE_CODE_SHA must be an exact git commit SHA") | ||
| return configured |
There was a problem hiding this comment.
Match the SHA override to the checked-out HEAD
When POLYLOGUE_CODE_SHA is set in a clean checkout, this returns any syntactically valid SHA without comparing it to git rev-parse HEAD, so collection and validation under the same environment can attest arbitrary code identity rather than the code that executed. Fresh evidence beyond the earlier dirty-worktree concern is that the cleanliness fix still bypasses HEAD resolution in this branch; resolve HEAD first and require the override to match it, reserving the override for installations without a checkout.
Useful? React with 👍 / 👎.
| generation_checks = { | ||
| "state": generation.state in {"active", "retained"}, | ||
| "archive_root": Path(generation.archive_root).resolve() == location.configured_root.resolve(), |
There was a problem hiding this comment.
Tie retained candidate evidence to the current promotion
After candidate A is promoted and then superseded by candidate B, A's metadata still has state == "active" while its separate retention_state becomes retained, so this predicate accepts A and _validate_aggregate combines its old candidate receipt with newly captured active/apply receipts for B. Fresh evidence beyond the earlier post-promotion preservation concern is that the new fallback accepts a superseded generation without validating a promotion receipt or linkage to the current active generation, allowing final proof to omit candidate acceptance evidence for the generation actually active.
Useful? React with 👍 / 👎.
| receipt_bindings = payload.get("bindings") | ||
| if not isinstance(receipt_bindings, Mapping) or receipt_bindings != bindings.to_document(): | ||
| raise LiveProofError("existing apply receipt bindings are stale or mismatched") |
There was a problem hiding this comment.
Preserve apply evidence across candidate promotion
The registered known-source-remediation apply happens before the source snapshot is frozen and the candidate is built, but this requires its embedded receipt to carry bindings identical to the current archive, including the active-index digest. Promotion necessarily changes that digest: collecting the wrapper after promotion rejects the original apply receipt here, while collecting it before promotion makes the wrapper fail final aggregate validation against the post-promotion active bindings. Preserve this historical prerequisite through promotion-specific evidence rather than requiring the pre-candidate apply receipt to match the final active index.
Useful? React with 👍 / 👎.
| bindings = capture_live_proof_bindings(resolved_archive_root, candidate_generation_id=candidate_generation_id) | ||
| if spec.producer == "archive_verification": | ||
| result, residues = _archive_verification_result( | ||
| spec, | ||
| resolved_archive_root, | ||
| candidate_generation_id=candidate_generation_id, | ||
| bindings=bindings, | ||
| ) |
There was a problem hiding this comment.
Recheck bindings after running archive verification
When the daemon commits to any bound tier after this snapshot is captured but before the full verification profile finishes, the command still writes a receipt whose evidence was read from newer state while its bindings describe older files; validating that newly emitted receipt then immediately reports stale bindings. Because collection is advertised for the live archive and the daemon is the normal writer, capture and compare a second binding snapshot after verification and refuse publication if it differs.
AGENTS.md reference: AGENTS.md:L163-L166
Useful? React with 👍 / 👎.
| binding_checks = { | ||
| "code_sha": recorded.code_sha == current.code_sha, | ||
| "source_snapshot": recorded.source_snapshot == current.source_snapshot, | ||
| "parser_fingerprints": recorded.parser_fingerprints == current.parser_fingerprints, | ||
| "lowering": recorded.lowering_fingerprint == current.lowering_fingerprint, |
There was a problem hiding this comment.
Revalidate archive identity after candidate promotion
When an archive is copy-restored or replaced in place with byte-identical tier files, its inode-backed archive_identity_digest changes while the source snapshot, tier digests, schemas, code fingerprints, and private root reference can all remain equal. This promoted-candidate fallback never compares the recorded archive identity with current.archive_identity_digest, so a candidate receipt issued for the prior archive instance is accepted against the replacement despite the protocol's exact-archive binding; include the authority identity in these post-promotion checks.
Useful? React with 👍 / 👎.
| payload = dict(document) | ||
| digest = payload.pop("receipt_sha256", None) | ||
| if not isinstance(digest, str) or not _SHA256_RE.fullmatch(digest) or hash_payload(payload) != digest: | ||
| raise LiveProofError("live-proof receipt self-hash is invalid") |
There was a problem hiding this comment.
A self-hashed document can add arbitrary top-level fields and still pass validation because the payload's key set is never checked; for example, adding "private_source": "/home/user/export.json" produces a receipt accepted by final consumers even though it violates the closed v1 schema and the private-path guarantee. Require exactly the fields emitted by LiveProofReceipt.payload() before accepting the hash rather than silently discarding extensions when constructing the typed return value.
Useful? React with 👍 / 👎.
| expected_root = store.generations_root / generation_id | ||
| index_resolved = index_path.resolve(strict=True) | ||
| expected_resolved = expected_root.resolve(strict=True) / "index.db" |
There was a problem hiding this comment.
Reject symlinked candidate generation directories
When .index-generations/gen-X itself is replaced by a symlink to an external directory, index_path.is_symlink() is false for the regular index.db beneath it and resolving both sides makes this equality succeed, so candidate collection attests storage outside the lifecycle store as a canonical owned generation. Fresh evidence beyond the earlier final-file symlink concern is that the new check protects only the leaf while explicitly following a symlinked generation ancestor; require the generation directory itself to be a real directory contained beneath the resolved generations root.
Useful? React with 👍 / 👎.
|
Residual scope for Ref polylogue-x97cf: harden the existing real CLI route and typed receipt validation for the three fixed modes (read_only, candidate, existing_apply_receipt), typed residues, immutable self-hashed bindings, exact candidate/source/package/code/schema fingerprints, input receipt ownership, private-path digest handling, fail-closed non-mutation, and registered-ID dispatch. Add anti-vacuity coverage against the production route. Exclusions: consumer orchestration, campaign redesign, production operations, local Beads mutation, and Bead closure. |
Summary
Add a fixed, read-only live-proof receipt protocol for the reindex campaign. The protocol emits immutable evidence for archive, inactive-candidate, and existing-apply-receipt routes and exposes validators for aggregate, candidate-acceptance, and final-proof consumers.
Problem
The phase graph requires source and candidate evidence to be bound to the exact code, archive, schema, parser, lowering, and generation state that produced it. The existing maintenance surfaces did not provide one typed receipt contract with fail-closed stale-evidence checks, private-path redaction, or a CLI route that could not execute archive mutations.
Solution
Add
polylogue.maintenance.live_proofwith a closed registry, immutable self-hashed receipts, current-binding validation, candidate generation checks, typed residuals, and consumer validators. Addpolylogue ops maintenance live-proof, which accepts only registered proof ids and writes a new receipt outside archive-owned storage with exclusive creation, including externally pointed active-generation and rebuild roots. The binding includes all six archive-tier schema versions, includingops.dbandaudit.db. Normalize the archive root once before binding and verification so path aliases cannot produce mismatched evidence. Validate the aggregate contract fornot_applicableresidues directly.Verification
direnv exec . devtools test tests/unit/maintenance/test_live_proof.py tests/unit/cli/test_maintenance_live_proof_cli.py: 41 passed.direnv exec . devtools verify --quick: all 24 steps exited 0 at02e3ac986884b7123aebf5b40af635fc62f1421a.not_applicableresidue and rejects the same status without its residue. The alias test proves symlinked archive roots bind to the canonical private path reference. Freshness, tier-content, WAL, ready-transaction, path-redaction, and failure-atomicity regressions are included.Scope disposition
The implementation is partial because the phase-specific source-remediation, candidate-acceptance, and final-proof orchestration consumers do not yet exist on the current master. The typed validator seams are present and the remaining wiring is carried by
polylogue-live-operation-receipts. No production archive mutation, candidate generation, promotion, restart, or postflight proof was performed.Ref polylogue-x97cf.
Summary by CodeRabbit
New Features
maintenance live-proofcommand for generating tamper-evident evidence receipts.Documentation