Skip to content

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#

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#

  1. A call that no permit matches is denied: no policy permits it.
  2. A call any forbid matches is denied, whatever permits it. The reason is the first @reason among the matching rules.
  3. A call permitted only by rules marked @approval requires a person's approval. If a permit without @approval also matches, it is allowed.
  4. 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#

Read-only on one HTTP tool
@id("api-reads")
permit (principal, action == Action::"http", resource in Server::"api")
when { ["GET", "HEAD", "OPTIONS"].contains(resource.method) };
Named tools of one MCP server
@id("fs-read")
permit (principal, action == Action::"tools/call", resource in Server::"filesystem")
when { ["read_text_file", "list_directory", "search_files"].contains(resource.name) };
Files only under /work
@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/*") };
Only some paths, any method
@id("tickets-in-project")
permit (principal, action == Action::"http", resource in Server::"tickets")
when { resource.path like "/projects/OPS/*" };
No keys in URLs
@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=*" };
Only in business hours (UTC)
@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 };
Only one person's runs may write
@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-…" };
Turn one tool off
@id("stripe-off")
@reason("stripe is off")
forbid (principal, action, resource in Server::"stripe");
The provider may search, never run code
@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_*" };
Web browsing: one domain only
@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.