berm

The Manifest

What a program says it is: its ABI version, its tools, its README, and what it reaches for itself.

The manifest is JSON, carried in a .berm.abi section of the image rather than behind an export. That placement is the point — reading what an image claims must not mean running it. Manifest::from_image parses the section with nothing compiled and nothing executed, which is what an embedder assembling a prompt, or a service listing a registry, actually needs.

The section is a wasm custom section, which is what #[link_section] emits on wasm32. It holds JSON records back to back, in the order the linker joined them: the program's, and one {"event": …} per #[event]. Manifest::from_image merges them into one manifest.

let manifest = Manifest::from_image(&bytes)?;
for tool in &manifest.tools {
    println!("{}: {}", tool.name, tool.description);
}

A ToolSpec carries the tool's name, the description a model reads, a JSON Schema for its arguments, and optionally returns, a JSON Schema for its result, and "query": true for a read-only tool. events lists the typed logs the program emits, each with a name, a description and a JSON Schema of its fields. They are rendered at compile time from the Schema impls of the types the tools and events name. A tool's arguments are an object without a title; every other object schema carries the struct's name as title, and a string enum its enum's name. A tool or event marked #[deprecated] carries "deprecated": true, and version is the crate's version when it was built.

Compatibility

Manifest::breaks(next) lists every way next breaks a caller of this manifest: a tool or event removed, a tool no longer a query, an argument newly required, a result or event field no longer always present, a changed type, an unsigned integer becoming signed, an argument taking fewer strings or a result written with more. Arguments are compared as the new program reads them, results and events as old callers read them; added fields break nothing. bermd refuses a redeploy that breaks a tool or event a caller used in the last 30 days, unless the deploy says force=true, and reports the rest.

Package

description, homepage and repository are the crate's, from its Cargo.toml, as version is. readme is the Markdown file #[program(readme = "README.md")] names, read at compile time. Each is empty when the crate sets none.

Dependencies

#[berm_lang::program(
    programs = ["weather"],
    syscalls = { "berm.get", "berm.http.fetch" = ["api.example.com"] },
    secrets = ["example"],
)]

is what an image says it will reach for once it runs: programs it calls by name, syscalls it makes beyond the ABI's own, each beside what it reaches through that call, and secrets it names as {secret:NAME} in a header. The manifest carries them as "deps": {"programs": [...], "syscalls": {"name": [...]}, "secrets": [...]}.

berm.http.fetch holds a program to them: a host it does not declare, or a secret it does not declare, is refused before anything is sent. berm.http.proxy does the same for the hosts declared under it, and sends through the host's proxy. berm.call reaches the program itself and the programs it declares, and refuses any other name. A declared secret is substituted by the host for the chain's caller, and the guest never holds its value. These are runtime and resolved wherever the image lands: nothing here is fetched, installed, or compiled in, which is what separates them from the crate's own [dependencies].

Declared by the author rather than found by reading the image, because a target the program computes at the call is invisible to any scan — a list built that way would be quietly short, and the runtime Refused would still be needed underneath it.

A declared program not deployed refuses nothing: crab deploy says so and carries on, and crab inspect marks it. A syscall the host does not serve refuses the deploy. What a served syscall reaches through it is that syscall's to grant. Refusing would make deploy order significant, and a restart brings programs back in whatever order the filesystem lists them — two that name each other could then never come up at all.

Two checks at load

Berm::deploy refuses an image on two grounds before it can ever be offered to a model.

A manifest built against a different abi_version is refused outright, rather than dispatched into a syscall its author did not mean.

A manifest that declares a tool the image does not export is also refused. The symbol table and the manifest are both in hand at load, so the disagreement is caught there instead of surfacing mid-conversation as a missing symbol. Exports are matched by the berm_tool_ prefix.

Both are the same idea as compiling at deploy rather than on first call: a broken image should fail for whoever introduced it, not for whoever happens to use it next.