needs:
host:
- path: /opt/vendor-tool # absolute, or a symbolic atom
access: ro # ro | rw
mount: /vendor/tool # OPTIONAL: present it at this VFS point
when: [macos] # OPTIONAL platform filter
optional: true # absent at bind = skipped (probes only)
why: "the tool probes its install root at boot" # MANDATORY
ARCHITECTURE · 19
Declarative composition.
A tebako system fully resolves the needs of its payloads and its running configuration: what runs with what, where it mounts, which host paths it may touch — all declared, all resolvable, jail-safe by default. The docker-compose analogue, with named errors instead of surprises.
|
Note
|
Status: shipped — needs blocks, the deny default, the resolution algorithm, image-layer compose. Status: planned — the record-mode generator and the bootstrap override channels are phased. |
Five documents, one owner each.
Every fact lives in exactly one row. A slice’s needs are never re-authored into the composition — they resolve from the slice’s own declaration. The operator layers may tighten or add grants; they may not edit a slice’s declaration.
| # | DOCUMENT | OWNER | ANSWERS | CONSUMERS |
|---|---|---|---|---|
D1 |
|
the slice’s feedstock |
what host surface do I need, and why |
resolver (via the registry mirror) · press (reads the image) |
D2 |
|
the composition author |
what runs with what, where, how visible |
shim dispatch · tebako run · tebako press |
D3 |
|
tebako press (baked) |
the resolved needs of everything stitched |
the bootstrap at run time |
D4 |
|
the operator |
defaults, registries, per-invocation tightening |
shim · bootstrap |
D5 |
|
the invoker |
a one-shot D2 row |
tebako run |
D1 — a slice declares its needs.
The in-image manifest’s needs: block is the ONLY spelling of host-surface
requirements — one key, one grammar, one semantics. Every entry is absolute (or
a symbolic atom: $HOME, $TMPDIR, $CWD, $TEBAKO_HOME — resolved at bind,
per invocation, never baked), carries ro|rw, an optional bind-mount spelling,
an optional platform filter, an optional: true marker for probe artifacts —
and a MANDATORY why:.
Fail-closed by construction: a need absent without the optional marker is a
named error; an ro ancestor may not hide an rw descendant; rw on a
user-surface atom requires the composition’s explicit consent — declared write
access is visible by construction. Deriving a grant derives its strict
ancestors as traverse reads, so canonicalization walks pass (the JVM’s
realpath-at-boot case).
D2 — the composition document.
tebako.yaml — YAML, versioned JSON Schema, discovered walking up from the
cwd (or named with -f). The CLI flags are this document written inline:
tebako run accepts --slice name@req, --runtime, --entrypoint,
--mount host:mount:ro|rw, --policy open|deny, --need path:ro — each flag
maps 1:1 onto a key; flags and -f compose, flags win.
version: 1
runtime:
name: ruby
requirement: "~> 3.3"
slices:
- { name: metanorma, requirement: ">= 2.1" }
- { name: openjdk, requirement: "21" }
entrypoint: mnconvert
policy: deny # the DEFAULT once anything is declared
mounts: # operator bind-mounts (docker -v)
- { host: ./data, mount: /data, access: ro }
needs: # composition-level additions, D1 grammar
host:
- { path: "$HOME/.fontist", access: rw, why: "fontist cache" }
Jail-safe by default.
|
Note
|
Status: shipped. |
Any declaration in force — a composition document, a slice with needs, a CLI
flag — turns the policy default to deny; open must be asked for by name. A
run with NO declarations anywhere runs open (never_denies) — declarations are
what turn the jail on. Under deny, the effective world is exactly: the VFS
images (read-only by construction, not policy-gated at all); the platform
floor; the system’s own surface ($TEBAKO_HOME and the exec cache, read-only
at bind); the union of declared needs; the composition’s mounts/needs; and
argument files auto-granted ro. Everything else: EPERM/EROFS, journaled — and
the journal line names the covering declaration
(would-need=slice:openjdk) or says none exists, so the operator learns the
exact key to add from the denial itself.
The resolution algorithm.
One algorithm, three front ends (shim, bootstrap, tebako run/press), in
order:
1. slice set the entrypoint slice + transitive depends: (at resolved versions) + the runtime
data slices join the mounts; they declare no needs (a needs block = named error)
2. needs union every slice's D1 needs.host, platform-filtered, atoms resolved at bind time
3. compose effective = needs-union ∪ mounts ∪ operator-grants, then operator tightening
(policy, --no-host, explicit denies) intersects — declarations request, the
operator tightens
4. needs-check every declared need verified against the effective policy; an uncovered need is
a NAMED RESOLUTION FAILURE before exec:
"slice <name> needs <path> (<access>, why: <why>) — denied by <source>"
5. export the authored part serializes to TEBAKO_JAIL (+_SOURCE, +_JOURNAL); the floor and
the system surface are NOT serialized — every bind re-derives them, so a spawned
child enforces exactly its parent's policy
| COMBINATION | RESOLVER | NEEDS SOURCES | POLICY DEFAULT |
|---|---|---|---|
|
— (resolved, never the composer) |
its release manifest |
n/a |
|
— (resolved) |
its D1 |
n/a |
|
— (resolved) |
none permitted (a needs block is a named error) |
n/a |
standalone package |
bootstrap |
D3 baked union ∪ lean runtime manifest ∪ operator env |
deny when D3 declares anything |
managed dispatch |
shim |
slice mirrors ∪ runtime manifest ∪ D2/D4/D5 |
deny when anything is declared |
|
CLI front end |
D2/D5 ∪ slice mirrors |
deny when anything is declared |
spawned child (class E) |
inherits TEBAKO_JAIL; the preload re-binds |
the PARENT’s composition — a child never re-resolves |
the parent’s |
Carried vs shared — one pipeline.
|
Note
|
Status: shipped — carried slots + shared runtime today. Status: planned — run-time payload resolution + lazy seeding (a planned amendment). |
Fat and lean are the same architecture (the locked model):
press always resolves and locks the full closure — every slice’s version and
sha256 recorded in the package manifest — and the only question left is where
the bytes live. A slice is carried (its bytes are carried in a trailer
slot) or shared (resolved at first run into the machine store, verified
against the locked digest, and shared with every package that needs the same
slice). A self-contained package carries everything — zero network, empty
cache, one file. A shared-runtime package carries its payloads and shares
the runtime — the shape packed-mn ships. lean/fat survive as deprecated
aliases with a named warning.
Carried slices seed the store lazily at first run — best-effort, tmp+rename,
journaled, never blocking the run — so a self-contained download doubles as a
cache prime. Shared slices resolve by the locked digests, fail-closed:
carry/shared changes where bytes come from, never what runs. Platform coverage
is declared by the slice itself (platforms: universal or an explicit triplet
list); the composition’s per-slice platforms: key is an assertion that can
narrow, never extend — a slice that cannot cover the host triplet is a named
error, never a silent fallback.
Discovery: the record mode.
A slice author usually does not know the host surface their executable touches.
policy: record (env form: TEBAKO_JAIL=record) allows every host check and
journals it (event=jail-allow path=… op=…) — nothing is denied, so the
workload cannot crash from policy during discovery, and spawned children append
to the same journal. The generator, tfs needs --from-journal, folds the
journal (denials included — a denial is an unmet need) into a draft needs:
block: atoms re-substituted, floor/store/exec-cache excluded, strongest
observed op wins, ancestor collapse, why: "TODO — observed: …" for the author
to replace. The human gate is the point: the record shows the observed minimum;
the author decides what production actually wants. The generator never edits a
manifest itself, and shipping record in force is a named lint warning.
Swap the composition, not the slice.
Slice images are immutable and content-addressed — a configuration change never
mutates or rebuilds one. In managed mode the swap IS editing tebako.yaml. A
standalone package’s baked composition is the DEFAULT; an external document
overrides it — first hit wins: --compose, then TEBAKO_COMPOSE, then a
sidecar <package>.tebako.yaml. The override replaces the baked
policy/mounts/needs/entrypoint; it can never conjure slice content — a slice
the trailer does not carry is a named error, and the trust checks apply to
newly referenced slices exactly as at press. The run’s provenance is journaled:
event=composition source=external(<path>) sha256=… or source=baked. Today
tfs exec --compose speaks the image-layer subset; the bootstrap/shim override
channels arrive in Phase-R.
See also: Jails and host proxying · The driver contract · Where shared slices live.