Skip to content

Guardrails that forbid destructive tool calls#

You will add guardrails to a workspace so that no agent — whoever wrote it, whatever its policy says — can delete, drop, pay, transfer or send through its tools, merge a pull request, or write anything outside office hours. Guardrails only forbid, and a forbid beats every permit, so no agent's policy can lift them.

Before you begin#

  • The workspace's admin role.
  • Know your tools' names: guardrails can match MCP tools by name (resource.name), HTTP requests by method, host and path, and the provider's own tools.

1. Write the guardrails#

no-destruction.cedar
// MCP tools whose names say they destroy, send or pay.
@id("no-destructive-tools")
@reason("tools that delete, send or pay are not allowed")
forbid (principal, action == Action::"tools/call", resource)
when {
  resource.name like "*delete*" || resource.name like "*remove*" || resource.name like "*drop*" ||
  resource.name like "*destroy*" || resource.name like "*purge*" || resource.name like "*truncate*" ||
  resource.name like "*send*" || resource.name like "*pay*" || resource.name like "*transfer*"
};

// No HTTP DELETE on any tool or page.
@id("no-http-delete")
@reason("deleting is for people")
forbid (principal, action == Action::"http", resource)
when { resource.method == "DELETE" };

// No merging, by MCP tool or REST.
@id("no-merges")
@reason("merging is for people")
forbid (principal, action == Action::"tools/call", resource)
when { resource.name like "*merge*" };

@id("no-rest-merges")
@reason("merging is for people")
forbid (principal, action == Action::"http", resource)
when { resource.host == "api.github.com" && resource.method == "PUT" && resource.path like "/repos/*/pulls/*/merge" };

// No code run on the model provider's side.
@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_*" };
office-hours.cedar
// Outside 07:00–19:00 UTC on weekdays, HTTP tools may only be read.
@id("office-hours")
@reason("writes wait for office hours")
forbid (principal, action == Action::"http", resource)
when { !(["GET", "HEAD"].contains(resource.method)) && (context.weekday > 5 || context.hour < 7 || context.hour >= 19) };

If one agent must merge with a person's approval — the pull-request reviewer — leave out no-merges and use that recipe's narrower guardrail, which exempts it.

Matching by name catches tools you add later whose names follow the same words — and can catch an innocent one (list_removed_items). Test before you rely on it (step 3).

2. Add them to the workspace#

  1. Anemoi → Guardrails → New guardrail.
  2. Name no-destruction, a Description, and the first text — or press Examples and start from No deleting, No merging pull requests, No code run by the model's provider.
  3. Create. Repeat for office-hours (example Writes in office hours only).
$ for g in no-destruction office-hours; do
    jq -n --arg n "$g" --rawfile t "$g.cedar" '{metadata: {name: $n}, spec: {text: $t}}' \
      | curl -sS -X POST "$WS/agent-guardrails" -H "Authorization: Bearer $ASTRA_TOKEN" \
          -H "Content-Type: application/json" -d @- | jq -r .metadata.name
  done
no-destruction
office-hours

A text with a permit is refused (a guardrail may only forbid).

3. Test them against an agent's policy#

A guardrail applies when a run is made; to see the combined decision before that, test the agent's policy with the guardrails appended:

$ curl -sS "$WS/agents/pr-reviewer" -H "Authorization: Bearer $ASTRA_TOKEN" | jq -r .spec.policy.tools > agent.cedar
$ cat agent.cedar no-destruction.cedar office-hours.cedar > combined.cedar
$ jq -n --rawfile p combined.cedar '{tools: $p, tests: [
    {server: "github", tool: "merge_pull_request"},
    {server: "github", tool: "delete_file"},
    {server: "github", tool: "pull_request_read"}]}' \
  | curl -sS -X POST "$WS/agent-policy-checks" -H "Authorization: Bearer $ASTRA_TOKEN" \
      -H "Content-Type: application/json" -d @- | jq -c '.tests[] | {decision, reason}'
{"decision":"deny","reason":"merging is for people (no-merges)"}
{"decision":"deny","reason":"tools that delete, send or pay are not allowed (no-destructive-tools)"}
{"decision":"allow","reason":"permitted by github-reads"}

The console's Test a call on an agent decides by the agent's own policy only.

4. See them work#

Runs made from now on carry the guardrails; runs under way keep the policy they started with. In a new run's Trace, a refused call shows the guardrail's rule and reason, and the agent is told merging is for people (no-merges). Each run's Receipt names the guardrails it ran under, and its policy digest covers them.

For every workspace#

To hold every workspace of the organisation, an organisation owner or admin writes the same text as an organisation guardrail; it appears in each workspace as org-<name>, read-only. See Agent governance.