Skip to content

Cedar Authorization

Cedar Authorization evaluates Cedar policies before each tool call, giving you declarative, identity-aware access control over agent behavior. It ships as a vended intervention in the TypeScript SDK.

The handler sits at the tool-call boundary. When the agent attempts to invoke a tool, Cedar Authorization maps the call to a Cedar authorization request and evaluates your policies. If no permit statement matches, the tool call is denied and the agent receives feedback explaining the denial.

Cedar conceptMaps toExample
PrincipalUser identityUser::"alice@acme.com"
ActionTool nameAction::"search"
ResourceStatic (unconstrained)Resource::"agent"
Context.inputTool arguments{ query: "quarterly report" }
Context.sessionInvocation metadata{ hour_utc: 14, call_count: 3, role: "admin" }

The design is fail-closed: if principal identity cannot be resolved, all tool calls are denied.

Permit specific tools and deny everything else:

import { Agent, tool } from '@strands-agents/sdk'
import { CedarAuthorization } from '@strands-agents/sdk/vended-interventions/cedar'
import { z } from 'zod'
const searchTool = tool({
name: 'search',
description: 'Search for information',
inputSchema: z.object({ query: z.string() }),
callback: (input) => `Results for: ${input.query}`,
})
const deleteTool = tool({
name: 'delete_record',
description: 'Delete a record by ID',
inputSchema: z.object({ record_id: z.string() }),
callback: (input) => `Deleted ${input.record_id}`,
})
const cedar = new CedarAuthorization({
policies: `
permit(principal, action == Action::"search", resource);
`,
})
const agent = new Agent({
tools: [searchTool, deleteTool],
interventions: [cedar],
})
await agent.invoke('Search for quarterly reports then delete record 42')
// If the agent calls search, it's permitted; a delete_record call is denied (no matching permit)

Cedar uses default-deny semantics. Tools without a matching permit statement are automatically blocked.

For multi-tenant agents where each request carries user identity, use principalResolver to extract the principal from invocationState and contextEnricher to forward role information into Cedar context:

import { Agent, tool } from '@strands-agents/sdk'
import { CedarAuthorization } from '@strands-agents/sdk/vended-interventions/cedar'
import { z } from 'zod'
const searchTool = tool({
name: 'search',
description: 'Search for information',
inputSchema: z.object({ query: z.string() }),
callback: (input) => `Results for: ${input.query}`,
})
const deleteTool = tool({
name: 'delete_record',
description: 'Delete a record by ID',
inputSchema: z.object({ record_id: z.string() }),
callback: (input) => `Deleted ${input.record_id}`,
})
const cedar = new CedarAuthorization({
policies: `
permit(principal, action, resource)
when { context.session.role == "admin" };
permit(principal, action == Action::"search", resource)
when { context.session.role == "analyst" };
`,
principalResolver: (state) => {
if (!state.user_id) return undefined
return { type: 'User', id: String(state.user_id) }
},
contextEnricher: ({ invocationState }) => ({
role: String(invocationState.role ?? 'none'),
}),
})
const agent = new Agent({
tools: [searchTool, deleteTool],
interventions: [cedar],
})
// admin can use any tool
await agent.invoke('Delete record 42', {
invocationState: { user_id: 'alice', role: 'admin' },
})
// analyst can only search
await agent.invoke('Delete record 42', {
invocationState: { user_id: 'bob', role: 'analyst' },
})
// denied: no permit matches for delete_record with role "analyst"

When principalResolver returns undefined (no identity found), the handler denies all tool calls for that request.

Cedar policies can reference context.session.call_count, which tracks how many times each tool has been invoked successfully during the session:

import { Agent, tool } from '@strands-agents/sdk'
import { CedarAuthorization } from '@strands-agents/sdk/vended-interventions/cedar'
import { z } from 'zod'
const sendEmailTool = tool({
name: 'send_email',
description: 'Send an email',
inputSchema: z.object({ to: z.string(), body: z.string() }),
callback: (input) => `Sent to ${input.to}`,
})
const searchTool = tool({
name: 'search',
description: 'Search for information',
inputSchema: z.object({ query: z.string() }),
callback: (input) => `Results for: ${input.query}`,
})
const cedar = new CedarAuthorization({
policies: `
permit(principal, action == Action::"send_email", resource)
when { context.session.call_count < 5 };
permit(principal, action == Action::"search", resource);
`,
})
const agent = new Agent({
tools: [sendEmailTool, searchTool],
interventions: [cedar],
})
// send_email is permitted for the first 4 calls, then denied on the 5th
// search is unlimited

Call counts persist with the agent’s appState and survive session reloads. Only successful tool calls increment the counter.

Pass your tools array to catch policy typos at construction time. The handler generates a Cedar schema from tool definitions and validates policies against it:

import { Agent, tool } from '@strands-agents/sdk'
import { CedarAuthorization } from '@strands-agents/sdk/vended-interventions/cedar'
import { z } from 'zod'
const searchTool = tool({
name: 'search',
description: 'Search for information',
inputSchema: z.object({ query: z.string() }),
callback: (input) => `Results for: ${input.query}`,
})
const deleteTool = tool({
name: 'delete_record',
description: 'Delete a record by ID',
inputSchema: z.object({ record_id: z.string() }),
callback: (input) => `Deleted ${input.record_id}`,
})
const cedar = new CedarAuthorization({
policies: `
permit(principal, action == Action::"search", resource);
permit(principal, action == Action::"delete_record", resource)
when { context.session.role == "admin" };
`,
tools: [searchTool, deleteTool],
contextEnricher: ({ invocationState }) => ({
role: String(invocationState.role ?? 'none'),
}),
})
// If a policy references Action::"deleet_record" (typo),
// construction throws a "Cedar policy validation failed" error naming the unrecognized action

Schema validation integrates with the cedar-for-agents ecosystem via @cedar-policy/mcp-schema-generator-wasm.

Block tools based on deployment context by forwarding environment metadata through contextEnricher:

import { Agent, tool } from '@strands-agents/sdk'
import { CedarAuthorization } from '@strands-agents/sdk/vended-interventions/cedar'
import { z } from 'zod'
const deployTool = tool({
name: 'deploy',
description: 'Deploy the service',
inputSchema: z.object({ version: z.string() }),
callback: (input) => `Deployed ${input.version}`,
})
const cedar = new CedarAuthorization({
policies: `
permit(principal, action == Action::"deploy", resource)
when { context.session has environment &&
context.session.environment != "production" };
`,
contextEnricher: ({ invocationState }) => ({
environment: String(invocationState.environment ?? 'unknown'),
}),
})
const agent = new Agent({
tools: [deployTool],
interventions: [cedar],
})
// works in staging
await agent.invoke('Deploy the service', {
invocationState: { environment: 'staging' },
})
// denied in production
await agent.invoke('Deploy the service', {
invocationState: { environment: 'production' },
})

For production deployments, keep policies in .cedar files rather than inline strings:

import { CedarAuthorization } from '@strands-agents/sdk/vended-interventions/cedar'
const cedar = new CedarAuthorization({
policies: './policies/agent.cedar',
entities: './policies/entities.json',
})

The handler reads and parses files at construction time. Invalid syntax throws immediately.

Update policies without restarting your agent process:

import { Agent, tool } from '@strands-agents/sdk'
import { CedarAuthorization } from '@strands-agents/sdk/vended-interventions/cedar'
import { z } from 'zod'
const searchTool = tool({
name: 'search',
description: 'Search for information',
inputSchema: z.object({ query: z.string() }),
callback: (input) => `Results for: ${input.query}`,
})
const cedar = new CedarAuthorization({
policies: './policies/agent.cedar',
})
const agent = new Agent({
tools: [searchTool],
interventions: [cedar],
})
// After editing agent.cedar on disk:
cedar.reload()
// Validates new policies before applying. Throws if invalid.

reload() reads fresh policy, entity, and schema files, validates them, and atomically swaps the active policy set. If validation fails, the previous policies remain in effect and the method throws.

Every authorization request includes a structured context object:

{
"input": { "query": "quarterly report" },
"session": {
"hour_utc": 14,
"call_count": 3,
"role": "admin"
}
}
  • context.input contains the tool’s input arguments, accessible in policies via context.input.fieldName
  • context.session.hour_utc is auto-populated with the current UTC hour (0-23)
  • context.session.call_count tracks per-tool invocation count
  • Additional context.session fields come from your contextEnricher

The onError option controls behavior when Cedar evaluation itself fails (malformed context, WASM errors):

  • 'throw' (default): raises the error to the caller
  • 'deny': treats evaluation failure as a denial (fail-closed)
  • 'proceed': allows the tool call despite the error (fail-open, use with caution)

For the full policy language grammar, operators, and built-in functions, see the Cedar policy language reference.