ADR 0047 — the bundle may carry actions the link may not

The bootstrap's sharpest open question, and the framing was wrong. "State on
this machine" was being read as the filesystem and the service manager. A
service running on this machine IS part of this machine — writing a file and
creating a database in a local store differ in mechanism, not in scope.

The real question was underneath: must the host learn what a database is? It
must not. Giving it a `database` resource type means tier 0 knows Postgres, then
a bucket, then a virtual host — the host acquiring the substrate's vocabulary
one service at a time, which is what ADR 0037 exists to stop.

So the bundle declares an ACTION and the host runs it and verifies it. What a
database means stays with the module that provides one; the host knows only how
to run a declared action against something local and check the result. Its
vocabulary grows by one shape rather than by one resource type per service.

Actions are permitted in the bundle and forbidden over the link, and the
asymmetry is deliberate. A bundle arrives WITH the binary: anyone able to put a
hostile action in it could equally have put it in the host itself, so refusing
actions there buys nothing and costs the bootstrap. The link is a separate
party, reachable separately, and an action there is the unbounded blast radius
ADR 0039 refuses. That decision stands unchanged.

And ongoing provisioning is not the host's at all — the control plane does it
once a mesh exists — so the asymmetry costs nothing.

Which dissolves the earlier worry about one mechanism with a tier boundary
inside it: there are two mechanisms, with different actors, scopes and trust
models, and that is the answer rather than a compromise.

Named rather than hidden: this is the escape hatch research 011 warned about,
arbitrary code in the place hardest to remove later. It is bounded by being
bundle-only and by every action having to declare how it verifies itself, and
that boundary is the whole defence.
This commit is contained in:
2026-08-26 23:56:39 +02:00
parent 5b3d0ebd4f
commit 93470f6162
3 changed files with 116 additions and 9 deletions
@@ -0,0 +1,99 @@
---
status: accepted
date: 2026-08-26
deciders: jochen
reconstructed: false
extends: 0043-a-declaration-is-an-ordered-list-of-owned-resources.md
---
# 47. The bundle may carry actions the link may not
## Context
[The substrate](../03-DESIGN/01-to-be/07-the-substrate.md) is raised in five steps, and steps two
and three happen before a mesh exists:
```
1 the host applies the bundle the store runs; no mesh exists
2 a database is created in it before there is anything to ask
3 the control plane's schema applied a migration against that database
4 the control plane starts
```
That was recorded as *the sharpest unresolved thing in the bootstrap path*, on the grounds that
creating a database inside a running store is not state on a machine.
**That framing was wrong, and it was reading
[ADR 0043](0043-a-declaration-is-an-ordered-list-of-owned-resources.md) too narrowly.** *State
on this machine* does not mean the filesystem and the service manager. A service running on this
machine is part of this machine. Writing a file and creating a database in a local store differ
in mechanism, not in scope.
The real question was underneath: **must the host learn what a database is?**
## Considered options
1. **Give the host a `database` resource type.** Then tier 0 knows Postgres — and next a bucket,
a virtual host, a repository. The host acquires the substrate's vocabulary one service at a
time, which is what [ADR 0037](0037-the-host-applies-it-does-not-decide.md) exists to stop.
Rejected.
2. **Have the control plane do all provisioning, including the bootstrap.** Rejected because it
is not there yet: at step 2 the thing that would do the provisioning does not exist.
3. **The bundle declares an action; the host runs it and reads back.** Chosen.
## Decision
**The host runs actions the bundle declares, and never learns what they mean.**
A bundle step says *run this, against this, and here is how to tell whether it worked*. The host
executes it and verifies. What a database is stays knowledge of the module that provides one;
the host knows only how to run a declared action against something local and check the result.
**Actions are permitted in the bundle and forbidden over the link.**
[ADR 0039](0039-the-link-is-the-security-boundary.md) says what arrives over the link is
*declarations of known shape, never a command to run*. That stands, unchanged. The bundle is a
different trust path and the asymmetry is deliberate:
| | the bundle | the link |
|---|---|---|
| where it comes from | the installer somebody built and copied onto the machine | a remote party |
| when it is fixed | at build time, pinned and reviewable | at any moment |
| what compromise means | whoever built the installer, who also built the binary | a control plane, which may be compromised separately |
| may contain an action | **yes** | **no** |
The reasoning is that a bundle arrives *with* the binary. Anyone able to put a hostile action in
it could equally have put it in the host itself, so refusing actions there buys nothing while
costing the bootstrap. The link has no such property: it is a separate party, reachable
separately, and an action there is the unbounded blast radius ADR 0039 refuses.
**And ongoing provisioning is not the host's at all.** Once a mesh exists, a consumer on one
node granted a database on another is provisioned by the control plane. The host never performs
a provisioning action from the link, so the asymmetry costs it nothing.
## Consequences
- **There are two provisioning paths, and that is the answer rather than a problem.** The host
runs bootstrap actions locally from the bundle; the control plane provisions across the mesh
afterwards. The earlier worry — *one mechanism with a tier boundary inside it* — dissolves,
because they are two mechanisms with different actors, scopes and trust models.
- **The host's vocabulary grows by one shape, not one per service.** `action` joins `directory`,
`file` and `service`. Adding a substrate service does not add a host resource type.
- **An action must say how to verify itself.** A step that runs and reports success without a
read-back is the fault this project is about, and an action is the easiest place to reintroduce
it — so the verification is part of the declaration rather than left to the applier.
- **This is the escape hatch [research 011](../01-RESEARCH/011-the-module-graph/features.md)
warned about.** Arbitrary code, in the one place it is hardest to remove later. It is bounded
by being bundle-only and by requiring its own verification, and that boundary is the whole
defence — it should be watched rather than trusted.
- **The bundle becomes something a person must be able to read.** If it can run commands, the
reason to keep it small and pinned stops being convenience and becomes review.
## References
- [ADR 0043](0043-a-declaration-is-an-ordered-list-of-owned-resources.md) — what a declaration is.
- [ADR 0039](0039-the-link-is-the-security-boundary.md) — bounded by form, over the link.
- [ADR 0037](0037-the-host-applies-it-does-not-decide.md) — why the host must not learn a
service's vocabulary.
- [`07-the-substrate.md`](../03-DESIGN/01-to-be/07-the-substrate.md) — the bootstrap order this
unblocks.