Skip to content

ARCHITECTURE · 17

Declarative overlays.

Images are read-only forever, which is the transforms law. When a workload must write, or a slice arrives sealed, the run gains a transform: a copy-on-write layer or an encryption view. Each transform enters exclusively through a written declaration, resolved before exec and journaled at run time. Nothing is transformed that was not declared.

Note

The gated COW composite, the TEBAKO_OVERLAYS / TEBAKO_DECRYPT env grammars, vfs-deny journaling, the needs generator’s write fold, and exit 68 are shipped.

The D1/D2 manifest model and the driver’s boot-time consumption are phased, and they are planned.

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, and ENC wraps exactly one image’s backend and sits under COW. The order is not a taste: EncBackend is a decrypting read view that 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, and 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 is amended once and 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 that 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 states that any per-run scratch satisfies the slice, and retained states that the slice’s writes must survive the run, so an operator-bound durable store is mandatory. The operator may widen durability upward and never demotes a retained need to scratch; declarations request, and the operator tightens. For decrypt, the fact of encryption is owned by the identity block (paths, algorithm, envelope refs, never keys), and 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 serves N audiences, and each run opens exactly its declared subtree. A data slice declares no needs.write ever, and 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, because the composition declares the need and binds the store. There is 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. The trust posture, stated honestly, is that a package signature covers slices and the trailer, not the bindings, and 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, or a decrypt recipient that is not pgp: plus 16 lowercase hex. A store containing ; is unrepresentable by construction, because it splits into a second entry that fails the grammar. The failure is 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, and 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 to any path below it (on a 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; and the whiteout journal keeps its EPERM under every area set. A malformed area fails the mount with EINVAL, fail-closed and never a silent widening.

SURFACE ANSWER JOURNAL

write under a declared write area

It lands in the overlay store.

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

write anywhere else in a held tree

The answer is EROFS, the jail-safe default.

event=vfs-deny op=write

read of a sealed path with no opening grant

The answer is ENOKEY (126), never garbage.

event=vfs-deny op=read class=ekey

store-side IO failure (ENOSPC, EACCES)

The answer is the write syscall’s own errno.

No translation occurs; the run degrades like a full disk.

Record mode discovers writes too

Under policy: record, every image mount stacks an ephemeral scratch COW, so 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 that 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:, with persistence ephemeral (the observed minimum) and each entry carrying its 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, and merges into D1 or D2. The generator never edits a manifest.

Swapping the bindings without rebuilding the slice

Overlay and key bindings live in the composition layer and are 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, and 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, and enc stays reserved, because 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 is 68 EX_TEBAKO_OVERLAY: an unbound retained store, missing or non-opening key material, an unwritable store, an orphan binding, or a malformed env grammar.