---
title: "Tool labels and approvals"
description: "Carry labels and argument origins through tool policy checks."
canonical_url: "https://docs.oz0.ai/build/parts/tools/policy"
---
# Tool labels and approvals

> Carry labels and argument origins through tool policy checks.

For: plugin authors.

Carry labels and argument origins through tool policy checks.

## Labels

A result may say what it holds, in its `_meta` under `oz0/labels` ([taint and labels](https://docs.oz0.ai/govern/taint-and-labels)): `trust` (`untrusted` for text from outside, such as a mail or a ticket's body), `level` (a name from the tenant's ordered list, such as `confidential`) and `compartments` (up to 16 names, `^[a-z0-9][a-z0-9_.-]{0,63}$`, such as a matter's ID), at most 2 KiB:

```python
return CallToolResult(
    content=[TextContent(type="text", text=body)],
    _meta={"oz0/labels": {"trust": "untrusted", "level": "confidential", "compartments": ["matter-17"]}},
)
```

The job that read the result joins these into its own labels. A level the tenant's list does not hold, an unknown key, a malformed compartment or too many withholds the result: the model reads an error, never the content. `trust: "trusted"` never lowers anything: an open-world function's results are untrusted unless the tenant vouches for the function.

The manifest says what a tool's results are when they say nothing:

```yaml
tools:
  - id: mail
    mcp: [python, -m, mail_server]
    labels:
      source: imap      # its results come from a labelled system: an unlabelled one gets the tenant's default level
      untrusted: true   # its results hold outside text, though it stays inside the tenant
```

A plugin that maps a source's own labels (a document store's sensitivity labels, say) to the tenant's list declares a `source` and sets `level` on each result. The platform takes your plugin's word for what it declares; a tool that under-labels lowers what its tenant's rules can protect.

## Where arguments came from

Mark the arguments a person should not have to take on trust, and say where their values must come from. In a function's input schema, `x-oz0-role` names what an argument is (`recipient`, `amount`, `host`, `path`, `payee` or `repository`; `format: email` counts as a recipient, `hostname`, `uri` and `url` as a host), and `x-oz0-origin` the floor its value's origin must reach: `request` (what the person asked), `identity` (the starter themselves), `record` (a tenant record) or `any` (anything the job read, but never a value the model made up). With FastMCP and Pydantic:

```python
from typing import Annotated
from pydantic import Field

TO = Field(json_schema_extra={"x-oz0-role": "recipient", "x-oz0-origin": "record"})


@server.tool(annotations=ToolAnnotations(destructive_hint=False, open_world_hint=True))
def send(to: Annotated[str, TO], body: str) -> str:
    return mail.send(to, body)
```

Before a call of `send`, the platform checks `to` against where the job's values came from, with no model in the decision: an address the person typed, their own, or one a vouched-for record returned passes; one only a web page named asks a person, and the card says why ([Tool policy](https://docs.oz0.ai/govern/tool-policy/origins-and-sequences#where-arguments-came-from)). A list's items are checked one by one. A function that returns tenant records, such as a CRM lookup, says so at its input schema's root:

```json
{"type": "object", "x-oz0-origin-source": "record", "properties": {"name": {"type": "string"}}}
```

Its results count as records only once a tenant admin vouches for it (`oz0 tenant tool-policy set --origin-record crm.lookup`): a plugin cannot raise its own results' rank. `plugin lint` checks the keywords in your [measured listing](https://docs.oz0.ai/build/parts/tools/snapshots#the-measured-listing): a floor's value, a floor without a role, and the record source's one value. For a list of values (`type: array`), the role and the floor may sit on the property or on its `items`, as in a list of recipients with `x-oz0-origin` on the list and `format: email` on its items; every value of the list is checked.

They are read through the schema an optional or a union argument takes too: the branches of `anyOf`, `oneOf` and `allOf` (a branch that is only `null` names nothing, as in Pydantic's `Optional[list[str]]`) and a local `$ref` such as `#/$defs/Address`, at most eight schemas deep. The property's own role wins; branches that name different roles, and a `$ref` that cannot be followed or goes round, mark the argument as one checked as an exact string, never as one left unchecked, and `plugin lint` says why.

## Approvals

`requires_approval` lists functions that pause the job until a person approves:

```yaml
- id: acme_github
    mcp: ["python", "-m", "acme_github.mcp"]
    requires_approval: [acme_github.merge]
```

Every call to them waits for an admin to approve or deny it in the console or with `oz0 approval`; a denied call is not made, and the model is told why. See [Approvals](https://docs.oz0.ai/agents/approvals).


## Sitemap

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