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
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.
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 lives | survives the Instance dying? | |
|---|---|---|
the binding (slug → this wasm) | in-memory, :persistent_term | no — gone with the Instance |
| the artifact (the wasm bytes) | content-addressed on disk | yes — 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:
| lang | BUILD_SRC | entry file | extra files |
|---|---|---|---|
| rust | path:. | src/main.rs | synthesized Cargo.toml |
| c | path:src | src/main.c | none |
| zig | path:src | src/main.zig | none |
| go | path:src | src/main.go | none |
| js | path:src | src/index.js | none |
| ts | path:src | src/index.ts | none |
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:
| profile | memory | timeout | caps |
|---|---|---|---|
| minimal | 64 MiB | 5,000 ms | build + run, nothing the profile didn't grant |
| network | 128 MiB | 30,000 ms | adds network |
| posix | 256 MiB | 60,000 ms | the widest standard profile |
| compute | 64 MiB | 5,000 ms | vfs 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.