fix: durable volumes survive a redeploy - #10
Merged
Conversation
--- PULL REQUEST DESCRIPTION --- # Durable volumes survive a redeploy Fixes a P0 that already destroyed real data: a newsroom's entire message history — reporters' story pitches, the editor's assignment — was lost to a routine `docker rm` + recreate. Workspace `kind: volume` resources and durable Moltnet sqlite stores got a brand-new empty Docker volume on every redeploy, through every launch path. ## The three original holes 1. **Run-scoped names.** `createPersistentVolumeName` folded a freshly minted `NOOPOLIS_RUN_ID` into every durable volume name, and silently DISCARDED an author's explicit `name` whenever a run id was present. 2. **Bootstrap could not seed.** `runProject` mounted with `volume-nocopy`, the exact flag that suppresses the copy-up delivering the image's `.spawnfile-volume-init` preimage that the Daimon ownership guard requires. 3. **Hand-launched containers got nothing, silently.** Nothing failed closed when a durable path had no backing volume. ## What changed - Workspace volumes, durable Moltnet stores and open-mode token directories are `lifecycle: "exclusive-reattach"`, named from the plan root plus the deployment lineage exactly as durable memory banks already were. An author-declared name is used verbatim. `createPersistentVolumeName` now takes no name at all and serves only genuinely run-scoped mounts. - `volume-nocopy` dropped for compiler-owned persistent mounts. Copy-up only populates an EMPTY volume, so reattach is unaffected. `src/target/*` keeps its own. - `require_backed_mount` in the entrypoint and the Daimon root wrapper refuses to start when a durable path is not a real mount point, naming the mount id, path and volume. `SPAWNFILE_ALLOW_EPHEMERAL_STATE=1` opts out. Comparison uses the kernel's octal-escaped mount-point form. - Declared names are unique-checked across EVERY mount source, and two resources whose declared names collapse onto one backing path are rejected. - Only an author-declared name is published in the distribution report (`declared_volume_name`) and honoured verbatim by a sourceless image deployment; a derived name never travels, since it encodes the creator's plan root. - `spawnfile dev up` gets its own lineage namespace, so a dev deployment can never resolve to a production deployment's derived volumes, and refuses to start on an author-declared name unless `--allow-declared-volumes` is passed. - `spawnfile down --volumes` never removes an author-declared volume; it reports them in the receipt's new optional `skipped_volumes`. ## Known tradeoff An organization declaring any of these resources loses the concurrent blue/green canary path and must stop-and-reattach. This was already true for anything declaring memory banks. Stated in `specs/SPEC.md` and `specs/CONTAINERS.md`. ## Follow-ups, deliberately not in this branch - **P2-2** `daimon-organization-acceptance-store` and `daimon-tool-state-*` remain run-scoped. After a redeploy the bridge holds receipts the runtime no longer knows about. Rated wider than originally scoped; worth its own change. - **P2-D** The declared-name refusal gates only the `dev` verb. `spawnfile run` and `up --deployment scratch` attach declared volumes with no gate. New in this branch (pre-branch, `run` run-scoped declared names so it could never reach production's volume). Documented in CONTAINERS.md, so not a defect, but the design is inconsistent and the rationale for refusing `dev up` applies identically. - **P3-E** The dev refusal lands after the full image build. The compile report exists before the Docker build; moving the check between them is the fix. - **P3-F** Compile-side declared names are unvalidated. `name: "clank-store,readonly"` emits `--mount source=clank-store,readonly,...` and Docker's CSV parser takes `readonly` as an option. The manifest schemas should mirror the consumer regex already enforced in `distributionReportSchema.ts`. - **P3-H** No migration note for operators who pinned `NOOPOLIS_RUN_ID`. Their old derived volumes are orphaned, not deleted, after upgrade. - `containerArtifacts.ts` is 424 lines, under the 454 it was but still over the repo's 400 ceiling, as are several pre-existing files.
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.
Durable volumes survive a redeploy
Fixes a P0 that already destroyed real data: a newsroom's entire message
history — reporters' story pitches, the editor's assignment — was lost to a
routine
docker rm+ recreate. Workspacekind: volumeresources and durableMoltnet sqlite stores got a brand-new empty Docker volume on every redeploy,
through every launch path.
The three original holes
createPersistentVolumeNamefolded a freshly mintedNOOPOLIS_RUN_IDinto every durable volume name, and silently DISCARDED anauthor's explicit
namewhenever a run id was present.runProjectmounted withvolume-nocopy, theexact flag that suppresses the copy-up delivering the image's
.spawnfile-volume-initpreimage that the Daimon ownership guard requires.when a durable path had no backing volume.
What changed
lifecycle: "exclusive-reattach", named from the plan root plus thedeployment lineage exactly as durable memory banks already were. An
author-declared name is used verbatim.
createPersistentVolumeNamenow takesno name at all and serves only genuinely run-scoped mounts.
volume-nocopydropped for compiler-owned persistent mounts. Copy-up onlypopulates an EMPTY volume, so reattach is unaffected.
src/target/*keeps itsown.
require_backed_mountin the entrypoint and the Daimon root wrapper refusesto start when a durable path is not a real mount point, naming the mount id,
path and volume.
SPAWNFILE_ALLOW_EPHEMERAL_STATE=1opts out. Comparison usesthe kernel's octal-escaped mount-point form.
resources whose declared names collapse onto one backing path are rejected.
(
declared_volume_name) and honoured verbatim by a sourceless imagedeployment; a derived name never travels, since it encodes the creator's plan
root.
spawnfile dev upgets its own lineage namespace, so a dev deployment cannever resolve to a production deployment's derived volumes, and refuses to
start on an author-declared name unless
--allow-declared-volumesis passed.spawnfile down --volumesnever removes an author-declared volume; it reportsthem in the receipt's new optional
skipped_volumes.Known tradeoff
An organization declaring any of these resources loses the concurrent blue/green
canary path and must stop-and-reattach. This was already true for anything
declaring memory banks. Stated in
specs/SPEC.mdandspecs/CONTAINERS.md.Follow-ups, deliberately not in this branch
daimon-organization-acceptance-storeanddaimon-tool-state-*remain run-scoped. After a redeploy the bridge holds receipts the runtime no
longer knows about. Rated wider than originally scoped; worth its own change.
devverb.spawnfile runand
up --deployment scratchattach declared volumes with no gate. New inthis branch (pre-branch,
runrun-scoped declared names so it could neverreach production's volume). Documented in CONTAINERS.md, so not a defect, but
the design is inconsistent and the rationale for refusing
dev upappliesidentically.
exists before the Docker build; moving the check between them is the fix.
name: "clank-store,readonly"emits
--mount source=clank-store,readonly,...and Docker's CSV parser takesreadonlyas an option. The manifest schemas should mirror the consumer regexalready enforced in
distributionReportSchema.ts.NOOPOLIS_RUN_ID. Theirold derived volumes are orphaned, not deleted, after upgrade.
containerArtifacts.tsis 424 lines, under the 454 it was but still over therepo's 400 ceiling, as are several pre-existing files.
Review
Two independent adversarial review rounds. Round one blocked on a cross-kind
declared-name collision that silently merged two states, a sourceless image
deployment ignoring declared names while the new spec text promised otherwise,
and a mount guard that false-positived on paths containing spaces. All three
fixed and re-verified. Round two found no surviving data-loss path and cleared
the branch to merge, with two call sites left unlocked; those are now
mutation-locked, along with
down --volumes.Every guarantee here is mutation-checked: reverting the naming fix, restoring
volume-nocopy, neutering the mount guard, collapsing the dev lineage,bypassing the mount merge, or treating declared names as deployment-owned each
turns a test red. A test asserting
prepare_volume_resourcewas merely calledpasses throughout the original bug; only asserting that data survived a
docker rmcatches it.Cold rebuild: 3394 passed, 0 failed, 948 suites. Boundaries clean.