Skip to content

ARCHITECTURE · 08

Dispatch.

Managed mode: you type a command name, and per invocation tebako picks the payload version and the runtime that command runs on. No version manager daemon, no shell eval hook — one dispatcher binary behind every shim, re-resolving from scratch every single time.

Note

Status: shipped — the dispatcher, its management verbs, the registry format, and the dispatch-time registry cache are live in tamatebako/tebako as crates/tebako-shim. One piece is still planned: a tebako use command that would write your default version for you — today you author ~/.tebako/config.yaml directly. Every command shown on this page exists.

Four artifacts, four jobs.

A lean tebako package already is a dispatcher: it resolves its runtime at run time. Managed mode splits that trick into four independent artifacts, each with its own publisher, its own release line, and its own trust anchor.

Payload

A .tfs image of the app — versioned, immutable, optionally signed. Pure-language payloads are runtime-independent and ship as one universal image for every platform; native-extension payloads ship per triplet per ABI line.

Runtime

Two artifacts — the interpreter executable plus its env .tfs image — published by the runtime factory, downloaded once per machine, SHA-256-verified, mounted (never extracted), and shared by every package and payload on the box.

Registry

A developer-hosted tpkg-registry.yaml on any git host, mirroring only the resolution fields: versions, per-platform artifacts with digests, runtime requirements, entrypoints, the default. The host’s releases are the storage — zero central infrastructure.

Dispatcher

tebako-shim — one small static Rust binary, linked once per command name into ~/.tebako/shims/<tool>. The thing on PATH that picks version and runtime per invocation and hands off.

One invocation, end to end.

Nothing is sticky. Every invocation of a shim walks the whole chain again — switch directories and the same command can resolve to a different version on a different runtime. That is the mise model: the project file is read at dispatch time, so there is nothing to activate and no shell state to stale.

argv0 is the selector $ metanorma doc.adoc — the shell found ~/.tebako/shims/metanorma, a link to the one tebako-shim binary; the link's NAME picks the payload multi-command suites: N entrypoints → N shims, each resolved independently payload version — the chain, first hit wins 1 · TEBAKO_<TOOL>_VERSION — the environment override, one shell at a time 2 · nearest .tebako-tools.yaml walking up from the working directory 3 · defaults: in ~/.tebako/config.yaml — your machine-wide pin 4 · the registry's default: — the publisher's declared pick a disabled version is a named refusal, never a fallback to another runtime resolution — cache first the entrypoint's runtime_requirement → newest COMPATIBLE runtime already cached (no download) → else download the newest compatible from the factory index, SHA-256-verify, cache, share machine-wide no runtime_requirement at all → zero-runtime dispatch: skip this box entirely the handoff — exec tebako-runtime-… --tebako-image payload.tfs:0:/ … --tebako-entry /bin/metanorma doc.adoc + TEBAKO_RUNTIME_IMAGE (the env image) + the jail policy in TEBAKO_JAIL* the driver mounts the images, installs the jail, rewrites argv — your program runs RULES OF THE CHAIN · the whole chain re-runs on EVERY invocation — nothing is sticky, nothing caches a choice · the version must be installed; a pin to a missing version is a named error, not a surprise fetch · the payload image is never mutated — swapping runtimes changes only this dispatch · payloads verify at install; a run re-verifies nothing · a run installs nothing and links nothing — install is the explicit verb (tebako install) SEE IT WITHOUT RUNNING IT tebako-shim which metanorma resolved version + its source, runtime, mounts, the exact argv — and it never downloads windows: spawn + wait, the child's exit code propagated

Figure 1 — The dispatch cascade: invocation name, version chain, runtime resolution, the exec handoff, and the interpreter running.

What the driver does with the handoff — mount order, longest-prefix dispatch, the fail-closed rules — is on the anatomy page; this page is about everything that happens before the interpreter starts.

Four links, evaluated in order; the first link that names a version wins, and later links are not consulted.

  1. The environment: TEBAKO_<TOOL>_VERSION — the command name uppercased, every non-alphanumeric character becoming an underscore (metanormaTEBAKO_METANORMA_VERSION). One shell, one pin; nothing is written anywhere.

  2. The project pin: the nearest .tebako-tools.yaml walking up from the working directory — a flat YAML mapping of command name to version. One subtlety, locked: a nearer file that does not pin this command does not shadow a farther file that does — the walk continues until a file actually answers.

  3. Your default: defaults: in ~/.tebako/config.yaml, which you author by hand. The dispatcher only ever reads this file — a planned tebako use command will be the writer when it lands; until then, editing the YAML is the mechanism, not a workaround.

  4. The registry default: the default: key the publisher declared in their tpkg-registry.yaml, read through the dispatch cache (below). Registered registries are scanned in config order; the first one carrying the payload answers.

If no link answers, the error says so — listing every link it tried and the versions actually installed (exit 69). A resolved version that is not installed, or that you disabled, is likewise a named refusal with the remedy in the message. Dispatch never guesses a version.

Runtime resolution — cache first, then the factory index.

The chosen entrypoint’s runtime_requirement (an engine plus a version constraint) is evaluated against ~/.tebako/runtimes/: the newest compatible runtime already cached wins, with no download. Only a cache miss reaches the network — the newest compatible interpreter the runtime factory released for this platform — which is SHA-256-verified against the release index before it enters the cache. Because the payload image is immutable, swapping runtimes never touches it: only this dispatch-time choice changes.

Pure-language payloads

The constraint is a range — ruby >= 3.3, < 5.0. Any newer runtime inside the range works; older payloads simply run on newer interpreters. One universal .tfs serves every platform — the per-platform runtime supplies the machine code.

Native-extension payloads

Locked to the ABI line they were built against — two-dimensional: the version line (ruby ~> 3.3.0) and the platform line (abi: arm64-darwin-23, the runtime’s own platform string). A newer line needs a new payload build; a wrong line on either axis is a named compatibility error, never a segfault.

Runtimes published before the abi key existed carry no platform line, and stay eligible — the compat window: a payload’s abi check never fails against an unknown line. And an entrypoint with no runtime_requirement at all — a native tool, a self-contained binary — skips this entire section: zero-runtime dispatch runs the install-time materialization straight from the store, with no runtime mounted and no VFS in the child.

The handoff.

One exec, one wire format — the same launcher ABI the standalone bootstrap speaks. The registry payload is a bare image, so it mounts whole: slot 0, mount point /. Dependency slices follow at their consumer-declared mount points.

$ tebako-shim which metanorma
tool: metanorma
payload: metanorma 1.2.3
  version source: project /work/Doc/.tebako-tools.yaml
  image: ~/.tebako/payloads/metanorma/1.2.3.tfs
entrypoint: /bin/metanorma
runtime: ruby "~> 3.3.0" → ruby 3.3.9 (cached)
  exe: ~/.tebako/runtimes/ruby-3.3.9-0.16.9-aarch64-macos/tebako-runtime-0.16.9-3.3.9-aarch64-macos
  image: ~/.tebako/runtimes/ruby-3.3.9-0.16.9-aarch64-macos/tebako-runtime-0.16.9-3.3.9-aarch64-macos.tfs
mounts:
  ~/.tebako/payloads/metanorma/1.2.3.tfs:0:/
exec argv:
  tebako-runtime-0.16.9-3.3.9-aarch64-macos
  --tebako-image ~/.tebako/payloads/metanorma/1.2.3.tfs:0:/
  --tebako-entry /bin/metanorma
  doc.adoc

Environment on top of the inherited one: TEBAKO_RUNTIME_IMAGE names the env image the driver mounts at the runtime root; when a jail policy applies, TEBAKO_JAIL carries it, with TEBAKO_JAIL_SOURCE and TEBAKO_JAIL_JOURNAL for the audit trail.

The shim consumes exactly three flags before handing over — --jail <spec>, --mount <host:mount:ro|rw>, --no-host (the tightening surface; they can only narrow what a payload’s manifest requests). Everything else, unknown flags included, reaches the payload verbatim; -- ends flag parsing for the arguments that literally are those names. On unix the shim execs — the process becomes the runtime, so signals and the exit code are the program’s own; on Windows it spawns, waits, and exits with the child’s code.

The registry — explicit, cached, never searched.

A registry is any git-host repository carrying a tpkg-registry.yaml; the host’s releases are the artifact storage. The file mirrors only resolution-relevant fields — versions, per-triplet artifacts with SHA-256 digests, runtime_requirement, entrypoints, default: — so the dispatcher resolves without downloading every payload. Platform selection happens in the file, declaratively: the dispatcher reads the entry for the host triplet (or universal) and fetches exactly that artifact. Nothing guesses by filename.

Explicit-only: a fresh install of tebako has zero registries. tebako add-registry <ref> registers one; there is no default registry, no central index, and no search chain — each reference form resolves in exactly one place (the repo’s default-branch root via the contents API, a pinned release artifact, a git blob path, or a local file://), and an unparseable reference is a named error, never a guess.

The dispatch cache: remote registries use a per-reference cache under ~/.tebako/registries/<sha256-of-ref>.yaml with a 24-hour TTL, so dispatch never blocks on the network twice in a row. tebako update-registries renews it ahead of time; tebako add-registry primes it with the bytes it already fetched; TEBAKO_OFFLINE=1 turns dispatch into cache-or-named-error — a stale cache still answers offline, a missing one fails by name.

Install is the explicit verb: tebako install <name[@ver] | ref> resolves the registry, selects the host entry, downloads, verifies (SHA-256 and/or the OpenPGP signature), and lands the payload byte-identical in the content-addressed cache — then registers the payload’s shims. Registry installs always link the declared entrypoints; installing a local package file links only with the explicit --shims; a run, of anything, ever, links nothing.

Managing the shim layer — the real verbs.

Invoked by its own name, the same binary is the management tool. This is the whole surface — there is no tebako use, no tebako shim add, no hidden subcommands:

Command What it does

tebako-shim list

installed payloads, their versions, the resolved default and its source, and every shim link

tebako-shim enable <tool>[@<ver>]

re-enable a disabled tool, or one disabled version of it

tebako-shim disable <tool>[@<ver>]

refuse dispatch of a tool, or of one version of it — dispatch of a disabled target fails with a named error

tebako-shim which <tool>

preview a dispatch without running it: the resolved version and which chain link produced it, the runtime, the mounts, the exact exec argv

tebako-shim doctor

diagnose the shim layer: PATH, shim → payload records, trust anchors re-hashed, registry cache freshness, runtime cache entries

tebako-shim install-shell [--shell bash|zsh|fish|csh]

prepend ~/.tebako/shims to PATH via a managed block in the shell startup file (Windows: the user PATH in the registry)

tebako-shim uninstall-shell [--shell …]

remove exactly the managed block — nothing else in the file is touched

Disable state lives in ~/.tebako/shims/.disabled.yaml — shim-managed, deliberately separate from your authored config.yaml, so disable never rewrites a hand-maintained file. doctor is the diagnostic path the error messages point at: it re-hashes payload images against their install-time trust anchors and exits 1 when anything is wrong.

Shell integration — one directory, no eval hook.

One directory on PATH: ~/.tebako/shims. tebako-shim install-shell inserts a managed BEGIN/END block into the right startup file for your shell (.bashrc, .zshrc, ~/.config/fish/config.fish, .cshrc) — idempotent, and uninstall-shell removes exactly its block. On Windows the shim directory goes onto the user PATH in the registry instead. The links themselves are the platform’s own: symlinks on unix; on Windows an NTFS hardlink first — no admin, no Developer Mode — with a byte copy as the named fallback, never a .cmd wrapper. And there is no eval "$(… init -)" hook for switching versions: the dispatcher reads the project file itself, per invocation.

Two distribution forms, one press.

tebako press produces both. For users without tebako: the standalone per-platform tpkg executable. For dispatcher users: the registry payload — one universal image for a pure-language app, per-triplet variants only when native code forces it. The release flow is press → sign → upload the artifacts → commit the registry manifest, and tebako publish automates it for GitHub releases and file:// mirrors. Building every variant is a matrix CI job, not manual work.

Every refusal has a name.

The dispatcher reuses the loader’s named exit set rather than inventing its own. The dispatch-specific truth behind the table: a native-extension payload on a wrong-ABI-line runtime is exit 69 with both lines named — never a segfault.

Exit Refusal When the shim raises it

64

usage

malformed management-command argv — the error prints the verb’s usage line

65

manifest

a payload record is missing or corrupt, a command is provided by two installed payloads, or a registry does not parse — the message points at tebako-shim doctor

69

unavailable

no compatible runtime cached or downloadable, the resolved version is not installed or is disabled, an offline registry miss — the chain links and the fix are named

70

checksum

a downloaded artifact failed SHA-256 verification against the release index — it was deleted; nothing entered the cache

74

I/O

a filesystem, lock, or install operation failed

75

contract

the runtime release speaks a handoff contract this shim does not — or declares none at all; refused before any download, both sides named

Once the interpreter is running, exit codes are the program’s — the shim is out of the picture by then.

The mnenv retirement, in practice.

Note

Status: shipped.

With tebako-shim shipped, mnenv — metanorma’s Ruby version manager — retires, and metanorma becomes the first dogfood consumer of this exact chain: a heavy native-extension app, pressed per (version × ruby line), published through its own registry, dispatched by the shim. The whole model on this page, eaten by its own cooks.