A hosted tool is a pack whose actions are vm actions. The pack carries one archive of the tool's files. Each call claims a fresh microVM on the node, copies the archive and the call's input files into it, runs the tool's command with the request as JSON on stdin, reads one JSON answer and the output files, deletes the VM, and then answers. A VM is never reused, has no network, and belongs to the calling organization for the length of one call. You call a hosted tool like any action: POST /v1/actions/call, as an MCP tool, or as a routine step.

Note

Hosted tools are a preview, on for every organization in the beta. No tool is published yet: in the beta only B3's organizations will publish tools, each into a namespace the registry's publisher list gives it, and publishing by outside publishers opens later. b3os tool comes with CLI 0.2.1, which is not published yet.

The manifest

A tool is a directory: b3pack.json, the tool's files under tool/ (they are unpacked at /tool in the VM), the notices file and fixtures/. This is the echo tool, the test tool in the repository (examples/tools/echo): one shell script with three actions.

json
{ "pack": "com.example.tools/echo", "version": "1.0.0", "title": "Echo", "description": "A hosted test tool of one POSIX shell script: noop answers an empty object, echo returns its text and its file, and fail fails the way its input asks. No network.", "publisher": { "id": "com.example.tools", "name": "Example Tools (a test publisher)" }, "license": "Apache-2.0", "engine": "^1.1", "pricing": { "model": "per_call", "usd": "0.0001" }, "tool": { "kind": "cli", "base": { "name": "base" }, "size": { "vcpu": 1, "memoryMb": 512 }, "bundle": { "archive": { "sha256": "", "bytes": 0, "format": "tar", "unpackedBytes": 0, "entries": 0 } }, "runtime": { "mode": "call" }, "network": { "mode": "none" }, "upstream": [], "notices": "NOTICES.md" }, "actions": [ { "key": "echo", "title": "Echo text and a file", "description": "Returns its text input as text, and its file input, when given, as the output file of the same name.", "inputSchema": { "type": "object", "required": ["text"], "additionalProperties": false, "properties": { "text": { "type": "string", "maxLength": 10000 }, "file": { "type": "object", "x-b3os-file": { "accept": ["text/plain", "application/octet-stream"] } } } }, "outputSchema": { "type": "object", "required": ["text"], "additionalProperties": false, "properties": { "text": { "type": "string" }, "file": { "type": "object", "x-b3os-file": { "produces": ["text/plain", "application/octet-stream"] } } } }, "effects": { "class": "pure" }, "impl": { "kind": "vm", "protocol": "json-stdio", "cmd": ["/tool/bin/echo-tool", "echo"] }, "execution": { "timeoutMs": 10000 } } ]}

The manifest above shows one of the three actions; the file in the repository has all three. What a tool pack may declare in this version:

FieldAccepted
enginea caret range that does not accept 1.0.0, such as ^1.1
tool.kindcli
tool.base{"name": "base"}: Ubuntu with Python 3 and jq
tool.sizeabsent, or {"vcpu": 1, "memoryMb": 512}: 1 vCPU and 512 MiB
tool.bundle.archiverequired: sha256, bytes, format (tar or tar.gz), unpackedBytes, entries; the build fills them
tool.runtime, tool.networkabsent, or {"mode": "call"} and {"mode": "none"}
tool.upstream, tool.noticesthe software the tool runs and did not write, and the notices file of the pack bundle
impl{"kind": "vm", "protocol": "json-stdio", "cmd": [...]}, cmd[0] under /tool/
execution.timeoutMs100 to 60000, default 30000
effects.classpure
x-b3os-fileon a top-level property of type: object, {"accept": [...]} in the input schema and {"produces": [...]} in the output schema, at most 4 each way
pricingfree, or per_call with usd of at most 1 and at most 6 decimal places, on the pack or an action. A tool in io.b3os has no price

A tool pack has no connections, no triggers and no egress, and every action is vm. A value this version does not take (another base or size, hosts or caller network, sessions, setup, image, formats) is refused with a sentence that names it (the b3pack publish checks 13 to 20 list them all).

Do not put a secret in a bundle. Treat every file of the archive and the pack bundle as public: callers never reach the VM, but the archive goes to every node, and a tool has no network to use a secret with anyway.

Build and publish

The build makes the archive from tool/ (the same files give the same bytes), fills tool.bundle.archive, runs the publish checks and signs the manifest with a development key id of the pack's namespace:

bash
node packages/packs/src/cli/tool.ts build examples/tools/echo --kid com.example.tools#dev-2026-10b3os tool publish examples/tools/echo

b3os tool publish uploads the archive (PUT /v1/actions/registry/blobs/sha256:<hex>) when the registry does not have it, publishes the pack, and prints the exact pin of each action. It needs a key that holds actions:tools:publish by name (* and actions:* do not grant it; in the dashboard, owners and admins hold it) and actions:write. A node installs the version at its next registry sync, after it has fetched the archive and checked its sha256.

Call it

A file value is {"$bytes": "<base64>", "name": "<name>"}, and the name matches ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$:

bash
curl -sS https://beta-api.b3os.org/v1/actions/call \ -H "Authorization: Bearer $B3OS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"ref": "com.example.tools/echo/echo@1", "input": {"text": "hi", "file": {"$bytes": "aGVsbG8K", "name": "hello.txt"}}}'

The answer is an action result with tier: "vm", the output with each output file as {"$bytes", "name", "bytes", "contentType"}, the call's cost in micro-USD (compute, floor, price, total), and the timing phases validate, start, copy, unpack, run, collect, delete and cost. As an MCP tool the action is echo__echo (GET /v1/actions/packs/com.example.tools/echo/tools), and in a routine it is a step:

yaml
steps: - id: echo uses: com.example.tools/echo/echo@1 with: text: hi

The json-stdio contract

The node puts each input file at /in/<field>/<name>, makes /out and /work, and runs cmd with no shell, in /work, as the VM's user, with one JSON object on stdin:

json
{ "action": "echo", "input": { "text": "hi", "file": { "path": "/in/file/hello.txt", "name": "hello.txt", "bytes": 6, "contentType": "text/plain" } }, "limits": { "timeoutMs": 10000, "maxOutputBytes": 262144 }, "call": { "id": "tcall_01m77a0c8dkdq3w5x6y7z8a9bc" }}

The tool writes its files directly under /out and, as its last line of stdout, exactly one of {"output": {...}}, where a file is {"path": "/out/<name>"}, and {"error": {"code": "...", "message": "...", "retryable": false}}. It exits 0 in both cases. The request names no organization, project or key, and the VM has none of the B3OS_* variables a sandbox has: a tool cannot tell who called it. stderr is never logged and never returned.

Limits

LimitValue
Time of one callthe least of the caller's timeout_ms, the action's timeoutMs and 60 seconds; on POST /v1/actions/call a priced action refuses a lower timeout_ms with 400 invalid_request, and a routine step's call runs to the action's own timeout
The VM1 vCPU and 512 MiB, the base template's disk, no network
The archive64 MiB packed; its manifest declares at most 512 MiB unpacked and 20,000 entries, and a node checks the archive against those numbers when it installs it
Inputthe call's 1 MiB request body, files included
Outputstdout up to 1 MiB; output files up to 256 KiB in all, inline
At once4 calls per organization, 8 per node
Starts120 a minute per organization, from every surface (HTTP, MCP, routines)
Publishers' prices20 USD a month per calling organization, counted on each node and kept across its restarts

Errors

CodeStatusreason
tool_failed502unpack (the archive did not unpack), exit (with exit_code, or the VM stopped answering while the command ran), signal (with signal), protocol (the last line of stdout is not one JSON answer), tool_error (the tool's own error: tool_code, tool_message, retryable), output_invalid (with errors), output_file, output_too_large
tool_timed_out504the tool's command did not finish in time
tool_unavailable503no_slot (no VM, or the call's time ran out before its command could start), copy, exec (the run was refused before it reached the VM), node_busy; nothing is billed; retry after Retry-After

Past a limit a call gets 429 rate_limited, 429 concurrency_limited or 429 quota_exceeded. Each of these, and every 503 tool_unavailable, carries Retry-After: have your client wait that many seconds before it sends the call again. Every call is a fresh VM. A call that finished is charged even if your client had stopped waiting, so send an Idempotency-Key with a priced call: a retry with the same key gets the stored answer and is not charged again. A routine step's retry carries the step's key and gets the answer of the call its first attempt started.

What a call costs

A hosted tool call is metered in four units, under the caller's own key:

UnitCounts
actions.vcpu_msthe CPU the call's VM used
actions.gib_msits memory, GiB times milliseconds
actions.tool_callone per call whose command ran: the per-call floor
actions.publisher_microusdthe publisher's price in micro-USD, once the tool answered, its output matched its schema and its output files were read

A failed call is charged compute and the floor, never the price; a call that failed before the command ran is charged nothing. A tool call writes no actions.call record. The publisher's unit is one micro-USD each and counts against a key's spend cap; the other three units are priced as the usage page says.

The publisher reads what its tools earned with b3os tool earnings --namespace com.example.tools (GET /v1/actions/publishers/{namespace}/earnings): calls, gross, B3OS's cut and net for one month. B3OS keeps nothing of a publisher's price until the earlier of 12 calendar months after its first priced call or 1,000 USD of sales, then 20%; the line says which rate is in force and why. A publisher sees daily totals per tool, never a customer or a call. Amounts are accrued: a record of what was sold. Nothing is paid out by B3OS's software in this version.

Not there yet

Publishing by organizations outside B3, private (org.) tools, the code-interpreter, files and browser bases, a network for a tool, input files by URL or upload and output files by link, files larger than the inline limits, sessions that keep a VM between calls, tools built from an image, and MCP servers as tools.

Ask a question... ⌘I