feat(contract): let the author declare which volumes survive a bake - #266
Merged
Merged
Conversation
The bake drops a stack's volumes so a buyer's machine initialises them
with its own credentials. Some must survive that: Ollama weights are
gigabytes behind a 600s pull, Qdrant embeddings cost as much to recompute
— preserving them is why the snapshot exists.
Until now the exceptions were a `match` on the stack slug inside
`bake_finalize.rs`. Four stacks were listed, floci and the rest were
queued, and each one needed a code change and a rebuilt binary.
The author knows which of their volumes are expensive and which hold
credentials; the platform does not. So it goes in `config_contract`,
beside the fields, reusing everything already built for them: the
`mutability` vocabulary, and the path from stacker.yml through the
submit body to `stack_template_version.config_contract` and on to
`bake.rs`, which resolves it before finalize runs.
config_contract:
services:
stackpilot-ollama:
volumes:
stackpilot_ollama: { mutability: fixed }
`fixed` ships the content inside the image; `generated` — the default for
anything undeclared — drops it. Erring that way costs a rebuild; the
opposite default would hand the author's credentials to every buyer.
`provided` and `editable` describe who types a value and are rejected:
a volume holds state, not a value.
The platform validates only the name, which is interpolated into a shell
pattern. It deliberately does not second-guess the declaration.
An earlier revision of this change did. It refused any volume whose
service declares `generated` or `provided` fields, reasoning that such a
service persists the secret. Measuring real containers killed that rule:
a Postgres data directory holds `SCRAM-SHA-256$4096:…` and not the
password in any searchable form; n8n keeps its own encryption key inside
`database.sqlite`; a Qdrant volume holds only collections, because Qdrant
reads its API key from the environment at every start. The secret is
absent from all three — so neither the field-based rule nor a search of
the volume's bytes tells the two that must reset from the one that must
be kept. The difference is behavioural, and only the author can see it.
Left in, the rule would have forced ai-knowledge-base to recompute its
embeddings on every buyer's machine: the exact expense a snapshot avoids.
FinalizeContext now carries the parsed contract rather than a flattened
key set, since the kind-per-service structure is what the volume policy
needs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
The bake drops a stack's volumes so a buyer's machine initialises them with its own credentials. Some must survive that: Ollama weights are gigabytes behind a 600s pull, Qdrant embeddings cost as much to recompute — preserving them is why the snapshot exists.
Until now the exceptions were a
matchon the stack slug insidebake_finalize.rs. Four stacks were listed, floci and the rest were queued, and each one needed a code change and a rebuilt binary.The author knows which of their volumes are expensive and which hold credentials; the platform does not. So it goes in
config_contract, beside the fields, reusing everything already built for them: themutabilityvocabulary, and the path from stacker.yml through the submit body tostack_template_version.config_contractand on tobake.rs, which resolves it before finalize runs.fixedships the content inside the image;generated— the default for anything undeclared — drops it. Erring that way costs a rebuild; the opposite default would hand the author's credentials to every buyer.providedandeditabledescribe who types a value and are rejected: a volume holds state, not a value.The platform validates only the name, which is interpolated into a shell pattern. It deliberately does not second-guess the declaration.
An earlier revision of this change did. It refused any volume whose service declares
generatedorprovidedfields, reasoning that such a service persists the secret. Measuring real containers killed that rule: a Postgres data directory holdsSCRAM-SHA-256$4096:…and not the password in any searchable form; n8n keeps its own encryption key insidedatabase.sqlite; a Qdrant volume holds only collections, because Qdrant reads its API key from the environment at every start. The secret is absent from all three — so neither the field-based rule nor a search of the volume's bytes tells the two that must reset from the one that must be kept. The difference is behavioural, and only the author can see it.Left in, the rule would have forced ai-knowledge-base to recompute its embeddings on every buyer's machine: the exact expense a snapshot avoids.
FinalizeContext now carries the parsed contract rather than a flattened key set, since the kind-per-service structure is what the volume policy needs.