ARCHITECTURE · 06
The feature surface.
The deep sibling of the overview: every feature of the platform, expressed the way the platform expresses it — as contracts. The newest of these (the payload manifest, the dependency algebra, suites, UI layers) are locked designs, and they are badged that way.
1 · The payload contract: manifest.yaml.
SHIPPED · drives dispatch + installA payload is not a blob — it is a self-describing artifact. Every .tfs image carries its own manifest at a well-known path inside the image, /__tpkg__/manifest.yaml, and any consumer — bootstrap, dispatcher, tfs CLI, a foreign project — reads it through TFS itself. No sidecars, no central service. YAML is the locked convention for all configuration surfaces, matching versions.yml and the patch manifests. Because the manifest is integrity-bound to the image digest, it is exactly as tamper-proof as the content.
# /__tpkg__/manifest.yaml — illustrative; the locked schema
schema_version: 1
kind: app # app | data | toolkit | runtime | language
name: my-app
version: 1.0.0
# — provenance —
producer: { tool: tebako-cli, tool_version: "…" }
created: 2026-07-27T00:00:00Z
source: { app_commit: 9f3c2ab… } # or src tarball hash / builder id
sbom: sbom.spdx.json # reference, inside the image
# — identity (two digests, two jobs) —
digest:
tree_hash: 4c9e… # plaintext merkle root — semantic identity (CAS)
blob_sha256: 71af… # transport identity
# — trust descriptors: optional, per part —
signing: { state: signed, keyid: 0123…cdef, mechanism: openpgp }
encryption: { state: none } # or per-part: { paths, algorithm, envelope_refs } — never keys
# — yours —
annotations: # free-form k/v, OCI-style; unknown keys preserved
org.example.homepage: https://example.orgTHE KIND SYSTEM
appdatatoolkitruntimelanguage
One schema, kind-specialized: an app has entrypoints, a data image has mount semantics and readers, a runtime promises an engine (below), a toolkit is a shared native layer, a language is the engine axis itself.
THREE TIERS, NO DUPLICATED AUTHORITY
The in-image manifest is the rich layer. The tpkg trailer stays minimal and C-parseable — mount/exec directives, per-slot digests, the signature — and references the manifest by image digest, never duplicates it. The registry manifest mirrors only the resolution-relevant fields, so the dispatcher resolves without downloading every payload.
Shipped substrate today: the tpkg v2 trailer and the release index. The in-image schema + validator, tfs info printing it, and press embedding it at build time are the landing that flips this to shipped.
2 · Promises: provides.
PLANNED · design lockedDependency resolution matches declared capabilities, never file guesses — the RPM Provides: / pkg-config semantics. A payload's provides is its promise of what it can do: which executables it carries, which libraries, which paths, which environment it sets. The dispatcher trusts the promise or rejects it — it never goes spelunking inside the image to find out.
# a runtime payload's promise — illustrative
provides:
engine: ruby
version: 3.3.7
abi_line: "3.3"
platform: aarch64-macos
built_from: { src_digest: 9c37…, patch_set: v0.2.1 }
env: { GEM_PATH: /__runtime__/lib/ruby/gems/3.3.0 }
capabilities: { exec: true, read: true, runtime: true }WHY PROMISES, NOT PROBING
A cached runtime is selected by matching the consumer's runtime_requirement against the cached runtimes' provides — read from their manifests, never from a version string scraped off a binary. The runtime images published by tebako-runtime-ruby already carry every fact this promise formalizes (engine, version, platform, src digest, patch set) — the contract turns today's release metadata into a queryable capability.
3 · Dependencies: requires.
PLANNED · design lockedPayloads declare their dependencies; the dispatcher resolves them as a graph. Three dependency kinds cover the whole space — the language engine, native toolkit layers, and shared data layers — and every node of the graph is the same signed .tfs artifact type. One coherent algebra, all the way down.
requires:
- kind: language # the engine (ruby today; python/julia later)
engine: ruby
constraint: "~> 3.3.0" # abi-line lock (native exts)
# …or ">= 3.3, < 5.0" (pure ruby — a range)
- kind: toolkit # a native toolkit layer (gtk-layer, qt-layer)
name: gtk-layer
constraint: ">= 3.24, < 3.25"
triplets: [aarch64-macos, x86_64-linux-gnu]
mount: /__layers__/gtk # declared by the CONSUMER (below)
- kind: data # a shared data layer
name: iso-codes
constraint: ">= 2024.1"
mount: /__app__/share/iso-codesTHE MOUNT-DECLARATION RULE
The mount point is declared in the consumer's manifest, authored at press time — docker-compose volume semantics. The consumer's code knows where it looks for things; the provider never dictates its mount location. The dispatcher resolves the capability, then mounts at the consumer-declared path — top-level, or inside the app's own memfs namespace.
THE RESOLUTION ALGORITHM
- Read the payload's manifest → build the graph (runtime + layers + app).
- Topological order — runtime first, layers, app last.
- Per node: cache hit on (version-constraint × triplet) → use it; else fetch from the provider registry, signature-verified.
- Verify → cache (0444 + trusted markers).
- Compose the mount stack (
/__runtime__,/__layers__/<name>,/__app__) with the declared env → exec.
4 · Capabilities & entrypoints.
SHIPPED · N=1 todayPLANNED · suites (N>1)Every payload carries an entrypoints array and a capabilities set. Today every package has exactly one entrypoint and runs as one command. The contract generalizes: a suite is one tpkg with N image slots and N entries — one binary, many commands, each dispatching to its own image and its own runtime requirement.
# N=1 — the simple app (today's shape)
entrypoints:
- name: my-app
path: /__app__/bin/my-app
runtime_requirement: { engine: ruby, constraint: ">= 3.3, < 5.0" }
capabilities: { exec: true, read: true }
# a suite — one tpkg, N slots, N commands (planned)
entrypoints:
- name: doc-render
path: /__slots__/doc/bin/render
runtime_requirement: { engine: ruby, constraint: "~> 3.3.0" }
- name: img-render
path: /__slots__/img/bin/render
runtime_requirement: { engine: ruby, constraint: "~> 3.4.0" }Capabilities are the payload's self-declaration of what consumers may do with it: exec (runnable), read (mountable for reading), runtime (usable as an engine). A data image declares { exec: false, read: true } — and the dispatcher believes it.
5 · The platform axis.
SHIPPED · per-platform assets todayPLANNED · universal + triplet manifest formEvery payload declares platforms as either universal — pure-ruby or data, one image for every platform — or an explicit list of canonical triplets in vcpkg form <arch>-<os>[-<abi>]. One mapping, owned by the tpkg crate, consumed identically by the dispatcher, the release tooling, and the registry:
| TRIPLET (CONTRACT) | RELEASE ASSET (TODAY) | STATUS |
|---|---|---|
| aarch64-macos | macos-arm64 | shipped |
| x86_64-macos | macos-x86_64 | shipped |
| x86_64-linux-gnu | linux-gnu-x86_64 | shipped |
| aarch64-linux-gnu | linux-gnu-arm64 | shipped |
| x86_64-linux-musl | linux-musl-x86_64 | shipped |
| aarch64-linux-musl | linux-musl-arm64 | shipped |
| x86_64-windows-ucrt | windows-ucrt64 | shipped |
| aarch64-windows-ucrt | — | reserved |
Two distribution forms ride this axis, both produced by tebako press: the standalone tpkg (per-platform always — for users without tebako) and the registry payload (one universal .tfs when pure-ruby; per-triplet variants only for native-ext apps). See the released matrices on the runtime and libtfs release pages.
6 · Shims: every package self-runs; one dispatcher rules them all.
The bootstrap-as-shim
SHIPPEDEvery lean/fat package already is a shim: a tiny launcher that reads its own trailer, resolves the runtime into the shared cache (downloading it once if needed), and execs your app. No installer, no package manager, no tebako on the user's machine. This has been the shipping behavior since tebako-bootstrap v0.2.0 — the version manager simply generalizes it.
tebako-shim, the dispatcher
PLANNED · design lockedA tiny static binary linked per tool into ~/.tebako/shims/<tool> — one directory on PATH, one-time setup, no eval-init hook. argv[0] selects the entrypoint (suites get one shim per command); the dispatcher reads .tebako-tools.yaml walking up from cwd for the per-project pin, then resolves the cached payload + runtime. The full design: the version manager.
Naming note: the project-pin file started life as .tebako-tools.json in the design docs and moved to .yaml with the YAML-everywhere lock — all configuration surfaces speak YAML.
7 · UI applications.
PLANNED · design lockedGUI apps get the same algebra as everything else: the toolkit is a layer, not a bundle. A gtk-layer is built once per triplet, published as a signed .tfs, and content-addressed — so ten GUI apps on one machine share exactly one copy. A webview variant (the app renders into the platform webview instead of a full toolkit) rides the same contract.
8 · The dependency that proves it: metanorma + inkscape.
PLANNED · the first dogfoodMetanorma is a heavyweight, native-extension, real-world app — and it shells out to heavyweight CLI tools, inkscape among them. Today that means "install inkscape on your host and hope the version fits." In the managed world it is a requires: edge: inkscape ships as its own isolated, signed payload with its own runtime constraint, mounted into metanorma's namespace at the consumer-declared path — and the host stays exactly as clean as it was before. pandoc, ghostscript, and every other "please also install…" CLI dependency generalize the same way.
Metanorma is the retirement gate for mnenv — the first real dogfood of the whole stack: payload build via tebako press (SDK native builds included), signed .tfs per (version × ruby line), the registry manifest, the dispatcher.
9 · The chain of trust, in one figure.
SHIPPED · package side — tebako-rsThe same chain the chain-of-trust page unpicks link by link — compressed to the figure every other contract on this page leans on.
10 · Future runtimes: the contract is not ruby-shaped.
PLANNED · python · julia · …Look back over this page and try to find the ruby in it. The manifest's kind: language takes an engine name; the constraint algebra (range vs abi-line lock) is language-neutral; the dispatcher's cache-constraint-mount-exec loop never inspects the engine; the trust chain signs bytes, not rubies; the bootstrap's launcher ABI hands over images and an entry, not an interpreter. A python payload with engine: python, constraint: ">= 3.12, < 4.0" and a python runtime promising provides: { engine: python, abi_line: "3.12" } slot into the same contract and the same dispatch algebra — no new machinery, just new artifacts.
The one genuinely ruby-specific piece is the patch home — tamatebako/ruby, where upstream interpreter source gets its IO routed through TFS. Each new language needs exactly one of those (a tebako-runtime-python factory beside tebako-runtime-ruby) — everything downstream of it is already language-agnostic. Tebako was born an "advanced image packager for interpretive languages"; the contracts finally say so.
11 · Compared to what you use today.
One honest line each — what that tool gives you, and what tebako does instead.
| TOOL | WHAT IT GIVES YOU | TEBAKO INSTEAD |
|---|---|---|
| rbenv / rvm | A ruby on your machine, per project, with an eval-init hook in every shell. | Runtimes shared machine-wide, picked per invocation by the dispatcher — one shims dir on PATH, no shell hooks. |
| rubygems / bundler | Gems installed into a ruby you must already have, on every machine, every deploy. | The whole bundle pressed into a signed image once — resolved at install, mounted read-only at run. |
| homebrew | Per-OS packages with host-wide side effects and a root-adjacent /opt tree. | Isolated payloads in a user-owned cache; nothing touches the host, uninstall is a cache prune. |
| .dmg / installers | A drag-into-Applications macOS app — one OS, no version algebra, no integrity story beyond Gatekeeper. | One contract across macOS/Linux/Windows, with versions, constraints, and OpenPGP verification built in. |
| dnf / rpm / apt | System-wide installs as root, dependency resolution against whatever the distro happens to ship this year. | Per-process namespaces, no root anywhere, and constraints resolved against a cache — the distro is irrelevant. |
| docker / OCI | User-space containers that need a daemon, a root-equivalent group, and a running engine per host. | One file, or one shim: no daemon, no engine, no privileges — the "container" is a mounted image inside the process. |
| VMs | A whole guest OS per app — gigabytes and a hypervisor to carry one ruby. | A few megabytes and no hypervisor: the platform boundary is the ABI, not a machine. |
| AppImage / flatpak / snap | Desktop single-file apps — Linux-first, toolkit bundled per app, stores and daemons of their own. | The same single-file trick on every desktop OS — plus CAS-shared toolkit layers, so ten GUI apps carry one GTK. |
Fairness note: several rows describe the managed mode, which is badged planned on this very page. The shipped standalone mode is the "one file" column; the rest is the locked design you're reading.
12 · Why it's amazing — the honest close.
Not adjectives — properties. Each is either shipped today or a direct consequence of a shipped mechanism.
A namespace closed under mounting
Images are files; any file is mountable — including files inside other mounts. Mounts compose and stack. The runtime image, a toolkit layer, and the app all hang in one per-process tree with longest-prefix dispatch.
Per-process, privilege-free
The VFS lives in userland: no kernel module, no FUSE requirement, no root, no daemon. Each process owns its namespace object — Plan 9's idea, delivered as a library.
Content-addressed, signed layers
Two digests per artifact — a merkle root for identity, a sha256 for transport — plus OpenPGP signatures and an immutable 0444 cache. Dedup is free; tampering has a named exit code.
One ABI any language can call
tebako_fs_* is a C ABI: callable from C, C++, Rust, Ruby, Python, anything with an FFI — with two independent implementations kept honest by the same oracle suite.
The packager and the packaged are the same artifact type
tebako-cli is a static binary that packages apps into static binaries; the dispatcher is a shim; every package was a shim all along. The platform eats its own cooking at every layer — which is why the contracts stay honest: the people who write them have to run on them.
Keep reading.
The overview for the platform story · runtime as image for the .tfs split · the chain of trust for the v2 trailer and verification · the version manager for the dispatcher design · the Rust track for what ships in code today.