COW → ENC → format backend
CowBackend( EncBackend( LimnifsBackend | DwarfsBackend | SquashfsBackend | … ) )
ARCHITECTURE · 20
Declarative overlays.
Images are read-only forever — that is the transforms law. When a workload must write, or a slice arrives sealed, the run gains a transform: a copy-on-write layer, an encryption view. Each enters exclusively through a written declaration, resolved before exec, journaled at run time. Nothing is transformed that was not declared.
|
Note
|
Status: shipped — the gated COW composite, the TEBAKO_OVERLAYS / TEBAKO_DECRYPT env grammars, vfs-deny journaling, the needs generator’s write fold, exit 68. Status: planned — the D1/D2 manifest model and the driver’s boot-time consumption are phased. |
One pinned stack, top to bottom.
Per mount point, from the syscall side down to the bytes:
COW is always outermost; ENC wraps exactly one image’s backend and sits under
COW. The order is not a taste: EncBackend is a decrypting read view — it
implements no write path — so only COW(Enc(base)) can accept a write, and
copy-up then reads through the ENC layer: modifying a sealed base file copies
its plaintext into the operator-bound overlay store. That store is outside the
image’s confidentiality envelope, stated honestly — scratch at rest is the
host’s disk-encryption layer, never a tebako declaration. Whiteouts name
plaintext view paths, consistent with the ENC format keeping names and
structure plaintext and only regular-file content ciphertext. A union mount
composes the per-image stacks beneath COW — sealed union members wrap
individually and merge as plaintext views.
D1 — a slice declares its writes and its seals.
Two blocks extend the ONE needs: key of the slice manifest — the composition
grammar, amended once, owned in one place.
needs:
write: # writable areas — COW is the only mechanism
- path: /app/var/cache # absolute in-image path, inside THIS slice's tree
persistence: ephemeral # ephemeral (DEFAULT) | retained
when: [macos] # OPTIONAL platform filter
why: "mnconvert writes its font cache at boot" # MANDATORY
decrypt: # key-binding requirements — ENC
- part: /fonts/licensed # a path from identity.encryption.parts[].paths
why: "the converter reads the licensed font tree" # MANDATORY
A write area is a PATH the slice will write, not a mechanism — COW is the only
mechanism, so resolution always satisfies a write need with a CowBackend stack.
persistence is the slice’s contract about its own data: ephemeral says any
per-run scratch satisfies me; retained says my writes must survive the run,
so an operator-bound durable store is mandatory. The operator may widen
durability upward, never demote a retained need to scratch — declarations
request, the operator tightens. For decrypt: the FACT of encryption is owned by
the identity block (paths, algorithm, envelope refs — never keys); the need
only names WHICH sealed parts the workload requires opened. Sealed state with
no needs.decrypt defaults to ALL parts required — fail-closed. With entries,
unlisted parts may stay sealed: one artifact, N audiences, each run opening
exactly its declared subtree. A data slice declares no needs.write ever; a
sealed data slice is the one exception that may carry needs.decrypt.
D2 — the operator binds the store and the recipient.
The composition document gains two top-level keys; D5 maps 1:1 as repeatable
--overlay / --decrypt flags, and flags win.
# tebako.yaml (D2)
overlays: # COW backing-store bindings
- slice: metanorma
store: ./overlays/metanorma # host dir; symbolic atoms expand
decrypt: # ENC key bindings — REFERENCES, never material
- slice: fonts-secure
recipient: pgp:3c8dba971d2b4f01 # a key in $TEBAKO_HOME/keys/
Bindings satisfy needs; they never create capability. An entry naming a slice
with no matching declared need is a named ORPHAN-BINDING error at resolution,
never a silent grant — giving an undeclared slice a write area takes both
lines: the composition declares the need AND binds the store. One store per
mount; the directory is created when missing and gains a derived rw identity
grant in the effective policy, so declared write access to host surface stays
visible by construction. recipient: names a key reference — pgp: plus 16
lowercase hex — resolved against $TEBAKO_HOME/keys/, the only secret-key
home. Key material in any authored document is a named validation error, not a
grammar. Trust posture, stated honestly: a package signature covers slices and
trailer, NOT the bindings; an overlay store’s content is host state, outside
every image’s trust anchor.
Resolution — before exec, never mid-run.
Between the composition algorithm’s needs-union and needs-check steps, the resolver gains three ordered steps:
3a. UNION every slice's needs.write / needs.decrypt, platform-filtered,
conflict rules per declaration — the runtime's release manifest
unions identically (the env image is a mount like any other)
3b. BIND match D2 bindings to needs
write + binding → the store is checked NOW (creatable, writable)
write + NO binding → ephemeral: per-run scratch under
$TEBAKO_HOME/tmp/overlays/<run-id>/, journaled
retained: named RESOLUTION FAILURE
decrypt + binding → the key must EXIST and open an envelope
covering the part — verified at bind, never
at first read
orphan binding → named error
3c. CHECK a declared need the composition does not satisfy fails BEFORE EXEC
— a need never surfaces as a mid-run EROFS/ENOKEY
5′. EXPORT the bound set serializes to TEBAKO_OVERLAYS / TEBAKO_DECRYPT;
spawned children inherit and re-bind identically
| VAR | GRAMMAR | NOTE |
|---|---|---|
|
|
a store keeps its own = and its drive-qualified : |
|
|
key REFERENCES cross the wire — key material never does |
Malformed env forms fail closed with exit 68, each a named error quoting the
offending entry: an empty spec, a stray ;, a missing =, a non-absolute
mount or store, a duplicate mount, a decrypt recipient that is not pgp: + 16
lowercase hex. A store containing ; is unrepresentable by construction — it
splits into a second entry that fails the grammar. Fail-closed, never a silent
misparse.
Run time — the write gate and the sealed read.
A mount carrying at least one write area stacks CowBackend with the declared
area set; the jail installs AFTER the mounts with the derived store grants in
force. The gate’s predicate is locked: areas are absolute in-image paths
normalized at mount time; a write to an area itself or any path BELOW it —
component boundary, /a/b never covers /a/bc — is permitted; all four write
verbs (pwrite, truncate, mkdir, remove) are gated identically; reads
are never gated; the whiteout journal keeps its EPERM under every area set. A
malformed area fails the mount with EINVAL — fail-closed, never a silent
widening.
| SURFACE | ANSWER | JOURNAL |
|---|---|---|
write under a declared write area |
lands in the overlay store |
|
write anywhere else in a held tree |
EROFS — the jail-safe default |
|
read of a sealed path with no opening grant |
ENOKEY (126), never garbage |
|
store-side IO failure (ENOSPC, EACCES) |
the write syscall’s own errno |
no translation — the run degrades like a full disk |
Record mode discovers writes too.
Under policy: record every image mount stacks an ephemeral scratch COW — the
"nothing is denied" promise now covers writes into held trees: the write lands
in scratch, journaled event=vfs-write, and is discarded at exit. The payload
observed a writable world it never owned. Deny-mode vfs-deny lines and
record-mode vfs-write lines both fold into tfs needs --from-journal, which
emits a draft needs.write: block beside needs.host: — persistence
ephemeral (the observed minimum), each with the why: TODO. Sealed-read
denials fold into a draft needs.decrypt: entry. The human gate is unchanged:
the author flips persistence, prunes noise, fills why, merges into D1 or D2.
The generator never edits a manifest.
Swap the bindings, never rebuild the slice.
Overlay and key bindings live in the composition layer — swappable by
construction. Managed mode edits tebako.yaml or passes flags. A standalone
package’s baked composition carries the resolved needs union and the
press-time bindings as the DEFAULT; the override document (argv --compose >
TEBAKO_COMPOSE > sidecar) carries overlays: / decrypt: with the same
grammar. The reserved L2 mounts[].mode: cow spelling becomes live for
exactly this; enc stays reserved — a sealed slice’s fact is its own
manifest’s, never the L2 row’s. The boot journal names what ran:
event=overlay mount=… store=… source=declared|ephemeral and
event=decrypt mount=… recipient=pgp:… grants=…. Key material never touches a
journal. The one added exit-code row: 68 EX_TEBAKO_OVERLAY — unbound
retained store, missing or non-opening key material, unwritable store, orphan
binding, malformed env grammar.