Skip to content

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  →  ENC  →  format backend
CowBackend( EncBackend( LimnifsBackend | DwarfsBackend | SquashfsBackend | … ) )

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

TEBAKO_OVERLAYS

<mount>=<store> ; separated, split on the FIRST =

a store keeps its own = and its drive-qualified :

TEBAKO_DECRYPT

<mount>=pgp:<keyid>

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

vfs-write (record mode) · silent success (deny mode)

write anywhere else in a held tree

EROFS — the jail-safe default

event=vfs-deny op=write

read of a sealed path with no opening grant

ENOKEY (126), never garbage

event=vfs-deny op=read class=ekey

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.