Cedar reference#
Each tool call an agent run makes is decided on its machine by a Cedar request against the run's tool policy (the version's, with the workspace's and organisation's guardrails appended). This page lists every name a policy can use, the annotations Anemoi reads, how a decision is made, and examples.
The schema#
entity Server = { name: String, kind: String };
entity Agent = { namespace: String, agent: String, version: Long, run: String, worker: String, act: String };
entity Tool in [Server] = { server: String, name: String };
entity Url in [Server] = { server: String, method: String, host: String, path: String };
entity ServerTool = { api: String, tool: String };
action "tools/call" appliesTo { principal: Agent, resource: Tool,
context: { phase: String, time: Long, hour: Long, weekday: Long } };
action "http" appliesTo { principal: Agent, resource: Url,
context: { phase: String, time: Long, hour: Long, weekday: Long, query: String } };
action "model/server_tool" appliesTo { principal: Agent, resource: ServerTool,
context: { phase: String, time: Long, hour: Long, weekday: Long } };
Principal#
Agent::"<namespace>/<agent>@<version>" — the agent version the run was
made from.
| Attribute | Type | Value |
|---|---|---|
namespace |
String | The workspace's namespace on the cluster. |
agent |
String | The agent's name. |
version |
Long | The version. |
run |
String | The run's name. |
worker |
String | The run's worker (<run>-0). |
act |
String | Who started the run: user:<id>, or what started it. |
Actions and resources#
| Action | When | Resource | Attributes |
|---|---|---|---|
Action::"tools/call" |
An MCP tool is called (and, phase list, when tools/list is filtered). |
Tool::"<server>/<tool>", in Server::"<server>" |
server, name |
Action::"http" |
A request to an HTTP tool, or a page through a web tool. | Url::"<host><path>", in Server::"<server>" |
server, method (upper case), host (no port), path (from /, without the query) |
Action::"model/server_tool" |
A model request offers a tool the provider runs on its side. | ServerTool::"<api>/<tool>" (in no server) |
api (anthropic, openai), tool (web_search, web_fetch, code_execution…, the provider's type without its version date) |
Server::"<name>" has name and kind (mcp, http, model); a web
tool's server kind is http. resource in Server::"github" matches the
tools and URLs of the tool named github.
Context#
| Key | Type | Value |
|---|---|---|
phase |
String | call, or list when deciding whether tools/list shows a tool. |
time |
Long | Unix seconds. |
hour |
Long | 0–23, UTC. |
weekday |
Long | 1 (Monday) to 7 (Sunday), UTC. |
args |
record | The call's arguments (an MCP tool's), or, for a provider's tool, its definition in the request (type, max_uses, allowed_domains…). Not in the schema: reading it is reported as a warning, and is fine. |
query |
String | http only: the URL's query string. |
Arguments are bounded: 6 levels deep, 64 items or fields each, strings cut
at 4 096 characters; integers stay integers, other numbers become strings,
nulls are left out, and keys starting with __ are dropped (an argument
cannot name an entity).
Annotations#
| Annotation | On | Meaning |
|---|---|---|
@id("name") |
any | The rule's name in traces, receipts, approvals and refusals. Without it: policy0, policy1… |
@reason("text") |
forbid |
What the agent is told when it denies: text (rule). Without it: forbidden by rule. |
@approval |
permit |
The calls it permits wait for a person; the workspace's editors decide. |
@approval("user:<id>, role:admin") |
permit |
Who decides: user:<id>, role:admin, role:editor, role:viewer, group:<name> (matches nobody yet); wait=1h among them sets the wait. |
@approval_wait("1h") |
permit with @approval |
How long the call is held: 90, 90s, 10m, 1h, 2d. 10 minutes by default, at most a day. |
Approvers that name nobody and waits that do not parse are warnings when the policy is written.
How a call is decided#
- A call that no
permitmatches is denied: no policy permits it. - A call any
forbidmatches is denied, whatever permits it. The reason is the first@reasonamong the matching rules. - A call permitted only by rules marked
@approvalrequires a person's approval. If apermitwithout@approvalalso matches, it is allowed. - Otherwise it is allowed, and the trace names the permitting rules.
A rule that errors while it is evaluated — reading an argument the call
does not have — does not apply. A forbid that reads context.args
must be guarded with has, or it lets calls without that argument through.
tools/list shows a tool when a call to it with no arguments, in phase
list, would be allowed.
A provider tool the policy does not permit is removed from the model
request; the model goes on without it. @approval on such a rule removes
it too, for now.
Not supported: policy templates (?principal, ?resource). A policy is
at most 64 KiB, with the guardrails appended.
Examples#
@id("api-reads")
permit (principal, action == Action::"http", resource in Server::"api")
when { ["GET", "HEAD", "OPTIONS"].contains(resource.method) };
@id("fs-read")
permit (principal, action == Action::"tools/call", resource in Server::"filesystem")
when { ["read_text_file", "list_directory", "search_files"].contains(resource.name) };
@id("fs-under-work")
@reason("files outside /work are off limits")
forbid (principal, action == Action::"tools/call", resource in Server::"filesystem")
when { context.args has path && !(context.args.path like "/work/*") };
@id("tickets-in-project")
permit (principal, action == Action::"http", resource in Server::"tickets")
when { resource.path like "/projects/OPS/*" };
@id("no-keys-in-urls")
@reason("no keys or tokens in URLs")
forbid (principal, action == Action::"http", resource)
when { context.query like "*key=*" || context.query like "*token=*" };
@id("business-hours")
@reason("tools only Monday to Friday, 12:00–21:00 UTC")
forbid (principal, action, resource)
when { context.weekday > 5 || context.hour < 12 || context.hour >= 21 };
@id("writes-only-by-ada")
@reason("only Ada's runs write")
forbid (principal, action == Action::"http", resource in Server::"github")
when { !(["GET", "HEAD"].contains(resource.method)) && principal.act != "user:0192f3a0-…" };
@id("stripe-off")
@reason("stripe is off")
forbid (principal, action, resource in Server::"stripe");
@id("model-web-search")
permit (principal, action == Action::"model/server_tool", resource)
when { ["web_search", "web_fetch"].contains(resource.tool) };
@id("no-provider-code-execution")
@reason("code runs in the sandbox, not at the provider")
forbid (principal, action == Action::"model/server_tool", resource)
when { resource.tool like "code_*" };
@id("web-docs-only")
permit (principal, action == Action::"http", resource in Server::"web")
when { ["GET", "HEAD"].contains(resource.method) && (resource.host == "docs.example.com" || resource.host like "*.docs.example.com") };
The console's Examples hold these and more, with the values to fill in.