Skip to content

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.

SHIPPEDexists in code today — shipped releases, or shipped-and-verified in tebako-rsPLANNEDlocked design (2026-07-26), not yet implemented — quoted so you can hold the project to it

1 · The payload contract: manifest.yaml.

SHIPPED · drives dispatch + install

A 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.org

THE 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 locked

Dependency 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 locked

Payloads 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-codes

THE 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

  1. Read the payload's manifest → build the graph (runtime + layers + app).
  2. Topological order — runtime first, layers, app last.
  3. Per node: cache hit on (version-constraint × triplet) → use it; else fetch from the provider registry, signature-verified.
  4. Verify → cache (0444 + trusted markers).
  5. Compose the mount stack (/__runtime__, /__layers__/<name>, /__app__) with the declared env → exec.

4 · Capabilities & entrypoints.

SHIPPED · N=1 todayPLANNED · suites (N&gt;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" }
one tpkgbootstrap + N imageslots + trailerentrypoints[N]in the manifestshim: doc-render→ slot 0 image · ruby ~> 3.3.0shim: img-render→ slot 1 image · ruby ~> 3.4.0shim: …one command per entrypoint — argv0 selects

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 form

Every 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-macosmacos-arm64shipped
x86_64-macosmacos-x86_64shipped
x86_64-linux-gnulinux-gnu-x86_64shipped
aarch64-linux-gnulinux-gnu-arm64shipped
x86_64-linux-musllinux-musl-x86_64shipped
aarch64-linux-musllinux-musl-arm64shipped
x86_64-windows-ucrtwindows-ucrt64shipped
aarch64-windows-ucrtreserved

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

SHIPPED

Every 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 locked

A 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 locked

GUI 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.

gui-app-one.tfsrequires: toolkit gtk-layermount: /__layers__/gtk~4 MB of app codegui-app-two.tfsrequires: toolkit gtk-layermount: /ui/toolkit (its own choice)~6 MB of app codegtk-layer.tfsbuilt ONCE per triplet · signedone content-addressed copyin the shared cache (0444)…not 2 copies inside 2 appsrunsmounts compose:runtime + layer+ app, per processhost-display honesty: on Linux the app talks X11/Wayland like any native binary — the VFS never virtualizes the display

8 · The dependency that proves it: metanorma + inkscape.

PLANNED · the first dogfood

Metanorma 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.tfsrequires: language ruby "~> 3.3.0"requires: data inkscape-payloadmount: /__deps__/inkscapeconsumer-declared mountruby runtime.tfsprovides: engine ruby, abi_line 3.3already cached — machine-sharedinkscape-payload.tfssigned · versioned · isolatedits own deps, its own constraintnever installed on the hostpandoc · ghostscript — same patternthe process sees/__runtime__ ← ruby.tfs/__deps__/inkscape ← inkscape.tfs/__app__ ← metanorma.tfszero host pollution:nothing to apt install,nothing to brew install,nothing left behinduninstall = cache prune

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-rs

The same chain the chain-of-trust page unpicks link by link — compressed to the figure every other contract on this page leans on.

signed payload.tfs + OpenPGP (rnp-rs)per-slot sha256TOFU keyringtrusted.pgp — new keysregistered by named promptverify at installsignature, then eachslot's sha256 — one passimmutable cache0444 + .sha256 / .originre-verify on re-fetch onlydispatch → runno per-run ceremonyfail closed, with names70 sha256 mismatch · 71 bad signature · 72 untrusted key · TEBAKO_REQUIRE_SIGNED=1

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.

TOOLWHAT IT GIVES YOUTEBAKO INSTEAD
rbenv / rvmA 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 / bundlerGems 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.
homebrewPer-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 / installersA 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 / aptSystem-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 / OCIUser-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.
VMsA 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 / snapDesktop 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.