---
title: "Tool declarations"
description: "Look up the fields for tool declarations."
canonical_url: "https://docs.oz0.ai/reference/plugin-manifest/tools"
---
# Tool declarations

> Look up the fields for tool declarations.

For: Plugin authors looking up manifest fields.

Look up the fields for tool declarations.

## `tools[]`

Each tool is an MCP server. Set exactly one of `mcp` (your own code), `package` or `url`; `package` may also set `mcp` to name the command to run. A flow tool sets `flow` instead, and none of them.

<field-group>
<field name="id" type="string" :required="true">

Lowercase letters, digits, dashes and underscores, unique in the plugin and in the tenant, and never `__`, which separates a tool from its function in the names models see (`<tool>__<function>`).

Start it with the plugin's name, dashes as underscores, or with the name's first dash-separated segment (`acme_github` and `acme_fetch` in `acme-github`), so it cannot clash with another plugin's; `oz0_` is reserved for Orchestrator Zero's verified plugins, and lint warns of a name that only looks like it (`ozo_`, `0z0_`). Agent names follow the same rule with dashes.

Models, policies and the audit see a function as `<id>.<function>`, and an address (`<plugin>/<id>.<function>`) only names its plugin, so an ID belongs to one plugin per tenant, as an agent's name does: an install whose tool ID another plugin of the tenant provides (in its installed version or a version waiting for its gate or rollout) is refused, naming that plugin.

`plugin lint` checks one plugin on its own and cannot see the tenant; `plugin preview` and the install can. See [Limits](https://docs.oz0.ai/concepts/limits-and-scaling/limits#the-limits).

</field>

<field name="mcp" type="string[]">

The command that starts the MCP server over stdio. Without `package`, it runs in the plugin's own environment, and `python` means that environment's Python. With `package`, it runs in the package's environment.

</field>

<field name="package" type="string">

An existing MCP server: `pypi:<name>[==<version>]`, `npm:<name>[@<version>]`, `oci:<registry>/<repository>@sha256:<digest>` or `binary:<url>`. Unpinned PyPI and npm packages are a warning; the edge pins them at install time. An npm range or tag (`@^1.2`, `@latest`) is an error: name one version, or none. Every PyPI and npm package is also pinned by the hashes of its files, with every package it depends on, which nodes check when they install it ([pinning and hashes](https://docs.oz0.ai/build/plugins/packages#pinning-and-hashes)). An `oci:` image must name its digest, and runs on nodes with Docker or Podman ([container images](https://docs.oz0.ai/build/plugins/container-images)). `binary:` is not run yet.

</field>

<field name="container" type="object">

For an `oci:` tool only: `memory_mb` (default 512), `cpus` (default 1) and `pids` (default 256), the container's limits.

</field>

<field name="isolation" type="string">

For an `oci:` tool: `gvisor` runs its container under gVisor's `runsc`, only on nodes labelled `isolation.gvisor=ready`, and never without it; the plugin's agents that call it require that label ([Sandbox tier 2](https://docs.oz0.ai/fleet/sandbox-tier-2)).

For a command tool (`mcp:` or a package that is not an image), `gvisor` runs its server under the node's own rootless runsc, behind the egress proxy, only on nodes labelled `isolation.gvisor-sandbox=ready`; not with `sandbox:` set, and not for a remote tool ([gVisor without Docker](https://docs.oz0.ai/fleet/sandbox-tier-2/gvisor#gvisor-without-docker)).

`microvm` boots its image in a virtual machine of its own, experimental, only on nodes labelled `isolation.microvm=ready`, which need no Docker ([microVMs](https://docs.oz0.ai/fleet/sandbox-tier-2/microvms#microvms)). A `microvm` tool's `env` and its plugin's secrets cannot be named `PATH`, `HOME`, `TMPDIR`, a proxy's variable, or `LD_*`, `DYLD_*`, `SMOLVM_*` or `OZ0_VMENV_*`.

</field>

<field name="url" type="string">

A remote MCP server over Streamable HTTP. `http://` is a warning; use `https://`.

</field>

<field name="connection" type="string">

A remote tool's (`url`) connection, one of this plugin's `connections` that lists the tool: each call carries the token the edge mints for it in its own session's `Authorization` header, never in `_meta`, and the server is listed with a token of it.

The tool's `url` may then be `https://{params.<name>}/<path>`, its host (and port) from a param of the connection, https only, its path literal, on a connection with steps (which record the host when it is connected); the connection's scope names its functions or `<tool>.*`. It sends no `Authorization` header of its own and is not bound under `requires.inject`. See [remote servers on connections](https://docs.oz0.ai/build/parts/tools#remote-servers-on-connections).

</field>

<field name="functions" type="map">

Overrides of a remote tool's functions, by the name its server lists each under (at most 256): `annotations` (`readOnlyHint`, `destructiveHint`, `openWorldHint`; they set what the server does not say and tighten what it says, and loosen only in an `ai.oz0` listing of OZ's own index), `output_schema` (an object schema of at most 64 KB, which the node validates structured results against and cards are drawn from), `usage` (`service`, `unit`, `units`, `cost_usd`: what each successful call spends) and `ai_label` (a JSON pointer such as `/body` to the string argument the tenant's AI label goes at the start of).

</field>

<field name="headers" type="map">

Headers a remote tool (`url`) sends with every request, such as `Authorization: "Bearer ${SEARCH_TOKEN}"`. Each `${NAME}` is a secret in `requires.secrets`, filled in on the node. A header that carries a credential must take it from a secret; at most 16 headers. Under a [credential binding](https://docs.oz0.ai/admin/credential-injection#remote-tools) of that secret the node sends its placeholder, and its credential proxy puts the value on, on the bound host only. See [headers of a remote server](https://docs.oz0.ai/build/parts/tools#headers-of-a-remote-server).

</field>

<field name="affinity" type="string">

`node` (default): runs on the job's home node. `any`: runs on any node that has the plugin.

</field>

<field name="requires_approval" type="string[]">

This tool's functions that a person must decide every time, as `<id>.<function>` (or `<plugin>/<id>.<function>` with this plugin's own name): every call to one waits for a decision ([Approvals](https://docs.oz0.ai/agents/approvals)), whatever the tenant's tool policy says short of refusing it, and the install screen shows it as "Ask, set by the plugin", with no switch.

Keep it for a call whose effect cannot be undone and reaches outside the tenant (sending mail, a merge), or one the system expects a named person to authorise. A write that should ask by default, which the installer may switch, is a rule of the plugin's [`policy`](https://docs.oz0.ai/reference/plugin-manifest/sets-policy#policy) instead.

</field>

<field name="manual" type="string[]">

Skills of this plugin that say how to use the tool's functions well, by name or as `<plugin>/<skill>` with this
plugin's own name: each one of the plugin's skills, listed once, at most 8. Another plugin's skill is refused ("a
tool's manual is its own plugin's skills"). An agent granted any of the tool's functions gets them
([A tool's manual](https://docs.oz0.ai/build/parts/skills#a-tools-manual)).

</field>

<field name="approval" type="object">

How this tool's calls are approved, when a person decides them: under `requires_approval`, the plugin's [`policy`](https://docs.oz0.ai/reference/plugin-manifest/sets-policy#policy), or the tenant's own rules ([Approvals](https://docs.oz0.ai/agents/approvals)).

`deadline`: how long a call waits before it is denied, never approved, 1m to 720h (the agent's, then the tenant's, 24h when nobody sets one; a tenant rule wins).

`escalation`: `off` (with `escalation_reason`, since it loosens oversight) or the ladder's steps, at most five, each `{at: <fraction or "90%">, to: deciders | owner | admins | role:<role> | team:<name>}`; the default reminds the people who may decide at a quarter, tells the agent's owner at half and the tenant's admins at three quarters.

`edit: false` keeps a person from approving a call with other arguments. `confirm: <argument>` asks a person to type that argument's value before a call runs, for high-risk calls. `deciders` and `not_starter` say who may decide. `plugin lint` checks each.

</field>

<field name="env" type="map">

Extra environment variables for the tool's process. Keys must be environment variable names, and not the dynamic loader's (`LD_*`, `DYLD_*`, `GLIBC_TUNABLES`, `GCONV_PATH`), `PATH` or the node's own (`OZ0_*`), which lint refuses and a node leaves out. `OPENAI_BASE_URL` and `ANTHROPIC_BASE_URL` may name a provider's public URL, never the node's loopback. An `oci:` tool's variables reach its container by a file; a value with a line break cannot, so under a name its container runtime reads on the host (`DOCKER_HOST`, `CONTAINERS_*`, `HOME`, `XDG_*` and the like) it is refused.

</field>

<field name="timeout" type="duration">

How long one call of the tool may run, such as `35m`: five minutes unless set, two hours at most. A call that runs longer fails.

</field>

<field name="sandbox" type="string">

`own` for a tool that runs what it is given in a [sandbox](https://docs.oz0.ai/concepts/security/sandbox-tiers#sandboxes) of its own, as pr-reviewer's `run_tests` runs a pull request's tests. In a job an outside event started, whose tools run in a sandbox of the job's, such a tool's calls run in the plugin's own server on the job's tier-1 home node instead, since a sandbox cannot start another.

Only a plugin that holds nothing may say so: no `requires.secrets`, no `optional_secrets`, no connection that covers the tool, no `egress`, and its own `mcp` command; lint and installs refuse it otherwise, and the node and the edge check again.

Only a call the job's workflow marked leaves the job's sandbox, and the edge checks each one and records it in the audit trail as `tool.own_sandbox`; a hook's call, an event's normaliser and a harness session's tool calls stay in the job's sandbox, where such a tool should refuse.

The tool's server runs outside any sandbox as the node's user, so it must not parse what it is given either: unpack or read the input in the sandbox it starts. It takes up to four calls at once.

`host` for a local tool that must see the node itself, its processes, mounts or services, as host-health does: on a Linux node with sandbox tier 1 every other plugin's server runs in a sandbox of its own, and this one runs as the node's user, next to the node's other processes.

`plugin lint` warns. On a node with sandbox tier 1, where `--plugin-sandbox strict` is the default, such a tool does not start until the node's operator exempts its plugin (`oz0-node run --plugin-sandbox-exempt <plugin>`), and the same holds for `own`: a line of a manifest never takes a tool out of the sandbox by itself.

The plugin's status and the node's findings say so. In a job an outside event started a `host` tool's calls are not made: they would run in the job's sandbox, where it could read only that sandbox. ::

<field name="approval.deciders" type="object">

Who may decide the approvals of this tool's calls: `role` (`operator`, the default, or `admin`), `teams` (teams of the tenant, by name, at most 16) and `people` (accounts, as `user:<name>`, at most 32) and `keys` (the tenant's API keys that may decide, by key ID, at most 8: a tenant's own plugin's integration; no other key decides an approval). A decider holds at least the role and, when teams or people are named, is one of the people or in one of the teams. The edge enforces it on every decision ([Who decides](https://docs.oz0.ai/agents/approvals/configure#who-decides)).

</field>
</field>
</field-group>

<field name="approval.not_starter" type="boolean">

`true` refuses the job's starter as the one who approves (four eyes); they may still deny.

</field>

<field name="secret_arguments" type="map">

The arguments of the tool's functions a model may write a `{{secret:NAME}}` placeholder into, by function, as JSON pointers (`run: [/env/API_KEY]`), at most 16 per function. A tenant admin binds a secret to one (`oz0 tenant credential bind <tenant> NAME --function <id>.<function> --argument <pointer>`); the node fills the value in inside the call and scrubs it from the result ([Credential injection](https://docs.oz0.ai/admin/credential-injection#secrets-in-a-models-arguments)).

</field>

<field name="flow" type="string">

A flow tool: the name of one of this plugin's flows, which the tool exposes as its one function, `<id>.<flow>`. An agent that lists the function decides whether to call it; its flow runs as a child job, each step through its own gates.

Its description, arguments and result are the flow's `description`, `input_schema` (required, with an object at its root) and `output_schema` (a warning without one); its annotations are derived from the steps, never looser than the most consequential one, and it declares the action class `oz0/class` (`host.read`, `host.write` or `host.destructive`) when a step may be a host tool.

It sets no `mcp`, `package`, `url`, `env`, `container`, `headers`, `sandbox`, `timeout`, `limits`, `isolation`, `secret_arguments` or `affinity` (its flow runs on its plugin's pool); `requires_approval` may name its function.

Lint refuses a flow that is not in the plugin, a second flow tool of the same flow, and a flow tool that would reach itself. See [Flows as tools](https://docs.oz0.ai/build/parts/flows/flow-tools#flows-as-tools).

</field>

<field name="limits" type="object">

For a local tool (`mcp`, or a PyPI or npm `package`): what its server may use of a node in its plugin sandbox, `memory_mb` (at least 16), `pids` (processes and threads, at least 8) and `cpus` (0.01 or more).

Unset, a server gets 1024 MiB, 256 and one CPU; a server that runs a browser, a JVM or a local model asks for more, such as `limits: {memory_mb: 4096, pids: 2048}`. The tenant's [ceiling](https://docs.oz0.ai/concepts/security/protected-files#limits-protected-paths-and-file-writes) still caps it.

`plugin lint` refuses it on a remote or OCI tool (an OCI tool has `container`) and warns that it does not apply with `sandbox: host` or `own`.

</field>

<field name="labels" type="object">

What the tool's results are labelled when they say nothing in their `_meta` ([taint and labels](https://docs.oz0.ai/govern/taint-and-labels)): `source`, the system its results come from (an unlabelled result then gets the tenant's default level, never the lowest), and `untrusted: true` for a tool whose results hold outside text although it stays inside the tenant, such as a mailbox. A result may say more in `_meta` under `oz0/labels` ([building tools](https://docs.oz0.ai/build/parts/tools/policy#labels)).

<field name="results.fit" type="boolean">

`true` declares that the tool's functions whose input schema has an integer `budget` property beside a `view` of the views `stub`, `summary`, `section` and `full` follow the convention of [results that fit a budget](https://docs.oz0.ai/build/parts/tools/results-and-files#results-that-fit-a-budget): they take `view`, `budget` and `query`, and answer with the biggest view that fits and a fit note under `oz0/fit`.

The runtime then fills in a `budget` a model's call leaves out, before the tool policy and an approval see the call.

`plugin lint` refuses it on a flow tool, warns on an empty `results` block, and, with a [measured listing](https://docs.oz0.ai/reference/plugin-manifest/tools#toolslistingjson), warns when no function of the tool takes a budget and names each function with a `budget` but without such a `view`, for which the runtime fills in nothing.

</field>

<field name="uses_model.purpose" type="string">

The tool's functions and hooks call a model, and why, in 1 to 200 characters. Each call then brings a key for the node's gateway path made for the call's job, in its MCP request's `_meta` under `oz0/model`; the model call is metered, budgeted and under the tenant's data rules like any model call of the job. An install needs the tenant's grant (`plugin install --accept-model`). Not on a remote or a flow tool ([Calling a model from a hook or a tool](https://docs.oz0.ai/build/parts/hooks#calling-a-model-from-a-hook-or-a-tool-uses_model)).

</field>
</field>

### Files between tools

A tool can hand another tool a file, such as [ai.oz0/github](https://docs.oz0.ai/build/plugins/github)'s `download_archive` hands pr-reviewer's `run_tests` a repository's archive, without the bytes passing through a model, the job's history or either plugin's storage. The tool that makes the file returns it as an MCP embedded resource whose URI is `oz0-file:sha256:<hex>`, the SHA-256 of its bytes, and names that URI in its result.

The runtime checks the bytes against the name, keeps them on the edge (per tenant, at most 32 MiB, for 90 days after they were last stored, apart from the jobs' own payloads) and drops them from the result. A call whose argument is such a URI gets the file as a path of its own on the node, in the call's `_meta` under `oz0/files` (`{"<uri>": "<path>"}`), deleted when the call ends.

For a tool from a container image the path is under `/oz0/files/`, a folder the container gets read-only, and for one in a microVM under `/work/.oz0-files/` in the folder the VM shares: the path is always one the tool opens where it runs. Only the job tree that stored the file reads it (with the later turns of its conversation, and a job a person with the content permission hands it to); any other job gets "not found" ([who may read a file](https://docs.oz0.ai/build/parts/tools/results-and-files#files-between-tools)).

## `tools.listing.json`

Not part of the manifest: a file next to it that `plugin test` and `plugin pack --list-tools` write and you commit, the plugin's [measured tool listing](https://docs.oz0.ai/build/parts/tools/snapshots#the-measured-listing). It holds, per tool, each function as a node records it for its snapshot (`name` as `<tool>.<function>`, the `listing` with its description, schemas and annotations, and its `hash`), and `source_digest`, the SHA-256 of the plugin's other files (without `tools.amplification.json` and `sbom.cdx.json`, which are written from them), which says when it is stale.

`plugin inspect`, `plugin lint`, the studio and the hub read it without running anything; the first node's snapshot still decides what nodes call. Do not edit it by hand: run `plugin test` again.

[Back to oz0-plugin.yaml](https://docs.oz0.ai/reference/plugin-manifest).


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
