Hosted tools
A command-line tool published as a pack. Each call runs it in a fresh microVM with no network, then deletes the VM. A preview, on for every organization.
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.
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:
| Field | Accepted |
|---|---|
engine | a caret range that does not accept 1.0.0, such as ^1.1 |
tool.kind | cli |
tool.base | {"name": "base"}: Ubuntu with Python 3 and jq |
tool.size | absent, or {"vcpu": 1, "memoryMb": 512}: 1 vCPU and 512 MiB |
tool.bundle.archive | required: sha256, bytes, format (tar or tar.gz), unpackedBytes, entries; the build fills them |
tool.runtime, tool.network | absent, or {"mode": "call"} and {"mode": "none"} |
tool.upstream, tool.notices | the 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.timeoutMs | 100 to 60000, default 30000 |
effects.class | pure |
x-b3os-file | on a top-level property of type: object, {"accept": [...]} in the input schema and {"produces": [...]} in the output schema, at most 4 each way |
pricing | free, 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:
bashnode 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}$:
bashcurl -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:
yamlsteps: - 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
| Limit | Value |
|---|---|
| Time of one call | the 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 VM | 1 vCPU and 512 MiB, the base template's disk, no network |
| The archive | 64 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 |
| Input | the call's 1 MiB request body, files included |
| Output | stdout up to 1 MiB; output files up to 256 KiB in all, inline |
| At once | 4 calls per organization, 8 per node |
| Starts | 120 a minute per organization, from every surface (HTTP, MCP, routines) |
| Publishers' prices | 20 USD a month per calling organization, counted on each node and kept across its restarts |
Errors
| Code | Status | reason |
|---|---|---|
tool_failed | 502 | unpack (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_out | 504 | the tool's command did not finish in time |
tool_unavailable | 503 | no_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:
| Unit | Counts |
|---|---|
actions.vcpu_ms | the CPU the call's VM used |
actions.gib_ms | its memory, GiB times milliseconds |
actions.tool_call | one per call whose command ran: the per-call floor |
actions.publisher_microusd | the 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.