Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Running the Service

bermd is a long-running service that deploys harnesses and serves their tools over MCP.

An invocation is ephemeral, but three things around it are not: the deployed set, the modules compiled from it, and the engine’s code cache. That is what the service exists to hold. A harness is compiled once, at deploy, so an invocation pays only instantiation.

bermd &
berm deploy example ./harness.elf
berm ls

Deploying

The control API is resource-shaped — GET, PUT and DELETE on /harnesses/{name} — for the reason dockerd’s is: the clients are a UI, a CLI, and curl, and a harness is a resource.

An image is compiled before it is stored. A rejected one therefore leaves nothing behind, rather than a file that fails again on every restart. What is stored is restored on the next start, and one unloadable image is reported and skipped rather than taking the service down with it.

There is no container here, so there is nothing to start or stop: a harness is deployed or it is not, and dockerd’s lifecycle verbs have nothing to attach to.

One endpoint

Every deployed harness appears on a single MCP endpoint at /mcp, with tools named {harness}.{tool} — the same dotted rule the guest ABI already uses for host call names, so fs.read reads the way it does inside a harness.

One endpoint rather than one per harness, for the reason dockerd has one socket rather than one per container. It costs two things, both cheap: names have to be namespaced, and harness names may not contain a dot, because dispatch splits on the first one.

Deploying and undeploying emit notifications/tools/list_changed. The tool set moves under clients that are already holding a list, which per-harness endpoints would have made someone else’s problem.

Usage, and why it is a resource

MCP gives a server one instructions string. A manifest carries one usage per harness. Folding every harness’s usage into instructions would put a manual in front of the model on every turn — which is the one thing usage is defined not to be.

So instructions stays a sentence, and each harness publishes its usage as a berm://{harness}/usage resource. A model reads it when choosing among one harness’s tools, and pays nothing for the harnesses it is not considering.

What a deployed harness can reach

Its arguments, the log, and any other harness on the same daemon.

There is no wiring step and no per-pair permission. What is deployed is reachable by name, which is the reach containers on one network have of each other, bounded the same way: by what the operator chose to run.

let result = berm_lang::call("inner", "echo", r#"{"query":"hi"}"#)?;

bermd serves this as one system harness, berm.call, and the target is a field in the request rather than part of the call number. So an image is never built against a particular deploy — the same bytes work wherever they land, and deploying them twice under two names makes them reachable as both.

A name nothing answers to is CallError::Refused: nothing ran. The target running and reporting failure is CallError::Failed. That is the same split the control API draws between a status and an Output::Failed, and a harness can act on it — falling back when something is not deployed, say.

--max-call-depth bounds the chain, 4 by default, 0 refusing the first nested call. The bound is on runaway composition rather than on the stack, which a level costs around 720 bytes of; what a level does cost is 64 MiB of reserved guest address space.

That reach stops at the daemon. A harness still touches the world outside only through system harnesses its host registered, and bermd registers none — so what matters before deploying an image is what the image is. Manifest::from_elf reads a harness’s tools and usage without compiling or running it, which is what berm inspect shows you.

The endpoint carries no authorization and binds loopback by default.