Skip to main content
One PolicyRule row expresses one governance intent for one subject. Rules are managed under /v1/workspaces/{workspace}/policies; see the generated API reference for request and response shapes.

Synopsis

Rules of one subject layer compile into a PolicyDocument. The layers resolve in the order workspace → agent → user → task into one immutable EffectivePolicy carrying source_policy_ids and resolver_version (policy-resolver-v1).

Fields

PATCH replaces params wholesale rather than merging keys.

Values

Subject types

The task layer is not a rule. It is supplied as a PolicyDocument in task_policy at task creation.

Effects

Target selectors

target parses as kind or kind:value. * parses as kind all with no value. A selector naming an unknown kind, or with an empty component, is rejected at parse time. Tool names are matched with fnmatch glob semantics against the LLM-facing tool name and the tool’s other names below. A tool that comes from a connection is allowed only by its connection’s id. Every other name it has only denies it or holds it for approval: an allow naming one of those does not admit it, and in a workspace with any allow on tools the tool is then denied. Connection names are not unique, and any workspace member can add a connection and choose its name, its server’s tool names and its OpenAPI operationIds. An allow that matched those names would admit every connection that reused them. To allow a connection tool, name it as tool:mcp:<instance id>:<tool> or tool:openapi:<connection id>:<tool>; the connection’s page shows the id. A delegate’s tool name comes from an agent name, which any member chooses and which is not unique either, so a delegate is allowed as tool:agent:<agent id>, or a remote one as tool:a2a:<agent address>.

Params by effect

Spend caps by kind

A cap on spend takes one period and at most one category, and binds to the workspace (every agent’s spend), an agent (its own) or, with client_id, a Registered Client. Every cap that matches applies. A run’s tool spend is the sum of its own task’s ledger lines, across resumed runs; a delegated agent’s calls count against its own task. A cap is reached once its sum equals it, and the call or turn that crosses it is not undone. Approver references are user:<id>, group:<id>, or a userset <type>:<id>#<relation>. A bare id is rejected at write time. Only user:<id> grants approval at resolution.

Merge and validation rules

Applied per layer in order. A lower layer may only tighten. A rejected chain raises a validation error, surfaced as HTTP 422.

Answer check

answer_check looks at an agent’s final answer before the person sees it. It has two sources, and exactly one of them decides for a run:
  1. A safety rule on content that sets answer_check, at any scope. It is an override, set by whoever can manage policies: {enabled: false} turns the check off for an agent whose own setting has it on, and a rule that turns it on replaces the agent’s language and instruction with its own.
  2. Otherwise the agent’s own answer_check setting (PATCH /v1/agents/{id} with {"answer_check": {"language": "ru"}}, or the agent’s edit form). Anyone who can edit the agent can set it; null leaves the check off.
Both take the same object, {enabled?: bool = true, language?: string, rewrite_instruction?: string (≤4000)}. A rule on the agent:
In a bundle, the same rule is a policy whose subject is the agent’s key:
With no leak nothing happens. Otherwise the workflow makes one extra call to the agent’s own model, with no tools and no streaming, asking it to fix only the leaks found (rewrite_instruction replaces the default instruction; the reply format below is appended to either). The model answers in two blocks, <changes> (what it fixed) and <answer> (the corrected answer). Only the <answer> block reaches the person, and only when there is exactly one, it is not empty and it keeps every link of the original; otherwise the original stays, minus CJK characters if those were the leak. While the check is on, the reply text is withheld from the live stream: the run’s llm.call.chunk events carry thinking but an empty chunk, so a client shows the call in progress. A text-only reply is the final answer, and it is checked before its llm.call.completed is published, so that event already carries the checked text. An answer given through the completion tool is checked when the run completes. The rewrite is metered like any model call and counts against the run’s budget.

Enforcement

A rule that parses is not necessarily a rule that acts. The compiler skips unrecognised effect and target combinations at debug log level, so the row exists and the API returns it while the runtime never sees it. Two further enforcement boundaries:

Install rules

Rules on registry: and registry_trust: decide whether a catalog item may become a workspace’s own. The check reads the enabled rules of the workspace and of the acting user, each as its own layer: A lower layer cannot widen a higher one’s allowlist: every layer must allow. registry:github is a skill imported straight from a GitHub URL, with trust community. The check runs on catalog install, on attaching a catalog skill to an agent, on editing a catalog item, on updating an installed skill, on a GitHub URL import, and when a task uses a catalog skill or agent the workspace never installed. A refusal answers 403 through the API and fails a task as non-retryable. Skills installed before a rule are not re-checked.

Defaults and overrides

New workspaces are seeded from config/default_policies.yaml. Seeding is idempotent per (target, effect, period) dimension: a dimension the workspace already has is left untouched. No approval rule is seeded.

Example

Deny one tool for one agent, and require approval on another with a named approver:
Resolved:

Authorization model

The separate resource authorization surface

Limits

The numeric ceilings and their code defaults

Errors

The validation and permission responses these rules produce

The policy engine

How the layers merge
Last modified on November 3, 2026