learn / 03·4 — under toolkit · promotion

one source fileCLIMBSthree rungs

A script that worked once normally dies in a scratch dir. Promotion is the ladder out — session → workspace → registry, two commands and a few generated files. Org owns the spec, wasm owns the artifact, and the one invariant underneath it all: authoring a tool never grants the tool anything.

promotion11 min read
A small figure on a monumental three-tier terraced ziggurat under a bright orange sky, a single glowing seed rising step by step from a workbench at the base to a vast vaulted archive at the summit — 1970s sci-fi style, saturated, geometric

the graveyard of session SCRIPTS

An agent — or you — writes a useful little program mid-session. A slug-maker, a CSV cleaner, a parser that finally tames some ragged scraper output. It works. And then, in nearly every stack ever built, that's where it dies: a file in a scratch directory, copy-pasted into the next project, drifting out of true a little more each time someone touches it.

The gap between code that worked once here and tool everyone can install is normally enormous. A repo. A build pipeline. A package-registry account. A manifest, a version, a release. An afternoon — and that's on a good day, for a human who already knows the ritual. The honest reason most one-off tools never make the climb isn't laziness. It's that the climb is a cliff.

Promotion turns that cliff into a ladder with three rungs and two commands. Same source file the whole way up; what changes at each rung is only how durable the tool is and how far it can travel.

the LADDER

pro·mo·tion /prə·ˈmoʊ·ʃən/ noun

1. the lifecycle ladder a tool climbs — session → workspace → registry — from a source file built live in the sandbox, to a source-owned toolkit directory on disk, to a stored workbook installable on another Nexus.

The three rungs are named verbatim in the engine. Rung one is the session: build-inline compiles your source in the sandbox and registers it as a live command — no manifest, no project. Rung two is the workspace: promote writes a real toolkit directory, source plus a generated manifest, that rebuilds deterministically forever. Rung three is the registry: the toolkit is just a workbook, so Library.store publishes it and Library.install registers its commands somewhere else.

the lifecycle ladder — one source file, three rungs of durability
sessionbuild-inline — built in the sandbox, registered live; the binding is ephemeral
workspacepromote — a source-owned toolkit dir on disk; rebuildable, packable, discoverable
registrystore + install — packed into a workbook, shipped, registered on another Nexus
same source the whole way up — only durability and reach change

rung one — no manifest CEREMONY

One command turns a source file into a live, callable command:

$ wbx toolkit build-inline slug js slug.js
built + registered command `slug` (js) → build/commands/9f2c…e1.wasm
run it via the Dock: run-command slug

That output is verbatim from the engine; only the hash is illustrative. There is no project to scaffold and no manifest to write — the pipeline is named exactly four stages: write → build-in-sandbox → content-address → register. Six languages have an inline lane — rust, c, zig, js, ts, go — and the build runs entirely in the wasm sandbox: no native toolchain, no host escape, because the compiler is itself wasm.

flowchart LR
  src["slug.js
(source an agent just wrote)"] build["build in the sandbox
(the compiler is wasm)"] addr["content-address
sha256(wasm bytes)"] reg["register the binding
(:persistent_term)"] land[["build/commands/<sha>.wasm"]] src --> build --> addr --> reg addr --> land style src fill:#ffffff,stroke:#121316 style build fill:#f3c5a3,stroke:#121316 style addr fill:#f2ddb0,stroke:#121316 style reg fill:#13d943,stroke:#121316,stroke-width:2.5px style land fill:#fbfaf6,stroke:#121316

The third stage is the load-bearing one. Content-addressing means the wasm bytes are hashed with sha256 and parked at build/commands/<sha>.wasm. Identical source compiles to identical wasm, which produces an identical hash, which lands at the same path — so rebuilds are idempotent by construction. The registry itself is guarded: a built command's path must canonicalize inside build/commands/, the dynamic table is capped at 4,096 entries against denial-of-service, and the four built-ins are merged into lookup last so a poisoned dynamic key can never shadow a real one.

Run it through the Dock with run-command slug — the same import every other command goes through, which is exactly the seam that makes self-authoring safe. We'll come back to that.

what “session” MEANS

Depth rung — skippable. Rung one is called the session rung because the binding is ephemeral, and it's worth being precise about which part dies. The engine's own description: a session command is built into :persistent_term, gone when the Instance dies. But the compiled artifact is content-addressed on disk — those bytes persist. What's ephemeral is the binding: the name-to-artifact mapping, not the artifact.

where it livessurvives the Instance dying?
the binding (slug → this wasm)in-memory, :persistent_termno — gone with the Instance
the artifact (the wasm bytes)content-addressed on diskyes — never deleted

The verdict of that table: the work isn't lost, the wiring is. And the engine softens even that — a small registry.json manifest in the commands directory lets a boot-time reload re-register the built commands from their cached artifacts. The bytes were on disk; the manifest just remembers which names pointed where.

One more property falls out of never deleting the bytes: hot-swap. Re-registering a name replaces the binding live, no restart. In-flight calls finish on the old bytes because the old content-addressed file still exists; a current read tells you which artifact a name resolves to right now. Replacing a tool never yanks the rug out from under a call already running on it.

rung two — PROMOTE

The second command materializes the session command as a real, source-owned toolkit directory under the toolkits root:

$ wbx toolkit promote slug js slug.js
promoted session command → workspace toolkit `slug` at toolkits/slug
  build it: wb toolkit build slug
  then it packs into a workbook (Library.store) and installs elsewhere (Library.install)

Verbatim output again. Before it writes anything, promote runs the same guards in order: the name can't be a reserved built-in, it must match ^[A-Za-z0-9_.-]+$, the language must be one of the six, and the source file must exist. Trip a guard and you get a clean refusal — the failure rail is honest:

$ wbx toolkit promote grep js slug.js
cannot promote: "grep" is a reserved built-in command name

On success it doesn't compile anything. Promote scaffolds; it does not build. It writes your source at the per-language layout path, a generated manifest.org, and a skills/overview.org skill stub — then hands you the separate wb toolkit build slug to do the deterministic rebuild. Here's the richest case, rust, on disk after promote:

toolkits/slug/
├── manifest.org          ← generated spec
├── Cargo.toml            ← synthesized: [package] name="slug" version="0.1.0" edition="2021"
├── src/main.rs           ← YOUR source, verbatim
└── skills/overview.org   ← skill stub with a verification checklist

The generated manifest.org carries the keys that make it a real, first-party toolkit — and crucially the keys that drive the rebuild:

#+TITLE: slug
#+TOOLKIT: slug
#+VERSION: 0.1.0
#+STATUS: experimental
#+TAGLINE: Promoted session command.
#+EXEC: command
#+TRUST: first-party
#+CLI_BIN: slug
#+BUILD_LANG: rust
#+BUILD_SRC: path:.
#+ARG_MODE: argv

* slug :toolkit:
  :PROPERTIES:
  :ID:      slug
  :CLI_BIN: slug
  :END:
  Promoted from a session command. Source-owned + rebuildable.

That :toolkit: tag on the headline is not decoration — it's what makes the promoted tool discoverable by the standard (tags :toolkit:) query the same way every other toolkit is found. The skill stub beside it is deliberately small: a when to use this, a one-line workflow (run-command slug — argv plus stdin to stdout), and a two-checkbox verification list. It's a starting point you grow, not a finished manual.

org owns the SPEC, wasm owns the ARTIFACT

This is the rung's thesis, and the engine states it plainly: promotion persists the source — rebuildable — not just the compiled bytes. Org owns the spec; wasm owns the artifact. Why that matters: a directory of compiled bytes rots the moment the compiler moves or the platform shifts. A directory of source plus a declarative manifest can be rebuilt forever, on any Nexus, into the identical artifact.

flowchart LR
  man["manifest.org
#+BUILD_LANG: rust
#+BUILD_SRC: path:.
#+CLI_BIN: slug"] dir["build_dir(slug)
src/main.rs + Cargo.toml"] comp["compile in the sandbox"] addr["content-address
sha256(wasm)"] same[["same sha as the session build"]] man --> dir --> comp --> addr --> same style man fill:#f2ddb0,stroke:#121316 style dir fill:#ffffff,stroke:#121316 style comp fill:#f3c5a3,stroke:#121316 style addr fill:#13d943,stroke:#121316,stroke-width:2.5px style same fill:#fbfaf6,stroke:#121316

Read that left to right as a story. The manifest declares the language, the build source path, and the binary name. wb toolkit build reads those keys, assembles the build directory the compiler expects, compiles in the sandbox, and content-addresses the result. Because the source is byte-identical and the compiler is the same wasm compiler, the same source produces the same sha, which lands at the same path — the deterministic rebuild meets the session build at the very same artifact. The manifest is the recipe; the wasm is the dish; the recipe always cooks the same dish.

The trust line of that thesis is the part that makes sharing possible: trust stays first-party — yours; a third-party consumer grants its own #+CAPS on install. Your manifest says #+TRUST: first-party because it's yours. When someone else installs it, they decide what it's allowed to touch. Nobody inherits your grants.

per-language LAYOUTS

Depth rung — skippable. Why does rust get #+BUILD_SRC: path:. and a synthesized Cargo.toml at the toolkit root, while everyone else gets path:src? Because the rust build directory wants src/main.rs with Cargo.toml sitting beside it at the toolkit dir, whereas the other languages want their entry file directly under the directory handed to the compiler. The layout table is real and small:

langBUILD_SRCentry fileextra files
rustpath:.src/main.rssynthesized Cargo.toml
cpath:srcsrc/main.cnone
zigpath:srcsrc/main.zignone
gopath:srcsrc/main.gonone
jspath:srcsrc/index.jsnone
tspath:srcsrc/index.tsnone

The verdict of that table in one line: rust is the exception that needs a manifest file in the build dir, so it gets path:. plus a Cargo.toml; the other five just need their entry file under src/, so they get path:src and nothing extra. The split isn't arbitrary — it's the build directory each compiler expects, encoded once in the layout table instead of remembered five times.

rung three — a toolkit IS a WORKBOOK

The third rung is where the tool leaves your machine. And here's the move that makes it cheap: there is no separate toolkit package format — a toolkit IS a workbook. Everything is reuse. Publishing is store, searching is search, signing is the bundle's ship step, sharing rides the same DID-member path workbooks already use.

sequenceDiagram
  participant A as Nexus A (publisher)
  participant S as storage backend
(local / S3 / R2) participant B as Nexus B (installer) A->>S: Library.store — pack workbook
commands ship as <name>.wasm parts Note over S: workbooks/<slug>.wbundle
tenant-scoped, auto-indexed S->>B: fetch the bundle blob Note over B: gate 1 — sha pin: sha256(blob) must match Note over B: gate 2 — signature (optional, off by default first-party) B->>B: register_artifact each *.wasm — capped by Policy Note over B: {:ok, %{commands: ["slug"]}}

Walk that sequence. Nexus A calls Library.store, which packs the workspace into a .wbundle on whichever backend is configured, tenant-scoped and auto-indexed for semantic search. A workbook packed with build carries each command as a <name>.wasm part. Nexus B fetches the blob and runs two supply-chain gates before anything registers: an optional sha: pin — sha256 of the blob must match first — and an optional require_signature check for a valid embedded bundle signature, which is off by default for first-party local installs. Only then does it unpack and register_artifact each wasm part.

And the registered commands land under the same ceiling as everything else, which the engine says in five words: installing never widens caps. That's the bridge into the security section — because the same sentence is true of authoring.

the capability CEILING

Self-authoring sounds like the start of an incident report. Agents writing and running their own tools? The invariant that makes it safe is structural, not policed. The Dock imports an Instance can see are built from its Policy capabilities by construction: the host provides only the imports the Policy grants, so a component that tries to import a non-granted capability simply fails to instantiate. run-command is one of those imports — it exists only if the commands cap is granted.

So authoring a tool never grants the tool anything. A built command runs through the exact same run-command gate as every other command; an ungranted capability is un-importable, therefore un-callable. The real Policy profiles make this concrete:

profilememorytimeoutcaps
minimal64 MiB5,000 msbuild + run, nothing the profile didn't grant
network128 MiB30,000 msadds network
posix256 MiB60,000 msthe widest standard profile
compute64 MiB5,000 msvfs only — no commands at all

The verdict of that table is the whole security story in one example. An Instance on compute has vfs and nothing else — it can't even call a command it authored, because run-command was never imported. An Instance on minimal can build and run slug — but slug gets no network, no model access, nothing minimal didn't already hand it. The tool can never exceed the granted capability set, no matter who wrote it or how.

Trust shapes the isolation tier on top of that. #+TRUST is recorded at build, defaulting to first-party; third-party commands run isolated on a separate BEAM VM, with graceful fallback, wired straight into the Dock's run-command path. The build gate refuses third-party toolkits with invalid provenance (the author must sign the toolkit first) and refuses any #+CLI_BIN that would shadow a reserved name. And every run-command call appends a step to _steps.jsonl — the tool name, exit code, duration, timestamp — so the whole episode is on the record.

scaffold, not a publish BUTTON

Honesty section. promote is a scaffold, not a publish button. The generated skill stub is deliberately minimal — a when-to-use, a one-line workflow, two checkboxes — and it says so: extend the skill as the toolkit grows. The manual layer the parent lesson celebrates still has to be written by someone. Promotion gives you the skeleton, not the muscle.

The lanes have edges, and we'd rather name them. Six languages, not all of them. Inline crates.io dependencies are rust-only on the build-inline lane; the js and ts inline lanes take npm deps via a synthesized package.json, but the dependency story isn't uniform across the six. The session rung is ephemeral by design — the binding is gone when the Instance dies, and while the bytes and a small reload manifest soften that, the rung is a workbench, not a vault. And the reserved names mean you can't call your tool grep, jq, upper, or wbox — the built-ins win those names, always.

A toolkit comes out the door marked #+STATUS: experimental by default, because that's honestly what a freshly promoted session command is. None of this is software that runs itself — it's a ladder that makes the climb from worked once to installable everywhere cheap, with a human deciding what's worth climbing.

questions people actually ASK

Can my agent really do this unattended?

Yes — and it's capped the whole time. An agent can write a source file, build-inline it into a live command, and promote it to a toolkit without a human in the loop. What it can't do is grant itself anything: the built command runs through the same Dock gate as every command, and an ungranted capability is un-importable. An agent on the minimal profile builds tools that get exactly what minimal grants and not one capability more.

What if I promote the same name twice?

Promote writes into toolkits/<name> — a named directory — so a second promote targets the same path. Treat it as a scaffold you re-run, not an append. If you've grown the manual or added files, copy them aside before re-promoting so a fresh scaffold doesn't clobber work you care about.

Why can't I name it grep?

Because grep is a reserved built-in, along with jq, upper, and wbox. The built-ins are merged into command lookup last precisely so a dynamic name can never shadow them — which is a security property, not an inconvenience. Both build-inline and promote refuse a reserved name up front with a clean error.

Do I have to promote, or can I live on build-inline?

You can live on build-inline. Rung one is a complete, callable tool for the life of the Instance, and the reload manifest brings it back at boot. Promote when you want the tool to be source-owned, rebuildable, packable, and discoverable — the moment it stops being a session convenience and starts being a thing you'd ship.

Does the consumer of my toolkit get my capabilities?

No. Trust stays first-party — yours. A third-party consumer grants its own #+CAPS on install, installing never widens caps, and the registered commands sit under their Policy profile. Your tool can never do more on their Nexus than their profile allows, regardless of what it could do on yours.

Will my rebuilt tool really have the same hash?

By the content-addressing rule it should: the same source compiled by the same wasm compiler hashes to the same sha and lands at the same path, which is exactly what makes rebuilds idempotent. We say should rather than always deliberately — the determinism follows from content-addressing, and that's the design, but it's the principle we stand behind, not a guarantee we'll overstate.

keep GOING

Promotion is the rung after the parent lesson's one sentence about it — here's the way back up, and the pieces it stands on.