Skip to content

ARCHITECTURE · 10

Dispatch.

Managed mode works as follows: the user types a command name, and per invocation tebako picks the payload version and the runtime that the command runs on. There is no version manager daemon and no shell eval hook, only one dispatcher binary behind every shim, re-resolving from scratch every single time.

Note

The dispatcher, its management verbs, the registry format, and the dispatch-time registry cache are shipped and live in tamatebako/tebako as crates/tebako-shim. One piece is still planned: a tebako use command that would write the user’s default version; today the user authors ~/.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 payload is a .tfs image of the app, versioned, immutable, and 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

The runtime is 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

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

Dispatcher

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

One invocation, end to end

Nothing is sticky. Every invocation of a shim walks the whole chain again, and a directory switch means 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 go 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.

The driver’s treatment of the handoff, the mount order, longest-prefix dispatch, and the fail-closed rules, is on the anatomy page; this page covers everything that happens before the interpreter starts.

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

  1. The environment link is TEBAKO_<TOOL>_VERSION: the command name uppercased, with every non-alphanumeric character becoming an underscore (metanorma → TEBAKO_METANORMA_VERSION). One shell carries one pin, and nothing is written anywhere.

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

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

  4. The registry default is the default: key that the publisher declared in the publisher’s tpkg-registry.yaml, read through the dispatch cache (below). Registered registries are scanned in config order, and 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 the user 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/, and the newest compatible runtime already cached wins, with no download. Only a cache miss reaches the network, which fetches the newest compatible interpreter the runtime factory released for this platform; that interpreter is SHA-256-verified against the release index before it enters the cache. Because the payload image is immutable, swapping runtimes never touches it, and 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, and older payloads simply run on newer interpreters. One universal .tfs serves every platform, and the per-platform runtime supplies the machine code.

Native-extension payloads

Native-extension payloads are locked to the ABI line they were built against, and the line is 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, and 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; this is the compat window, in which a payload’s abi check never fails against an unknown line. An entrypoint with no runtime_requirement at all, a native tool or 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 speaks 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>, and --no-host, which form the tightening surface and can only narrow what a payload’s manifest requests. Everything else, unknown flags included, reaches the payload verbatim, and -- ends flag parsing for arguments that literally carry those names. On unix the shim execs, so the process becomes the runtime and 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, and 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.

Registry use is 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 is the second property: 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, and TEBAKO_OFFLINE=1 turns dispatch into cache-or-named-error, in which a stale cache still answers offline and 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

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, and no hidden subcommands:

Command What it does

tebako-shim list

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

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

It re-enables a disabled tool, or one disabled version of it.

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

It refuses dispatch of a tool, or of one version of it; dispatch of a disabled target fails with a named error.

tebako-shim which <tool>

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

tebako-shim doctor

It diagnoses the shim layer: PATH, shim → payload records, trust anchors re-hashed, registry cache freshness, and runtime cache entries.

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

It prepends ~/.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 …]

It removes exactly the managed block, and nothing else in the file is touched.

Disable state lives in ~/.tebako/shims/.disabled.yaml, shim-managed and deliberately separate from the user’s authored config.yaml, so disable never rewrites a hand-maintained file. doctor is the diagnostic path that 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

Shell integration is one directory on PATH: ~/.tebako/shims. tebako-shim install-shell inserts a managed BEGIN/END block into the right startup file for the user’s shell (.bashrc, .zshrc, ~/.config/fish/config.fish, .cshrc); it is 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, and on Windows an NTFS hardlink first (no admin and no Developer Mode) with a byte copy as the named fallback, never a .cmd wrapper. There is also no eval "$(… init -)" hook for switching versions, because the dispatcher reads the project file itself, per invocation.

Two distribution forms, one press

tebako press produces both forms. For users without tebako, the form is the standalone per-platform tpkg executable; for dispatcher users, it is the registry payload, one universal image for a pure-language app with per-triplet variants only when native code forces them. 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 is that 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

The management-command argv is malformed, and 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 is cached or downloadable, the resolved version is not installed or is disabled, or an offline registry miss occurred; the chain links and the fix are named.

70

checksum

A downloaded artifact failed SHA-256 verification against the release index; it was deleted, and 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; the refusal comes before any download, with both sides named.

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

The mnenv retirement, in practice

Note

This retirement is 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, and dispatched by the shim. The whole model on this page is thus exercised by the very project it serves.