Skip to content

feat(contract): let the author declare which volumes survive a bake - #266

Merged
vsilent merged 1 commit into
devfrom
feat/contract-volume-policy
Sep 23, 2026
Merged

vsilent merged 1 commit into
devfrom
feat/contract-volume-policy

Conversation

@vsilent

@vsilent vsilent commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

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.

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>
@vsilent
vsilent merged commit e34b2dc into dev Sep 23, 2026
2 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants