> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beta.b3os.org/llms.txt
> Use this file to discover all available pages before exploring further.

# 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:

```bash
node packages/packs/src/cli/tool.ts build examples/tools/echo --kid com.example.tools#dev-2026-10
b3os 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

| 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`](/errors#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`](/errors#tool_timed_out) | 504 | the tool's command did not finish in time |
| [`tool_unavailable`](/errors#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`](/errors#rate_limited), [`429 concurrency_limited`](/errors#concurrency_limited) or [`429 quota_exceeded`](/errors#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](/concepts/usage) 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.