# Tool policy (/docs/run/tool-policy)



```ts
import { createAdmin } from "@nylorun/admin";

const admin = createAdmin();

await admin.toolPolicy.put({
  rules: [
    { effect: "ask", agent: "support", tool: "refund_order", reason: "Refunds need a person" },
    {
      effect: "deny",
      kind: "http",
      tool: "refund_*",
      when: { amount: { gt: 10000 } },
      reason: "Refunds over 10000 are refused",
    },
    { effect: "ask", server: "github", tool: "create_*" },
  ],
});
```

The tool policy decides which MCP and HTTP tool calls run, wait for a person,
or are refused. It is the Tenant's, set with a management key, and applies to
every agent, every session, and every harness at once. An agent does not
declare approvals.

`put` replaces every rule, and an empty list allows every call. To change one
rule, read the policy with `admin.toolPolicy.get()` (`{ rules, updatedAt }`),
edit the list, and write it back. A policy holds at most the Runtime limit
`toolRules` (200 by default). A selector or `when` path is at most
`toolRuleFieldChars` (200) characters, and a `reason` or `matches` expression at
most `toolRuleTextChars` (500); past any of them, `put` is `400`. See
[Runtime limits](/docs/run/limits).

In Studio, the **Tool policy** tab of the [Tenant page](/docs/run/studio#the-tenant)
lists the rules, strictest first, edits one in a panel, and adds one with
**Add rule**; it saves the policy whole, as `put` does.

From a terminal, `nylorun tenant tool-policy get` prints the policy as JSON,
and `put <file>` (or `put -` for standard input) replaces it with the file's
rules. What `get` prints, `put` takes back, so the policy can live in a
repository and be applied from CI:

```sh
npx nylorun tenant tool-policy get > tool-policy.json
npx nylorun tenant tool-policy put tool-policy.json
```

## Rules [#rules]

| Field    | Value                                                                                                                                                                                                                                                                       |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `effect` | Required. `allow`, `ask`, or `deny`.                                                                                                                                                                                                                                        |
| `tool`   | The tool's identity: an MCP tool's own name on its server (with `server`), an HTTP tool's declared `name`, or a flow HTTP stage's key. The name the model knows a tool by never matches. A `tool` containing `__` is refused with `400`. `*` matches any run of characters. |
| `server` | An MCP server's name. `*` matches any run of characters. A rule with `server` never matches an HTTP tool.                                                                                                                                                                   |
| `kind`   | `mcp` or `http`.                                                                                                                                                                                                                                                            |
| `agent`  | The agent that makes the call: for the session's own agent, its agent manifest's name (never the Agent's id, such as `users/ada/desk`); a [subagent's](/docs/build/subagents) id; or a flow agent's id for its HTTP stages. `*` matches any run of characters.              |
| `when`   | Fields of the call's input by dotted path, such as `customer.tier`, each with a test: `equals`, `in`, `gt`, `gte`, `lt`, `lte`, or `matches` (a regular expression). Every test a field sets must hold.                                                                     |
| `reason` | Shown with a held or denied call, to the person and to the model. Without one, the Runtime describes the rule.                                                                                                                                                              |

A pattern matches the whole name: `refund_*` matches `refund_order`, not
`partial_refund`. `matches` is a regular expression searched in a string
value; anchor it with `^` and `$` to match the whole value. `gt`, `gte`, `lt`,
and `lte` hold only for numbers. A `when` field the input does not have never
matches.

### Matching [#matching]

* A rule matches a call when every field it sets matches. A rule that sets only
  `effect` matches every MCP and HTTP call.
* The strictest matching rule decides, whatever the order: `deny`, then `ask`,
  then `allow`.
* A call no rule matches runs.
* A policy the Runtime cannot read refuses every call.

An MCP tool is named by `server` and `tool`, never by the `server__tool` name
the model sees. A policy stored with such a rule, written before the Runtime
refused them, cannot be read: every MCP and HTTP call is denied with a reason
naming the rule, and `admin.toolPolicy.get()` (`GET /v1/tenant/tool-policy`)
answers `409 request_rejected`, until you `put` the policy again. Rewrite each
such rule:

```json
{ "effect": "ask", "tool": "github__create_issue" }
```

becomes

```json
{ "effect": "ask", "server": "github", "tool": "create_issue" }
```

Through an MCP gateway that puts a prefix before each tool name it lists, such
as LiteLLM's `github-create_issue`, a rule still names the server's own tool
(`create_issue`) once the Agent's credential sets the gateway's `toolPrefix`.
See [Gateway tool prefix](/docs/run/credentials#gateway-tool-prefix).

### What each effect does [#what-each-effect-does]

| Effect  | The call                                                                            |
| ------- | ----------------------------------------------------------------------------------- |
| `allow` | Runs.                                                                               |
| `deny`  | Is never sent. The model gets a failed tool result, `tool.denied`, with the reason. |
| `ask`   | Is held. The turn pauses until a person approves or denies it.                      |

A held call shows as `held` in
[`session.toolCalls()`](/reference/runtime/sessions/page-a-sessions-tool-calls).
The turn pauses with `turn.paused`, which carries one `approval` interaction
per held call:

| Field            | Value                                                                      |
| ---------------- | -------------------------------------------------------------------------- |
| `id`             | The interaction to answer.                                                 |
| `prompt`         | `Approve <server> / <tool>?` for an MCP call, `Approve <tool>?` otherwise. |
| `reason`         | The rule's `reason`, or the Runtime's description of the rule.             |
| `server`, `tool` | The tool's identity, as the rule matched it.                               |
| `input`          | The call's input, as it will be sent.                                      |

The event's `toolName` stays the name the model called the tool by.

The Runtime decides as it records the call, before anything is sent, and the
gateway runs only calls the record holds running. A harness of your own cannot
skip the policy.

## Approve a held call [#approve-a-held-call]

```ts
await session.approve(interactionId, true);
```

`session.approve(interactionId, approved, { idempotencyKey? })` answers one held
call. Without an `idempotencyKey`, each call makes its own; pass one to retry
the same answer safely. The `interactionId` is the interaction's `id` in `turn.paused`
(`interactions[].interaction.id`); `session.pending()` lists the same open
interactions. Approved, the Runtime sends that exact call, with the input the
person saw. Denied (`false`), the call fails to the model as `tool.denied` and is never
sent. When every held call of the turn is answered, the turn goes on.

Nothing holds a process while a call waits: an approval can come a day later,
or after the Runtime restarts. Cancelling the turn ends each held call as
`cancelled`. See [Pauses and control](/docs/run/control).

In [Studio](/docs/run/studio), a paused session shows an approval card for each
held call, with its tool as `server / tool` (and "as `<toolName>`", the model's
name), why it is held, and its input, and **Approve** and **Deny**.

## Where it applies [#where-it-applies]

| Call                                                                                                           | Decided by the policy                                                                    |
| -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Remote MCP server tools, deferred ones run through `tool_call` included                                        | Yes                                                                                      |
| [HTTP tools](/docs/build/tools)                                                                                | Yes                                                                                      |
| A [flow's](/docs/build/flows#http-stages) HTTP stages and HTTP verifiers                                       | Yes. An `ask` pauses the flow's turn.                                                    |
| Sandbox tools (`bash`, `read`, `write`, …)                                                                     | No. Their network side is the Agent's [egress records](/docs/run/agents#egress-records). |
| The harness's own tools (`read_artifact`, `save_artifact`, `load_skill`, `read_skill_resource`, `tool_search`) | No                                                                                       |
| The Runtime's tools, `memory` and `schedule_wake`                                                              | No                                                                                       |
| A [custom harness's](/docs/build/harness#tools-of-your-own) own tools                                          | No. They run in the harness's own process.                                               |

A call a [subagent](/docs/build/subagents) makes is decided like any other,
with `agent` set to the subagent's id. `deny` fails it to the subagent's model.
`ask` holds it and pauses the parent's turn; approve it on the parent's
session. A [flow agent used as a subagent](/docs/build/subagents#flow-agents-as-subagents)
runs in its own session, and a call held there waits on that session.

## Reference [#reference]

| Call                              | Endpoint                                                                                      |
| --------------------------------- | --------------------------------------------------------------------------------------------- |
| `admin.toolPolicy.get()`          | [Get the Tenant's tool policy](/reference/management/tool-policy/get-the-tenants-tool-policy) |
| `admin.toolPolicy.put({ rules })` | [Set the Tenant's tool policy](/reference/management/tool-policy/set-the-tenants-tool-policy) |
| `session.approve()`               | [Send a command to a session](/reference/runtime/sessions/send-a-command-to-a-session)        |

## Next step [#next-step]

<Cards>
  <Card title="Pauses and control" description="Resolve pauses and cancel a turn." href="/docs/run/control" />

  <Card title="Events" description="tool.completed and turn.paused." href="/docs/run/events" />
</Cards>
