Skip to content

Counterparty erasure has no local origination surface: sweep guard is wired but only externally reachable #172

Description

@George-RD

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

  1. 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 checkapi/scoped_admission.rs::resolve_scoped_admission from api/actions.rs::post_actions.
  • Scoped reservation transactional recheckstore/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.

Related: #130

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an AFK agent

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions