You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Counterparty crypto-erasure guards are production-wired and invoked at startup, but no local owner origination path exists—the erasure set can only be populated by an accepted external ledger on restore. Add an authenticated owner command to originate erasure locally, signed ledger entry first, then local transaction. Give the erasure-sweep census row a caller-level test.
Acceptance criteria
Add owner API command: erasure_counterparty(id: ULID) -> Result. 2. Sign the ledger delta and persist. 3. Run transactional erasure sweep. 4. Add caller-level test covering the command path and the transactional recheck. 5. Inventory existing guard tests from Ship recurring Gmail drafts as the first complete delegation proof #130 and carry them forward.
Blocked by
None (correctness gap; no fit-review fence required)
Original report
Summary
#130 extends Store::mark_learned_artifacts_erased with a generic reviewed-scope standing-rule sweep, so a crypto-erased counterparty loses live scoped authority. The guard site is production-wired and unconditionally invoked at startup, and it is reachable whenever a valid, continuity-compatible external terminal ledger carrying erased ids is accepted through restore — which the archived overlay-restore contract explicitly provides for. What is absent is local owner origination: no production caller of record_terminal_erasure exists in any commit, so nothing running on this terminal can begin an erasure.
Filed at the moment the gap is created, rather than left for a caller census to discover two changes later.
The precise state, in three parts
1. The path is live and unconditional.main.rs:303-309 calls validate_startup_and_reconcile_overlay_terminal_erasures, which at main.rs:144 calls counterparty_erasure::reconcile_overlay_terminal_erasures (:89-106) after the boot-clock and audit-chain gates. There is no configuration or feature guard. It loads the signed ledger, iterates erased_counterparty_ids(), rejects SYSTEM_SCOPE (:98, :108-115), closes the in-memory scope (:99-102), then finish_local_erasure (:117-129) calls Store::mark_learned_artifacts_erased at :122.
2. Local origination is absent, and always was.OverlayControl::record_terminal_erasure (overlay_export_restore/control/continuity.rs:125-152) is the sole mutator of the erasure set — the only site performing body.erased_counterparty_ids.insert(...) followed by write_ledger(&signed). Every other reference under control/ is a read, a comparison, a superset check, or a wire-format field. Its only non-test caller is erase_counterparty (counterparty_erasure.rs:71), which carries #[allow(dead_code)] at :59-60; all of its own callers are *_tests.rs. git log -S on both the function and its annotated form returns exactly one commit — 5c51ded feat(kernel): implement counterparty key model (AD-140, resolves QQ-7) (#102) — so the function was born dead and was never wired.
3. External ledgers can carry ids, by contract. Bundle merge collects merged.erased_counterparty_ids() (operation/process.rs:174) and applies a terminal-erasure delta at :175. The incoming baseline is re-signed under the local master key and rejected on mismatch (process.rs:263-278), but that check validates shape and lineage, not provenance: sign_ledger (control/continuity.rs:242-252) HMACs a canonical TerminalLedger of exactly {version, continuity_id, sequence, erased_counterparty_ids} (control/wire.rs:108-113), with no producer identity, build provenance, or origination-path field.
The archived design makes this an input rather than an accident — openspec/changes/archive/2026-07-24-implement-overlay-export-restore/design.md §5: "portable recovery additionally requires the latest separately preserved signed ledger, analogous to the external master key. Restore requires the local/imported ledger and bundle baseline to share the same continuity id, then merges their monotonic state." That section also places "a malicious host operator deliberately supplying both an old bundle and stale valid ledger" outside the trust boundary rather than declaring it impossible.
So the work set is not provably empty. It is unpopulated by local action, and populated by any accepted continuity-compatible ledger.
Reachability condition
Reconciliation always runs. It has work iff a continuity-compatible signed terminal ledger containing at least one parseable, non-SYSTEM_SCOPE ULID counterparty id is present locally or imported through restore. Producing such a ledger requires the master key but not a live local caller — so the missing piece is specifically local owner origination, not production erasure ingress.
Why the guards were still right to land in #130
The mechanism is fail-closed and correct: the SQLite erased_counterparties marker is the single admission predicate, checked at both entry paths and re-read inside the same BEGIN IMMEDIATE transaction that selects/reserves a scoped rule or consumes a fired token, so an erase committing mid-admission is ordered rather than raced.
The alternative was to leave the hole open until an origination surface arrives — so that the first accepted erasure would land on a runtime where erased counterparties keep live scoped authority to act on their own data. Because external ledgers are an accepted input today, that exposure is live, not hypothetical. Building the mechanism first and disclosing that local origination is missing is strictly better than shipping the surface onto an unguarded runtime.
This is deliberate mechanism-first sequencing, disclosed at creation.
Inherited obligation
Whoever lands the origination surface inherits proving guards written earlier by someone else. #130's reachability census is the map. Rows to carry forward:
Generic standing-rule erasure sweep — proven at store level by owner_review_created_scoped_rule_is_revoked_on_counterparty_erasure_store_level; needs a caller-level test once origination exists.
is_counterparty_erased + normal admission check — api/scoped_admission.rs::resolve_scoped_admission from api/actions.rs::post_actions.
Scoped reservation transactional recheck — store/standing_rules.rs::consult_and_reserve_scoped_rule from api/scoped_admission.rs::consult_scoped_rule.
Fired-entry marker check and fired-token transactional recheck — both UNPROVEN / latent in #130 for a different and unrelated reason: token minting is inert while the dark-window Allow allowlist is empty (action_catalog_contracts.rs:54-60) and email.create_draft is DarkWindowPolicy::Prohibited (action_catalog_data.rs:84). An origination surface does not unblock these — they need a non-empty allowlist. Do not assume one fix reaches both.
Canon exposure
AD-140 ratifies crypto-erase — named as canon at openspec/openspine-change-sequence.md:411, scope at :414-416: "per-counterparty payload keys replacing the single global artifact-store key; crypto-erase = key deletion — plaintext unrecoverable while the hash chain keeps tamper evidence; derived overlay artifacts" invalidated. The completion machinery exists, is wired, and runs; what is absent is any way for an owner to originate the act locally.
docs/threat-claims.md carries no erasure claim. Its only counterparty row is CLAIM-31 (:44), about counterparty identity binding being gated on an authenticated owner-principal context and audited atomically — not erasure. No gated claim is currently false, so nothing needs walking back; the exposure is a missing surface, not an overstatement.
Correction owed to #130's census
The census row for the generic erasure sweep records NONE in the production-caller column. That understates the state: the guard is wired and unconditionally invoked at startup. The accurate wording is "wired and unconditionally invoked at startup; reachable via imported continuity ledger; local origination absent." That correction belongs with #130, not here.
Scope
Add an authenticated owner-facing erasure command that originates a terminal erasure locally — signed ledger entry first, then the local transaction — and give the erasure-sweep census row a caller-level test.
Out of scope: plaintext briefcases, SYSTEM_SCOPE/pending protected payloads, owner-review row lifecycle, and cross-store atomicity between the external ledger, SQLite, and filesystem key cleanup. Those are recorded residuals of #130.
What to build
Counterparty crypto-erasure guards are production-wired and invoked at startup, but no local owner origination path exists—the erasure set can only be populated by an accepted external ledger on restore. Add an authenticated owner command to originate erasure locally, signed ledger entry first, then local transaction. Give the erasure-sweep census row a caller-level test.
Acceptance criteria
Blocked by
None (correctness gap; no fit-review fence required)
Original report
Summary
#130extendsStore::mark_learned_artifacts_erasedwith a generic reviewed-scope standing-rule sweep, so a crypto-erased counterparty loses live scoped authority. The guard site is production-wired and unconditionally invoked at startup, and it is reachable whenever a valid, continuity-compatible external terminal ledger carrying erased ids is accepted through restore — which the archived overlay-restore contract explicitly provides for. What is absent is local owner origination: no production caller ofrecord_terminal_erasureexists in any commit, so nothing running on this terminal can begin an erasure.Filed at the moment the gap is created, rather than left for a caller census to discover two changes later.
The precise state, in three parts
1. The path is live and unconditional.
main.rs:303-309callsvalidate_startup_and_reconcile_overlay_terminal_erasures, which atmain.rs:144callscounterparty_erasure::reconcile_overlay_terminal_erasures(:89-106) after the boot-clock and audit-chain gates. There is no configuration or feature guard. It loads the signed ledger, iterateserased_counterparty_ids(), rejectsSYSTEM_SCOPE(:98,:108-115), closes the in-memory scope (:99-102), thenfinish_local_erasure(:117-129) callsStore::mark_learned_artifacts_erasedat:122.2. Local origination is absent, and always was.
OverlayControl::record_terminal_erasure(overlay_export_restore/control/continuity.rs:125-152) is the sole mutator of the erasure set — the only site performingbody.erased_counterparty_ids.insert(...)followed bywrite_ledger(&signed). Every other reference undercontrol/is a read, a comparison, a superset check, or a wire-format field. Its only non-test caller iserase_counterparty(counterparty_erasure.rs:71), which carries#[allow(dead_code)]at:59-60; all of its own callers are*_tests.rs.git log -Son both the function and its annotated form returns exactly one commit —5c51ded feat(kernel): implement counterparty key model (AD-140, resolves QQ-7) (#102)— so the function was born dead and was never wired.3. External ledgers can carry ids, by contract. Bundle merge collects
merged.erased_counterparty_ids()(operation/process.rs:174) and applies a terminal-erasure delta at:175. The incoming baseline is re-signed under the local master key and rejected on mismatch (process.rs:263-278), but that check validates shape and lineage, not provenance:sign_ledger(control/continuity.rs:242-252) HMACs a canonicalTerminalLedgerof exactly{version, continuity_id, sequence, erased_counterparty_ids}(control/wire.rs:108-113), with no producer identity, build provenance, or origination-path field.The archived design makes this an input rather than an accident —
openspec/changes/archive/2026-07-24-implement-overlay-export-restore/design.md§5: "portable recovery additionally requires the latest separately preserved signed ledger, analogous to the external master key. Restore requires the local/imported ledger and bundle baseline to share the same continuity id, then merges their monotonic state." That section also places "a malicious host operator deliberately supplying both an old bundle and stale valid ledger" outside the trust boundary rather than declaring it impossible.So the work set is not provably empty. It is unpopulated by local action, and populated by any accepted continuity-compatible ledger.
Reachability condition
Reconciliation always runs. It has work iff a continuity-compatible signed terminal ledger containing at least one parseable, non-
SYSTEM_SCOPEULID counterparty id is present locally or imported through restore. Producing such a ledger requires the master key but not a live local caller — so the missing piece is specifically local owner origination, not production erasure ingress.Why the guards were still right to land in
#130The mechanism is fail-closed and correct: the SQLite
erased_counterpartiesmarker is the single admission predicate, checked at both entry paths and re-read inside the sameBEGIN IMMEDIATEtransaction that selects/reserves a scoped rule or consumes a fired token, so an erase committing mid-admission is ordered rather than raced.The alternative was to leave the hole open until an origination surface arrives — so that the first accepted erasure would land on a runtime where erased counterparties keep live scoped authority to act on their own data. Because external ledgers are an accepted input today, that exposure is live, not hypothetical. Building the mechanism first and disclosing that local origination is missing is strictly better than shipping the surface onto an unguarded runtime.
This is deliberate mechanism-first sequencing, disclosed at creation.
Inherited obligation
Whoever lands the origination surface inherits proving guards written earlier by someone else.
#130's reachability census is the map. Rows to carry forward:owner_review_created_scoped_rule_is_revoked_on_counterparty_erasure_store_level; needs a caller-level test once origination exists.is_counterparty_erased+ normal admission check —api/scoped_admission.rs::resolve_scoped_admissionfromapi/actions.rs::post_actions.store/standing_rules.rs::consult_and_reserve_scoped_rulefromapi/scoped_admission.rs::consult_scoped_rule.UNPROVEN / latentin#130for a different and unrelated reason: token minting is inert while the dark-window Allow allowlist is empty (action_catalog_contracts.rs:54-60) andemail.create_draftisDarkWindowPolicy::Prohibited(action_catalog_data.rs:84). An origination surface does not unblock these — they need a non-empty allowlist. Do not assume one fix reaches both.Canon exposure
AD-140 ratifies crypto-erase — named as canon at
openspec/openspine-change-sequence.md:411, scope at:414-416: "per-counterparty payload keys replacing the single global artifact-store key; crypto-erase = key deletion — plaintext unrecoverable while the hash chain keeps tamper evidence; derived overlay artifacts" invalidated. The completion machinery exists, is wired, and runs; what is absent is any way for an owner to originate the act locally.docs/threat-claims.mdcarries no erasure claim. Its only counterparty row isCLAIM-31(:44), about counterparty identity binding being gated on an authenticated owner-principal context and audited atomically — not erasure. No gated claim is currently false, so nothing needs walking back; the exposure is a missing surface, not an overstatement.Correction owed to
#130's censusThe census row for the generic erasure sweep records
NONEin the production-caller column. That understates the state: the guard is wired and unconditionally invoked at startup. The accurate wording is "wired and unconditionally invoked at startup; reachable via imported continuity ledger; local origination absent." That correction belongs with#130, not here.Scope
Add an authenticated owner-facing erasure command that originates a terminal erasure locally — signed ledger entry first, then the local transaction — and give the erasure-sweep census row a caller-level test.
Out of scope: plaintext briefcases,
SYSTEM_SCOPE/pending protected payloads, owner-review row lifecycle, and cross-store atomicity between the external ledger, SQLite, and filesystem key cleanup. Those are recorded residuals of#130.Related: #130