# Policies

> Per-tool allow, require-approval, and block actions; pattern matching, effective policy resolution, and default annotations derived from integration specs.

- Repository: RhysSullivan/executor
- GitHub: https://github.com/RhysSullivan/executor
- Human docs: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052
- Complete Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/llms-full.txt

## Source Files

- `apps/docs/concepts/policies.mdx`
- `packages/core/api/src/policies/api.ts`
- `packages/core/api/src/handlers/policies.ts`
- `packages/core/sdk/src/policies.ts`
- `packages/hosts/mcp/src/browser-approval.ts`
- `packages/core/sdk/src/errors.ts`

---

---
title: "Policies"
description: "Per-tool allow, require-approval, and block actions; pattern matching, effective policy resolution, and default annotations derived from integration specs."
---

Executor gates every tool call through a layered policy model: user-authored rules in the `tool_policy` table, plugin-derived `requiresApproval` annotations on persisted tool rows, and runtime enforcement in `tools.list` and `execute`. Rules are owner-scoped (`org` or `user`), matched by dotted patterns against tool identity, and resolved to the most restrictive action across owners.

## Policy actions

Each rule assigns one of three actions to matching tools.

| Action | SDK value | Runtime behavior |
|--------|-----------|------------------|
| Always run | `approve` | Tool invokes without an approval prompt, even when the plugin marks `requiresApproval: true`. |
| Require approval | `require_approval` | Execution pauses for human approval before the handler runs. |
| Block | `block` | Tool is hidden from `tools.list` by default; direct `execute` returns `ToolBlockedError`. |

Restriction rank (highest wins when owners disagree): `block` (3) > `require_approval` (2) > `approve` (1).

<Note>
The web UI labels `approve` as **Always run** and `block` as **Block** in menus, or **Blocked** when showing current state.
</Note>

## Owner scope and precedence

Policies are v2 owner-scoped rows, not legacy scope stacks. Each row carries `owner` (`org` | `user`), a `pattern`, an `action`, and a fractional-index `position` (lower lex order = higher precedence within that owner).

Resolution proceeds in two stages:

1. **Per owner:** walk rules sorted by `position` (then `id` for ties). The first pattern that matches the tool wins for that owner.
2. **Across owners:** compare each owner's winning match and keep the most restrictive action. Org rules are the outer guardrail; a user `approve` cannot weaken an org `block`, but a user `require_approval` can strengthen an org `approve`.

```mermaid
flowchart TD
  subgraph inputs [Inputs]
    P[tool_policy rows]
    A[tool.annotations.requiresApproval]
    T[tool id for matching]
  end

  subgraph perOwner [Per-owner match]
    S[Sort by owner rank then position]
    M[First matching pattern per owner]
  end

  subgraph resolve [Effective policy]
    R[Most restrictive action wins]
    F{User rule matched?}
    U[user source + pattern + policyId]
    D[plugin-default source]
  end

  P --> S --> M --> R
  T --> M
  R --> F
  F -->|yes| U
  F -->|no| D
  A --> D
```

<ParamField body="owner" type="org | user" required>
Who owns the rule. `org` applies tenant-wide; `user` applies to the bound subject only. Creating `owner: "user"` rules requires an executor with a user subject.
</ParamField>

## Pattern matching

Patterns match against the tool identity string `<integration>.<owner>.<connection>.<tool>`. Static tools (for example `executor.coreTools.*`) match against the full `ToolAddress` instead.

| Pattern form | Example | Matches |
|--------------|---------|---------|
| Universal | `*` | Every tool id |
| Exact | `vercel.org.main.deploy` | Only that four-segment id |
| Plugin-wide | `vercel.*` | Any tool under `vercel` |
| Subtree (trailing `*`) | `vercel.dns.*` | `vercel.dns.create`, `vercel.dns.zones.list`, etc. |
| Mid-segment `*` | `github.*.*.repos.list` | Wildcards exactly one segment per `*`; targets a tool name across all connections |

Validation (`isValidPattern`) rejects empty patterns, leading/trailing dots, `..`, leading `*` (except bare `*`), and partial wildcards like `me*` or `a.b*`.

<Info>
The web UI authors patterns from display ids like `integration.tool`. It expands them to `integration.*.*.tool` so a rule applies across all connections. Patterns that already end in `*` (`integration.*`, `*`) pass through unchanged.
</Info>

### Pattern examples

```text
vercel.dns.create          → exact match only
vercel.dns.*               → vercel.dns.create, vercel.dns.delete, vercel.dns.zones.list
vercel.*                   → any vercel tool at any depth
github.*.*.repos.list      → github.org.acme.repos.list, github.user.alice.repos.list
*                          → everything
```

## Plugin default annotations

When no user rule matches, Executor falls back to the tool row's `annotations.requiresApproval`. Plugins stamp these defaults when tools are produced or refreshed.

| Plugin | Default `requiresApproval` | `approvalDescription` |
|--------|---------------------------|----------------------|
| OpenAPI | `true` for `POST`, `PUT`, `PATCH`, `DELETE` operations | `"METHOD /pathTemplate"` |
| GraphQL | `true` for mutation bindings | `"mutation fieldName"` |
| MCP | `true` when upstream `destructiveHint` is set | upstream tool title or name |

The effective policy source is `plugin-default` in this case. `approve` from the plugin default means the tool runs without prompting; `require_approval` triggers elicitation.

User rules with source `user` always override plugin defaults when they match, regardless of annotation.

## Enforcement surfaces

### `tools.list`

By default, blocked tools are omitted. Pass `includeBlocked=true` on the HTTP query or SDK filter to surface them for admin views.

The list response includes `requiresApproval` and `approvalDescription` from plugin annotations. The UI combines stored policies with annotations via `effectivePolicyFromSorted` to show the effective action per tool.

### `execute`

On every invocation, Executor resolves the effective policy, then:

| Effective action | Behavior |
|------------------|----------|
| `block` | Returns `ToolBlockedError` with `address` and matched `pattern`. |
| `approve` | Skips approval even if `requiresApproval` is set. |
| `require_approval` | Fires elicitation before the handler. |
| No user rule + `requiresApproval: true` | Fires elicitation using `approvalDescription` when present. |
| No user rule + no annotation | Runs immediately. |

Declined or cancelled approval returns `ElicitationDeclinedError`.

### MCP approval flow

When a gated call pauses, MCP hosts can surface browser approval. With `?elicitation_mode=browser`, the host returns an `approvalUrl` pointing at `/resume/<executionId>`. The model calls the `resume` tool to wait for the human decision. Modes `model` (default) and `native` keep approval inline with the agent.

## Data model

Policies persist in the `tool_policy` table (SQLite locally, Postgres in cloud). Rows are partitioned by tenant and owner policy, same as connections.

<ResponseField name="id" type="PolicyId">
Stable rule identifier (`pol_…`).
</ResponseField>

<ResponseField name="owner" type="org | user">
Rule owner partition.
</ResponseField>

<ResponseField name="pattern" type="string">
Dotted match pattern (see grammar above).
</ResponseField>

<ResponseField name="action" type="approve | require_approval | block">
Enforcement action.
</ResponseField>

<ResponseField name="position" type="string">
Fractional-index sort key. New rules without an explicit position are inserted above the current minimum (highest precedence).
</ResponseField>

## HTTP API

:::endpoint GET /policies
List all tool policies visible to the authenticated identity. Returns an array of policy objects with epoch-millisecond `createdAt` and `updatedAt`.
:::

:::endpoint POST /policies
Create an owner-scoped policy.

<RequestExample>
```json title="Create policy"
{
  "owner": "org",
  "pattern": "vercel.dns.*",
  "action": "require_approval"
}
```
</RequestExample>

<ParamField body="owner" type="org | user" required>
Owner partition for the new rule.
</ParamField>

<ParamField body="pattern" type="string" required>
Valid match pattern.
</ParamField>

<ParamField body="action" type="approve | require_approval | block" required>
Enforcement action.
</ParamField>

<ParamField body="position" type="string">
Optional fractional-index position. Omit to insert at the top of the owner's list.
</ParamField>
:::

:::endpoint PATCH /policies/:policyId
Update pattern, action, or position. Payload must include `owner` to identify the partition.

```json title="Update action"
{
  "owner": "org",
  "action": "block"
}
```
:::

:::endpoint DELETE /policies/:policyId
Remove a policy. Payload must include `owner`.

```json title="Remove policy"
{
  "owner": "org"
}
```

Returns `{ "removed": true }`.
:::

## SDK surface

The `executor.policies` namespace mirrors the HTTP API and adds resolution:

```typescript title="Policy CRUD and resolve"
const rules = await executor.policies.list();

const created = await executor.policies.create({
  owner: "org",
  pattern: "github.*.*.repos.delete",
  action: "block",
});

await executor.policies.update({
  id: String(created.id),
  owner: "org",
  action: "require_approval",
});

await executor.policies.remove({ id: String(created.id), owner: "org" });

const effective = await executor.policies.resolve(
  ToolAddress.make("tools.github.org.main.repos.delete"),
);
// { action, source, pattern?, policyId? }
```

Pure helpers exported from `@executor-js/sdk` (and `@executor-js/sdk/shared`) support UI and tests without a live executor:

- `matchPattern(pattern, toolId)`
- `isValidPattern(pattern)`
- `resolveToolPolicy(toolId, policies, ownerRank)`
- `resolveEffectivePolicy(toolId, policies, ownerRank, defaultRequiresApproval?)`
- `effectivePolicyFromSorted(toolId, sortedPolicies, defaultRequiresApproval?)`

## Resolution examples

<AccordionGroup>
<Accordion title="Org block overrides user approve">
Given org rule `vercel.*` → `block` and user rule `vercel.dns.create` → `approve`, calling `vercel.dns.create` is blocked. The org guardrail wins.
</Accordion>

<Accordion title="User require_approval strengthens org approve">
Given org `vercel.*` → `approve` and user `vercel.dns.create` → `require_approval`, that tool pauses for approval.
</Accordion>

<Accordion title="Position precedence within one owner">
Given `vercel.dns.create` → `approve` at position `a0` and `vercel.dns.*` → `require_approval` at `a1`, `vercel.dns.create` matches the more specific rule first and runs without approval.
</Accordion>

<Accordion title="Plugin default when no rule matches">
With no user rules, a tool annotated `requiresApproval: true` (for example an OpenAPI `DELETE`) pauses for approval. A `GET` tool with no annotation runs immediately.
</Accordion>

<Accordion title="Explicit approve overrides plugin default">
A user rule `vercel.*.*.delete` → `approve` suppresses the plugin's `requiresApproval` on the delete tool.
</Accordion>
</AccordionGroup>

## Errors

| Error | When |
|-------|------|
| `ToolBlockedError` | `execute` on a tool whose effective action is `block`. Message includes the matched pattern. |
| `ElicitationDeclinedError` | User declined or cancelled an approval prompt. |
| `StorageError` | Invalid pattern or action on create/update; `owner: "user"` without a bound subject; policy not found on update. |

MCP tool dispatch maps `ToolBlockedError` to code `tool_blocked` in the sandbox result.

## Related pages

<CardGroup>
<Card title="Manage policies" href="/manage-policies">
Create and update owner-scoped rules from the web UI, CLI, and HTTP API; handle approval pauses end to end.
</Card>
<Card title="Executions" href="/executions">
Paused states, `execute` and `resume` routes, and MCP elicitation handling when approval is required.
</Card>
<Card title="Tools" href="/tools">
Tool addresses, discovery, and how `includeBlocked` surfaces gated tools.
</Card>
<Card title="MCP proxy" href="/mcp-proxy">
Single MCP endpoint with shared auth and per-tool policies across integrations.
</Card>
<Card title="HTTP API reference" href="/http-api-reference">
Full `policies` route payloads and error shapes.
</Card>
<Card title="SDK reference" href="/sdk-reference">
`executor.policies` helpers and typed policy exports.
</Card>
</CardGroup>
