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
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
Acap 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:
- A
safetyrule oncontentthat setsanswer_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. - Otherwise the agent’s own
answer_checksetting (PATCH /v1/agents/{id}with{"answer_check": {"language": "ru"}}, or the agent’s edit form). Anyone who can edit the agent can set it;nullleaves the check off.
{enabled?: bool = true, language?: string, rewrite_instruction?: string (≤4000)}. A rule on the agent:
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 onregistry: 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 fromconfig/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:Related
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