Policies¶
Policies are rules that define what your agent can and can't do. Instead of embedding guardrails in prompts or hardcoding logic in your agent scaffold, you write policies in a dedicated language and enforce them from outside the agent. This is policy-as-code, and it's how you get reliable, auditable control over agent behavior.
Why Policy-as-Code?¶
Prompt-based guardrails are fragile. They can be bypassed by prompt injection, ignored by the model, or forgotten when you update the system prompt. Hardcoded rules are tedious: every agent needs different logic, and updates mean changing code everywhere.
Policies solve both problems:
| Approach | Problem | Policy-as-code solution |
|---|---|---|
| Prompts | Can be bypassed or ignored | Policies enforce from outside the agent |
| Hardcoded rules | Tedious, per-agent, scattered | Policies are centralized and reusable |
| Ad-hoc checks | Inconsistent, hard to audit | Policies are versioned and auditable |
With policies, you define rules once and apply them across all your agents. Updates happen in one place. And because the harness enforces policies from outside the scaffold, they can't be bypassed by the agent itself.
Cedar: The Policy Language¶
Sondera uses Cedar, an open-source policy language created by AWS. Cedar is designed for authorization logic: it's fast, auditable, and expressive enough to handle complex rules without becoming unreadable.
For complete syntax reference, operators, and testing patterns, see Writing Policies.
A Cedar policy has three parts:
- Effect:
permit(allow) orforbid(deny) - Scope: Who (
principal), what (action), on what (resource) - Conditions: Optional
whenclause for fine-grained control
In the agent context:
| Term | What it means |
|---|---|
principal |
Your agent (the entity making requests) |
action |
A list of operations (e.g., Read, Prompt, Transfer) |
resource |
The trajectory or message being evaluated |
Here's a real example:
@id("block-dangerous-commands")
forbid(principal, action == MyAgent::Action::"Bash", resource)
when {
context has parameters_json && // parameters_json is always available as a JSON string
(context.parameters_json like "*rm -rf*" ||
context.parameters_json like "*sudo*")
};
This policy blocks any Bash command containing rm -rf or sudo. The harness evaluates it at the PRE_TOOL stage, before the command executes.
How Policies Become Decisions¶
When you call harness.adjudicate(), Cedar evaluates your policies and returns a decision: ALLOW, DENY, or ESCALATE.
Cedar evaluates in this order:
- Check forbid policies: If any
forbidmatches, then DENY (or ESCALATE if all matching forbids have@escalate) - Check permit policies: If any
permitmatches, then ALLOW - Default: If no policies match, then DENY
This is a default-deny model. Actions are blocked unless explicitly permitted. It's safer: a missing policy means "don't allow" rather than "allow everything."
// Even though this permits everything...
permit(principal, action, resource);
// ...this forbid still blocks DeleteFiles (forbid takes precedence)
forbid(principal, action == MyAgent::Action::"DeleteFiles", resource);
When a policy denies an action, you choose how to handle it: block the agent entirely, or steer it by returning the reason so it can try a different approach. For actions that need approval rather than outright denial, use the @escalate annotation to trigger an escalate decision. See Decisions for details.
What Policies Can Access¶
Policies evaluate against a context object that contains information about the current request. What's available depends on the stage:
| Stage | What you can check | Example |
|---|---|---|
PRE_MODEL / POST_MODEL |
Text content, role | resource.content like "*ignore instructions*" |
PRE_TOOL |
Tool arguments | context.parameters_json like "*command*" |
POST_TOOL |
Tool results | context.response_json like "*secret*" |
Always use has checks
Cedar produces an error if you access a field that doesn't exist. Always check before accessing. For example:
You can also access trajectory state to write policies based on what the agent has already done:
For complete stage-by-stage examples, see Stages.
Policy Patterns¶
Allow by default, block specific actions¶
// Allow everything
permit(principal, action, resource);
// Except these dangerous tools
forbid(principal, action == MyAgent::Action::"DeleteDatabase", resource);
forbid(principal, action == MyAgent::Action::"SendEmail", resource);
Block by default, allow specific actions¶
// Only allow these tools
permit(principal, action == MyAgent::Action::"Search", resource);
permit(principal, action == MyAgent::Action::"Read", resource);
// Everything else is denied by default (no permit matches)
Conditional rules¶
@id("spending-limit")
forbid(principal, action == MyAgent::Action::"Transfer", resource)
when {
context has parameters &&
context.parameters has amount &&
context.parameters.amount > 10000
};
@id("block-sensitive-paths")
forbid(principal, action == MyAgent::Action::"FileWrite", resource)
when {
context has parameters &&
context.parameters has path &&
context.parameters.path like "/etc/*"
};
Annotations¶
Policies must include an @id annotation to identify them in adjudication results. This makes it easy to see which policy allowed or denied an action.
@id("high-value-transfer-block")
forbid(principal, action == MyAgent::Action::"Transfer", resource)
when {
context has parameters &&
context.parameters has amount &&
context.parameters.amount > 100000
};
When a policy matches, its metadata is included in result.policies. See Decisions for how to access policy metadata in your code.
Local vs Platform¶
Policies can be defined locally in your code (CedarPolicyHarness) or managed centrally in Sondera Platform (SonderaRemoteHarness). Both use the same policy language and API.
Next Steps¶
- Stages: Where policies are evaluated in the agent loop
- Trajectories: How policy decisions are recorded
- Writing Policies: Complete guide to Cedar syntax and patterns