fix: allow safe Boat destroy after suspension - #207
Merged
Merged
Conversation
Destroy on a suspended remote placement ran sleep-reconcile and children through fm-on.sh, which needs a reachable host; a stopped sandbox returns SSH 255, so sleep then destroy required a wake first. Sleep now records sleep_quiesced=1 when its remote checks passed, and wake clears it, so a failed-wake compensation that ends suspended carries no proof. Destroy skips the remote checks only for a suspended or provisioned record with that proof, refuses an unproven dormant record before any remote call, and fm-boat.py re-checks the proof under its lock. Running placements keep the remote checks; children, unlanded work, decisions and --yes guards are unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Fix Boat F3: bin/fm-boat.sh destroy on a suspended remote placement fails with SSH 255 before it reaches the provider operation. The wrapper's remote checks (sleep-reconcile and children via fm-on.sh) require a reachable host, and a stopped sandbox is unreachable. The fail-closed outcome is correct but the UX is wrong: sleep then destroy requires a wake first. In the destroy branch, when the recorded lifecycle is suspended or provisioned and the sleep-time checks already proved the route idle, skip the remote reachability checks. Keep them for a running placement, and keep every other destroy safety check (live children, unlanded work, unresolved decisions, the --yes confirmation). Evidence: boat-lifecycle-proof report section 3, F3 row, recommendation 3. Tests must use a fake or stub provider and SSH; never create, wake, or destroy a real Boat sandbox.
Binding firstmate tightening (supersedes the plain lifecycle check): lifecycle=suspended alone does NOT prove the sleep-time quiescence/work checks ran - fm-boat.py compensate() also sets suspended after a FAILED wake of an ever-ready sandbox, without those checks, and sleep(..., destroy=True) then deletes directly. So bypass the remote SSH checks in destroy ONLY when there is durable proof that a successful sleep-time quiescence/work check completed for the current generation (record such proof at sleep, since none existed), and refuse otherwise with a clear message. Implementation choice: sleep records sleep_quiesced=1 in the Boat record only when the wrapper's remote checks ran and passed (FM_BOAT_SLEEP_QUIESCED=1); wake clears it before transitioning, so failed-wake compensation leaves suspended without proof; the wrapper refuses an unproven dormant destroy before any remote call ("wake it so destroy can run remote checks"); fm-boat.py re-verifies the proof under its lock (FM_BOAT_DESTROY_DORMANT=1) and refuses if the placement changed.
Acceptance criteria:
Firstmate-Validation-Generation: 6c7d0ce5a7622acefad52f680469db1f
What Changed
Risk Assessment
✅ Low: The change is bounded, preserves existing safety checks, and revalidates dormant deletion proof under the lifecycle lock.
Testing
Changed routing tests and focused lifecycle tests passed. Isolated real-CLI checks with fake provider/SSH confirmed dormant deletion and safety refusals, with product transcripts captured; parent replay reproduced SSH 255. No real sandbox was contacted. Lint and broad CI were left to their assigned phases; disposable files were removed.
Evidence: CLI behavior and provider-state transcript
Evidence: Parent commit reproduces SSH 255
Evidence: Provider-side proof revalidation
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ **Review** - passed
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
TMPDIR="$PWD/.validation-tmp" bash tests/fm-boat-routing.test.shTMPDIR="$PWD/.validation-tmp" uv run --no-project tests/boat-lifecycle-cases.py "$PWD" LifecycleTests.test_dormant_destroy_requires_sleep_time_quiescence_proof LifecycleTests.test_wake_invalidates_sleep_time_quiescence_proof LifecycleTests.test_sleep_guards_refuse_unresolved_reply_and_decision -vTMPDIR="$PWD/.validation-tmp" bash .validation-tmp/driver.sh— real CLI scenarios with isolated records and fake provider/SSHTMPDIR="$PWD/.validation-tmp" bash .validation-tmp/lock-driver.sh— executable provider-side guard checksTMPDIR="$PWD/.validation-tmp" CODE_ROOT="$PWD/.validation-tmp/parent" BASELINE=1 bash .validation-tmp/driver.sh— parent-commit regression replayRemoved disposable fixtures, parent copy, drivers, and generated Python cache; verified clean git status.✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.