R-241 part 3: abandoning starts a 14-day countdown that ends the question
Until now "set aside" renamed the remote store and touched neither the escrow
nor the key, so the hub went on holding a sealed package for a key the box no
longer used. Shape (c) compares those two, finds them different, and offers
recovery - correctly, and for ever. A customer who had already said "I do not
want the old data" would be asked again at every login.
The operator's ruling is that the answer is NOT a "they decided" flag: fix the
state, do not remember that it is wrong. So the decision starts a countdown,
at the end of which the set-aside store and the sealed package that protects
it are removed TOGETHER. Afterwards shape (c) has nothing to compare and the
offer falls silent on its own - because the state is right, not because
something remembers it once was not.
THE GRACE IS REAL. The recovery offer stays reachable for the whole 14 days;
that is the change-of-mind path, and a grace in which recovery is impossible
would be decorative.
BOTH HALVES OR NEITHER. Removing only the store leaves a package that opens
nothing; removing only the package leaves ciphertext nobody can ever decrypt.
The two cannot be atomic across two machines, so it is a two-phase commit:
delete the store, record a durable marker, and keep DECLARING
offsite.abandon_purge_requested until the hub's ACK stops reporting a
superseded package. A crash between the halves re-declares on the next sweep;
it never leaves the pair half-removed and silent.
HUB HALF - SEC 8.2 ANSWERED: yes, the hub was needed, and only for this.
store.PurgeSupersededEscrowForCustomer is the one place R-198's retention is
ever undone, and it never touches host_escrow (the package covering the key
the box uses now). The handler acts on the DECLARATION, never an inference,
and is placed immediately BEFORE the ACK is built - so
GetEscrowStatusForCustomer reads the effect and the SAME response closes the
box's two-phase commit. No second round-trip and no window where the box
thinks it is still owed. felhom-agent was NOT touched.
The countdown starts in ResetOrphanedRepo, NOT in the shared helper: the
helper is also the unclaimed auto-reset path, where nobody decided anything,
and an as-delivered box tidying a stranger's leftover store must not get a
customer's deletion clock. Pinned by a test.
Cancellation is wired into the recovery unlock, BEFORE the tier-up and the
listing - those can fail, and a countdown surviving a successful unlock
because a later step errored would delete the history the customer just
proved they can open.
The sweep is a Daily job at 05:10, not on the backup leg: it must run on a box
whose tier is not configured for runs. Quiet by construction on every box with
no countdown, and that silence is asserted.
Tests (all clock-injected; SEC 7.4 forbids shortening a live timer):
Scenario E (aside + package kept + countdown + offer still reachable, and
NOTHING deleted), Scenario F (both halves, the declaration repeating, the
close-out), Scenario G (cancel, path still nameable, no later deletion),
plus: not closed out while the package remains, a transport failure leaves the
countdown due and retrying, the no-op sweep issues zero remote commands, and
the unclaimed auto-reset starts no countdown.
RED-PROOFS, each with the mutation confirmed present in the file first:
F1) store deletion skipped -> Scenario F FAILS (no rm issued)
F2) declaration dropped from the report -> Scenario F FAILS (the hub is
never asked; the package would outlive the store for ever)
G) CancelAbandon made a no-op -> Scenario G FAILS (uncancellable countdown)
Green: controller and hub both build, vet and test clean; controller gates OK.
NOTHING WAS DELETED ANYWHERE - the terminal step has only ever run against
in-test fakes.
This commit is contained in:
@@ -318,6 +318,34 @@ type OffboxTarget struct {
|
||||
// NOT promise the history can be reopened — that is the R-202 lesson, and a conditional promise
|
||||
// that turns out false is worse on this surface than saying less.
|
||||
OrphanedRenamedTo string `json:"orphaned_renamed_to,omitempty"`
|
||||
|
||||
// ── ABANDONMENT (v0.206.0, R-241) — deciding to give up the old history is a FINISHABLE thing ──
|
||||
//
|
||||
// Until now "set aside" renamed the remote store and touched nothing else, so the hub went on
|
||||
// holding a package for a key the box no longer used — and the recovery question came back at
|
||||
// every login, for ever. The operator's ruling (2026-08-07) is that the answer is NOT to remember
|
||||
// that the customer decided, but to reach a state where the question stops arising: **fix the
|
||||
// state, do not remember that it is wrong.** These fields are that countdown.
|
||||
//
|
||||
// ⚠ THEY ARE NOT A "THEY DECIDED" FLAG, and the distinction is the ruling. Nothing here suppresses
|
||||
// the recovery offer — the offer stays reachable for the whole 14 days, because a grace period in
|
||||
// which nothing can be done is decorative (Scenario E). What ends the offer is the TERMINAL STEP
|
||||
// removing the store and the sealed package together, after which shape (c) has nothing to
|
||||
// compare and falls silent on its own.
|
||||
//
|
||||
// AbandonStartedAt / AbandonAt are RFC3339; AbandonRepoPath is the move-aside path the terminal
|
||||
// step deletes. AbandonRepoPath is separate from OrphanedRenamedTo deliberately: that field is
|
||||
// overwritten by the NEXT reset, and a countdown that loses the path it is counting down to would
|
||||
// delete nothing and report success.
|
||||
AbandonStartedAt string `json:"abandon_started_at,omitempty"`
|
||||
AbandonAt string `json:"abandon_at,omitempty"`
|
||||
AbandonRepoPath string `json:"abandon_repo_path,omitempty"`
|
||||
// AbandonPurgeRequested — the remote store IS deleted and the hub has not yet confirmed the sealed
|
||||
// package is gone. It is a two-phase-commit marker for an operation in flight, NOT a memory of a
|
||||
// decision: it is set by the terminal step, declared in the report, and cleared the moment the
|
||||
// hub's ACK stops reporting a superseded package. If the two halves could not be removed together
|
||||
// this marker is what makes the box keep asking until they are (Scenario F).
|
||||
AbandonPurgeRequested bool `json:"abandon_purge_requested,omitempty"`
|
||||
}
|
||||
|
||||
// CrossDriveBackup configures per-app backup to a secondary drive.
|
||||
|
||||
Reference in New Issue
Block a user