# Manage policies

> Create and update owner-scoped tool policies, interpret effective policy precedence, and handle approval pauses from the web UI, MCP, and CLI resume flow.

- 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/hosts/mcp/src/browser-approval-store.ts`

---

---
title: "Manage policies"
description: "Create and update owner-scoped tool policies, interpret effective policy precedence, and handle approval pauses from the web UI, MCP, and CLI resume flow."
---

Executor persists owner-scoped tool policies in the `tool_policy` table and resolves them on every `tools.list` and `execute` call through `resolveEffectivePolicy`. Policies are created and updated through the web UI at `/policies`, the HTTP `policies` API group, SDK `executor.policies` methods, and built-in core tools (`executor.coreTools.policies.*`). When a matched action is `require_approval`, invocation pauses until a human approves via the console `/resume/:executionId` page, the MCP `resume` tool, or `executor resume`.

## Policy actions

| Action | At list time | At invoke time |
|--------|--------------|----------------|
| `approve` | Tool appears in search and catalog | Runs without an approval prompt |
| `require_approval` | Tool appears | Pauses for human approval before running |
| `block` | Hidden from search (unless `includeBlocked`) | Fails with `ToolBlockedError` |

<Note>
The HTTP API and SDK schema use `approve`, not `allow`. User-facing labels in the web UI map `approve` to **Allow**.
</Note>

## Owner scopes

Each policy row is scoped to an `owner`:

| Owner | Web UI label | Role |
|-------|--------------|------|
| `org` | Workspace (or **Local** on single-player hosts) | Outer guardrail shared across the workspace |
| `user` | Personal | Inner preference for the signed-in member |

Org and user each contribute at most one matched rule per tool. The final action is the **most restrictive** match across owners: `block` beats `require_approval`, which beats `approve`. A user `approve` cannot weaken an org `block`; a user `require_approval` can strengthen an org `approve`.

Within a single owner, rules are ordered by `position` (fractional-indexing key). Lower lexicographic `position` means higher precedence. The first matching pattern for that owner wins.

## Pattern grammar

Policies match against the full tool address:

```text
<integration>.<owner>.<connection>.<tool>
```

Static core tools (for example `executor.coreTools.policies.list`) match on their fqid address directly.

| Pattern form | Example | Matches |
|--------------|---------|---------|
| Universal | `*` | Every tool |
| Integration subtree | `vercel.*` | Any tool under `vercel` |
| Trailing wildcard | `vercel.dns.*` | Literal prefix plus anything deeper |
| Exact | `vercel.dns.create` | One tool path |
| Mid-segment `*` | `vercel.*.*.dns.create` | Wildcards exactly one segment per `*` |

<Warning>
Invalid patterns are rejected at create time: empty strings, leading `*`, partial wildcards like `me*`, and consecutive dots are not allowed. Use `isValidPattern` (SDK) or the web UI validator before writing rules.
</Warning>

### Connection-agnostic UI patterns

The tools tree displays `integration.<tool>` without owner or connection segments. When you author a rule from a tool or category row, the UI stores a connection-wildcarded pattern via `toPolicyPattern`:

```text
integration.*.*.<tool-or-subtree>
```

A leaf rule on `records.create` becomes `myapi.*.*.records.create`; a category rule on `records` becomes `myapi.*.*.records.*`. Rules authored on one connection apply to every connection for that integration.

## Effective policy resolution

```mermaid
flowchart TD
  subgraph inputs [Inputs per tool call]
    T[Tool address]
    P[Owner-scoped policy rows]
    D[Plugin default annotation]
  end
  subgraph perOwner [Per owner]
    O1[Org: first match by position]
    O2[User: first match by position]
  end
  subgraph merge [Cross-owner merge]
    M[Most restrictive action wins]
  end
  subgraph fallback [No user match]
    F[Plugin default: requiresApproval or approve]
  end
  T --> O1
  T --> O2
  P --> O1
  P --> O2
  O1 --> M
  O2 --> M
  M -->|no match| F
  D --> F
```

When no user-authored rule matches, the executor falls back to the plugin annotation `requiresApproval`. OpenAPI maps `POST`, `PUT`, `PATCH`, and `DELETE` to `requiresApproval: true`; read-only `GET` operations default to auto-approve. MCP upstream tools inherit `requiresApproval` from `destructiveHint`.

## Manage policies in the web UI

Open **Policies** (`/policies`) to list, create, reorder, and remove rules.

<Steps>
<Step title="Add a policy">
Enter a pattern, choose an action (`Allow`, `Require approval`, or `Block`), and pick **Workspace** or **Personal** when the host exposes both owners. Click **Add policy**. New rules without an explicit position are inserted at the top of that owner's list (highest precedence).
</Step>
<Step title="Author from the tools tree">
On an integration's **Tools** tab, use **Set policy for …** on a tool row (exact rule) or category row (subtree rule). The menu previews the stored pattern before write. The tool detail header badge is the same surface: set, recognize, or clear a rule.
</Step>
<Step title="Reorder for precedence">
Use **Move up** / **Move down** on `/policies` to change `position`. More-specific patterns should sit above broader ones so a leaf rule is not shadowed by a later group rule.
</Step>
<Step title="Verify">
Confirm the rule appears under **Active policies** with the expected owner, pattern, and action. Invoke a gated tool or run `tools.list` to confirm blocked tools disappear from search.
</Step>
</Steps>

<Tip>
`usePolicyActions` auto-places new tree-authored rules below more-specific existing rules so a freshly added group rule does not silently override a leaf rule.
</Tip>

## HTTP API

| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/policies` | List all policies for the authenticated identity |
| `POST` | `/policies` | Create a policy |
| `PATCH` | `/policies/:policyId` | Update pattern, action, or position |
| `DELETE` | `/policies/:policyId` | Remove a policy |

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

<ParamField body="owner" type="string" required>
`org` or `user`.
</ParamField>

<ParamField body="pattern" type="string" required>
Tool address pattern. Must pass `isValidPattern`.
</ParamField>

<ParamField body="action" type="string" required>
`approve`, `require_approval`, or `block`.
</ParamField>

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

<ResponseField name="id" type="PolicyId">
Stable policy identifier.
</ResponseField>

<ResponseField name="owner" type="Owner">
`org` or `user`.
</ResponseField>

<ResponseField name="pattern" type="string">
Stored match pattern.
</ResponseField>

<ResponseField name="action" type="ToolPolicyAction">
`approve`, `require_approval`, or `block`.
</ResponseField>

<ResponseField name="position" type="string">
Fractional-indexing order key.
</ResponseField>

<ResponseField name="createdAt" type="number">
Unix milliseconds.
</ResponseField>

<ResponseField name="updatedAt" type="number">
Unix milliseconds.
</ResponseField>

<RequestExample>

```bash title="Create a require-approval gate"
curl -X POST "$EXECUTOR_BASE_URL/policies" \
  -H "content-type: application/json" \
  -H "authorization: Bearer $TOKEN" \
  -d '{
    "owner": "org",
    "pattern": "executor.coreTools.policies.list",
    "action": "require_approval"
  }'
```

</RequestExample>

:::

:::endpoint PATCH /policies/:policyId
Update an existing policy. The payload must include `owner` so the handler can verify partition ownership.
:::

<ParamField path="policyId" type="PolicyId" required>
Policy to update.
</ParamField>

<ParamField body="owner" type="Owner" required>
Owning partition (`org` or `user`).
</ParamField>

<ParamField body="pattern" type="string">
New pattern.
</ParamField>

<ParamField body="action" type="ToolPolicyAction">
New action.
</ParamField>

<ParamField body="position" type="string">
New order key for precedence within the owner.
</ParamField>

:::

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

<ParamField path="policyId" type="PolicyId" required>
Policy to remove.
</ParamField>

<ParamField body="owner" type="Owner" required>
Owning partition (`org` or `user`).
</ParamField>

:::

## SDK and core tools

`createExecutor` exposes `executor.policies.list`, `.create`, `.update`, `.remove`, and `.resolve(address)`.

<CodeGroup>

```typescript title="SDK (promise mode)"
const policy = await executor.policies.create({
  owner: "org",
  pattern: "myapi.*.*.records.create",
  action: "require_approval",
});

const effective = await executor.policies.resolve(
  "myapi.org.prod.records.create" as ToolAddress,
);
```

```typescript title="Core tools via execute"
const result = await tools.executor.coreTools.policies.create({
  owner: "org",
  pattern: "vercel.dns.*",
  action: "block",
});
```

</CodeGroup>

<Warning>
`policies.create`, `policies.update`, and `policies.remove` core tools carry `annotations: { requiresApproval: true }`. Mutating policies always pauses for approval so prompt-injected code cannot silently disable guardrails.
</Warning>

## Handle approval pauses

When effective policy or tool annotations require approval, `execute` pauses and returns a `PausedExecution`. Resume the pause from the console, MCP, or CLI.

### Web console

Browser approval URLs follow:

```text
<origin>/resume/<executionId>?mcp_session_id=<sessionId>
```

The `mcp_session_id` query routes MCP-session-scoped pauses to `/api/mcp-sessions/:id/executions/:executionId/resume`. Pauses without that query use the standard `/api/executions/:executionId/resume` path.

<Steps>
<Step title="Open the approval page">
Follow the `approvalUrl` from the paused MCP result or the link printed by `executor call` / `executor resume`.
</Step>
<Step title="Review the gated action">
The page shows the elicitation message, tool id, and argument preview.
</Step>
<Step title="Decide">
Click **Approve**, **Decline**, or **Cancel**. The host records `{ action, content? }` and wakes any in-flight `resume` waiter.
</Step>
</Steps>

### MCP elicitation modes

| Mode | Query | Resume surface |
|------|-------|----------------|
| `browser` (typical for human-in-the-loop) | `?elicitation_mode=browser` | `approvalUrl` plus long-poll on MCP `resume` |
| `model` (default) | absent or `?elicitation_mode=model` | MCP `resume` tool exposed to the agent |
| `native` | `?elicitation_mode=native` | Client-native elicitation only; no `resume` tool |

In `browser` mode, `BrowserApprovalStore.waitForResponse(executionId)` blocks until the console posts a decision. `recordResponse` wakes the waiter.

### CLI resume

```bash
executor resume --execution-id <id> --action accept
executor resume --execution-id <id> --action decline
executor resume --execution-id <id> --action cancel
```

Optional `--content '{"key":"value"}'` supplies form content when `action=accept`. If the resumed execution pauses again, the CLI prints the next approval URL.

`executor call`, `executor resume`, and `executor tools` auto-start the local daemon when needed.

<ResponseExample>

```text title="Paused call output"
Approval required:
http://127.0.0.1:5173/resume/exec_abc123?mcp_session_id=sess_xyz
```

</ResponseExample>

### Declined or canceled approvals

If the human declines or cancels, the engine returns `ElicitationDeclinedError` and the gated tool does not run. For browser flows, the MCP `resume` call completes with acknowledgement text (`I've denied it`, `I've canceled it`) rather than the tool result.

## Troubleshooting

<AccordionGroup>
<Accordion title="Rule exists but the wrong action applies">
Check owner precedence first (org block beats user approve), then position within the owner (lower `position` wins). A broad rule listed above a specific rule shadows the leaf.
</Accordion>
<Accordion title="UI pattern does not match invoke-time address">
Tree-authored rules store `integration.*.*.<tail>`. Compare against the full five-segment address or static fqid, not the shortened display id.
</Accordion>
<Accordion title="MCP resume hangs in browser mode">
Confirm the console approval page loaded with the same `mcp_session_id` from the paused result and that you clicked Approve or Decline. The in-process store times out after 10 minutes without a decision.
</Accordion>
<Accordion title="Policy mutation did not take effect immediately">
Policy writes are optimistic in the web UI. A failed API call rolls back the row; check network errors on create/update/remove.
</Accordion>
</AccordionGroup>

## Related pages

<CardGroup>
<Card title="Policies concept" href="/policies">
Per-tool actions, pattern matching grammar, and plugin-derived defaults.
</Card>
<Card title="Executions" href="/executions">
Paused states, `execute` and `resume` routes, and elicitation handling.
</Card>
<Card title="MCP proxy" href="/mcp-proxy">
Single MCP endpoint, elicitation modes, and shared policy enforcement.
</Card>
<Card title="HTTP API reference" href="/http-api-reference">
Full `policies` route payloads and error shapes.
</Card>
<Card title="CLI reference" href="/cli-reference">
`executor resume`, `executor call`, and auto-start behavior.
</Card>
</CardGroup>
