Running the Service
bermd is a long-running service that deploys programs 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 program is compiled once, at deploy, so an invocation pays only instantiation.
bermd &
crab deploy
crab ls
Deploying
The control API is resource-shaped — GET, PUT and DELETE on
/programs/{name} — for the reason dockerd's is: the clients are a UI, a CLI,
and curl, and a program is a resource.
GET /programs/{name} answers the program as anyone it is visible to sees it:
its IDL under program, and under served the calls of its tools from outside
that ended done, per day over the last 30 days and in total.
GET /programs/{name}/idl answers the IDL alone: name, version, digest, package
fields and public tools, what a caller needs. GET /programs/{name}/runtime answers
how it runs: its other tools, deps, events, and what of deps nothing here
answers to. A deploy answers the IDL beside how it broke callers of the program
it replaced.
A deploy is recorded only once its image has compiled, so a rejected one is never restored. What is recorded is restored on the next start, and one unloadable image is reported and skipped rather than taking the service down with it.
A tool is called with POST /programs/{name}/tools/{tool}, the body its
argument object. A tool its manifest marks query also answers
GET /programs/{name}/tools/{tool}?args=<json>, the argument object URL-encoded;
any other tool answers that with 405.
There is no container here, so there is nothing to start or stop: a program is deployed or it is not, and dockerd's lifecycle verbs have nothing to attach to.
One endpoint
Every deployed program appears on a single MCP endpoint at /mcp, with tools
named {program}.{tool} — the same dotted rule the guest ABI already uses for
host call names, so fs.read reads the way it does inside a program. A call
over MCP carries no caller, so the endpoint lists and calls only #[anonymous]
tools.
One endpoint rather than one per program, for the reason dockerd has one socket rather than one per container. It costs two things, both cheap: names have to be namespaced, and program 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-program endpoints
would have made someone else's problem.
READMEs as resources
MCP gives a server one instructions string, sent on every turn. Each program
with a README publishes it as a
berm://{program}/readme resource, which a model reads when choosing among one
program's tools.
What a deployed program can reach
Its arguments, the log, itself, and the programs on the same daemon its
manifest declares in programs. A call to any other name is
CallError::Refused.
let result = berm_lang::call("inner", "echo", r#"{"query":"hi"}"#)?;
bermd serves this as one syscall, 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 program 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 program still touches the world outside only
through syscalls its host registered, and bermd registers none — so
what matters before deploying an image is what the image is. Manifest::from_image
reads a program's tools and README without compiling or running it, which is what
crab inspect shows you.
The endpoint carries no authorization and binds loopback by default.