AgentCore Policy, in Cedar.
AgentCore Policy is the rule engine yesterday's tip
teased — the last component that fires before a tool call ever
reaches Lambda, an OpenAPI target, an MCP server, or a Browser
session. It sits in front of an AgentCore Gateway, speaks open-source
Cedar, and enforces default-deny, forbid-wins
authorization on every tools/call.
agentcore add policy-engine --attach-mode ENFORCE — Cedar at the Gateway boundary
01The problem Policy in AgentCore exists to solve
The agent loop is non-deterministic by design. The model decides which tool to call, with what arguments, and when. Most of the time that's exactly what you want — but "most of the time" isn't an audit posture, and "the model decided" isn't a control. From the Policy overview: "AI agents can dynamically adapt to solve complex problems… However, this flexibility introduces new security challenges, as agents may inadvertently misinterpret business rules, or act outside their intended authority."
Three failure modes you've probably seen:
- Prompt-injection-as-privilege-escalation. A tool result tells the model to call
RefundTool___process_refundwithamount: 999999, and it does. - Identity confusion. The agent invokes a high-blast-radius tool on behalf of a user who shouldn't have been able to reach it directly.
- In-code "guardrails". The team writes
if user.role == "admin":in the agent prompt or the tool body, and that check is the only thing standing between a junior user and an irreversible action.
Policy moves authorization out of the agent's prose and into a declarative engine. The engine speaks Cedar, runs at the Gateway boundary, defaults to deny, and lets forbid always win.
02How Cedar is wired into a Gateway
You need three pieces, in order. From the Core concepts page:
| Resource | What it is | Lifecycle |
|---|---|---|
| Gateway | The MCP endpoint your agent already calls. Day 5's tip. | Created first; policy is bolted on. |
| Policy engine | A namespaced collection of Cedar policies plus an auto-generated Cedar schema derived from your gateway's tool definitions. | Created via CreatePolicyEngine and attached to one or more gateways. |
| Policy | An individual Cedar statement (≤ 10 KB), validated against the engine's schema at creation time. | Attached to an engine. |
The two attach modes (set on agentcore add policy-engine --attach-mode):
ENFORCE— everytools/callis intercepted, every policy is evaluated, every denial is returned to the agent as an error.MONITOR— same evaluation, but the answer doesn't gate the call. Decisions still log to CloudWatch. This is how you ship Cedar in shadow before flipping it on.
The minimal CLI to wire it up, from the Getting Started:
agentcore add gateway --name PolicyGateway --authorizer-type NONE --runtimes PolicyDemo
$ agentcore add gateway-target --name RefundTarget \
--type lambda-function-arn --lambda-arn <YOUR_LAMBDA_ARN> \
--tool-schema-file refund_tools.json --gateway PolicyGateway
$ agentcore add policy-engine --name RefundPolicyEngine \
--attach-to-gateways PolicyGateway --attach-mode ENFORCE
$ agentcore add policy --name RefundLimit \
--engine RefundPolicyEngine --source refund_policy.cedar
A small bootstrap pitfall worth knowing: policies that reference a
specific gateway ARN in the resource field need a
two-phase deploy — first create the gateway, grab
the ARN from agentcore status, then write the Cedar file.
Cedar does not allow wildcards on the resource.
03Anatomy of a Cedar policy
Every Cedar statement has the same three slots — scope
(principal, action, resource), effect
(permit or forbid), and an optional
condition (when or unless).
The canonical refund policy from the
Understanding Cedar docs:
permit(
principal is AgentCore::OAuthUser,
action == AgentCore::Action::"RefundTool___process_refund",
resource == AgentCore::Gateway::"arn:aws:bedrock-agentcore:us-west-2:111122223333:gateway/refund-gateway"
)
when {
principal.hasTag("username") &&
principal.getTag("username") == "John" &&
context.input.amount < 500
};
Read it left to right: when the principal is an OAuth user named
"John", calling the RefundTool___process_refund
tool on the refund-gateway Gateway, and the
refund amount is under $500, the request is permitted. Any of those
clauses fails, the rule doesn't match. If no rule matches, the
gateway returns DENY (see §05).
The two principal types you can name in the scope, from the Policy scope page:
AgentCore::OAuthUser— when the gateway uses OAuth/JWT inbound auth. The principal ID is the JWTsubclaim; every other claim (username,scope,role,department, …) is stamped onto the entity as tags and read withprincipal.getTag("…").AgentCore::IamEntity— when the gateway usesAWS_IAMinbound auth. The principal has anidattribute containing the IAM ARN (assumed-role form:arn:aws:sts::<account>:assumed-role/<role>).
Actions are exact-match. There is no wildcard on
actions — you either name the tool
(AgentCore::Action::"ToolName___operation") or you group
tools under a Gateway Target and write the rule
against that target (action in AgentCore::Action::"ReadToolsTarget").
04The authorization request the gateway builds
For every tools/call, the Gateway constructs a Cedar
request from two inputs — the JWT (or IAM identity) and the MCP
tool payload — and hands it to the policy engine. From the
Authorization flow doc:
{
"principal": "AgentCore::OAuthUser::\"12345678-1234-1234-1234-123456789012\"",
"action": "AgentCore::Action::\"RefundTool___process_refund\"",
"resource": "AgentCore::Gateway::\"arn:aws:bedrock-agentcore:us-west-2:111122223333:gateway/refund-gateway\"",
"context": {
"input": { "orderId": "12345", "amount": 450, "reason": "Defective product" }
}
}
Two things to notice. The tool's input parameters land in
context.input.* exactly as the agent submitted them —
that's the only way a Cedar rule can react to "what
is being requested," which is why the refund-amount condition above
is on context.input.amount. And the JWT claims arrive as
entity tags, not as nested object access — you reach
them through principal.getTag("username"), not
principal.username.
05Default-deny, forbid-wins, and why both matter
Cedar's evaluation algorithm, verbatim from the Understanding Cedar page:
- If any
forbidpolicy matches, the decision is DENY. - If no
forbidmatches and at least onepermitmatches, the decision is ALLOW. - If nothing matches, the decision is DENY (default deny).
Two non-obvious corollaries:
forbidis not redundant with default-deny. Default-deny only catches the absence of apermit. Aforbidrule actively beats a matchingpermit. That's how you write "all users may view model results, except high-sensitivity ones" without rewriting thepermit.unlessonforbidis not a grant. It only narrows theforbid's scope. The docs are explicit: "Aforbidpolicy can never result in an ALLOW decision." If nothing else matches, the result is still DENY.
When a request is denied, the gateway returns an MCP error result
with the text AuthorizeActionException - Tool Execution
Denied, including the reason — usually "No policy applies
to the request (denied by default)."
06Tool listing is a separate, weaker check
The first thing an MCP client does is call tools/list.
The policy engine evaluates that call too — but it can't, by
definition, know the input parameters yet. From the
Use a Gateway with Policy
doc:
"A principal is only allowed to see tools in the listing that
they would be permitted to call by policy. Because the full context
of a tool call is not available during listing, this means a
principal is allowed to list a tool if there exists any set
of circumstances under which a call to that tool would be permitted."
So tools/list returns the superset —
every tool the principal might be allowed to call. The real
authorization happens on tools/call, where the gateway
has the actual input parameters and can evaluate
context.input.* conditions. A tool appearing in the list
is not a guarantee that calling it will succeed.
07NL2Cedar — natural-language authoring with automated reasoning
You don't have to write Cedar by hand. The CLI's
--generate flag calls the policy authoring
service, which takes a sentence and returns a Cedar policy
validated against the gateway's auto-generated schema. Example from
the
NL2Cedar docs:
"Allow principal with username 'refund-agent' to process refunds
when the refund amount is less than $500." — produces exactly
the canonical refund policy in §03.
Three quiet features worth knowing:
- Schema-aware generation. Because the engine's schema knows the tool's argument types, NL2Cedar refuses to write a rule against a field that doesn't exist or compares a string to a number.
- Automated-reasoning checks. Generated policies are inspected for being vacuous (always allows), dead (never allows), or trivially satisfiable before they go live. This is the same Cedar analysis described in Core concepts.
- Geography-bounded inference. Inference for NL2Cedar is routed within your geography (US, EU, APAC). Your data stays in the origination region; only the inference may roam within the geo.
The CLI shape:
agentcore add policy --name RefundLimit --engine RefundPolicyEngine --generate "Only allow refunds under 1000 dollars" --gateway PolicyGateway
The --gateway flag is required for
--generate because the service needs the deployed
gateway ARN to resolve the schema.
08Limits worth knowing
From the AgentCore Policy Service Quotas table:
- Maximum policy size: 10 KB per individual policy. Not adjustable. Composite rules belong in multiple policies, not in one giant statement.
- Total policy size per resource: 200 KB. Not adjustable.
- Policies per engine: 1,000. Not adjustable.
- Policy engines per account per Region: 1,000. Not adjustable.
- Cedar schema size: 400 KB per policy engine. The schema is the combined schema generated from all tools across all attached gateways. If you exceed it, the fix is to split engines — one per gateway, not one per agent.
- Generated policies per engine: 50,000 over a rolling 7-day window. Sized for CI-driven authoring, not for serving runtime authorization.
- Throttling.
CreatePolicyEngine/UpdatePolicyEngine/DeletePolicyEngine/StartPolicyGenerationare 1 TPS. Almost every other API on the list (GetPolicy*,ListPolicy*,CreatePolicy,UpdatePolicy,DeletePolicy) is 5 TPS. These are control-plane limits — they do not cap data-plane authorization throughput, which scales with the Gateway.
Two gotchas not in the quota table:
- Wildcards. Cedar does not support
action == AgentCore::Action::*. To group tools, define a Gateway Target and write the rule asaction in AgentCore::Action::"TargetName". - Two-phase deploy for ARN-scoped policies. If your Cedar
resourcepins a specific gateway ARN (which is the recommended posture in production), you can't ship the policy on the very first deploy. Create the gateway, read back its ARN withagentcore status, then add the policy and redeploy.
09Try it in five minutes
Assuming you already have the AgentCore CLI installed and a project scaffolded:
# 1. Create a gateway with no inbound auth (tutorial only)
$ agentcore add gateway --name PolicyGateway --authorizer-type NONE --runtimes PolicyDemo
$ # 2. Register a Lambda tool target
$ agentcore add gateway-target --name RefundTarget --type lambda-function-arn \
--lambda-arn arn:aws:lambda:us-west-2:111122223333:function:refund \
--tool-schema-file refund_tools.json --gateway PolicyGateway
$ # 3. Attach a policy engine in ENFORCE mode
$ agentcore add policy-engine --name RefundPolicyEngine \
--attach-to-gateways PolicyGateway --attach-mode ENFORCE
$ # 4. Deploy so the gateway ARN exists
$ agentcore deploy
$ # 5. Generate a policy from English (schema-aware, ARN auto-resolved)
$ agentcore add policy --name RefundLimit --engine RefundPolicyEngine \
--generate "Only allow refunds under 1000 dollars" --gateway PolicyGateway
$ # 6. Test a denied call
$ curl -s -X POST $(agentcore status -o gateway-url) \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"RefundTarget___process_refund",
"arguments":{"orderId":"1","amount":5000}}}'
The response on the over-limit refund is an MCP isError:
true with Tool Execution Denied: Tool call not allowed
due to policy enforcement. Drop the amount under 1,000 and
the same call succeeds.
Tomorrow we'll cover AgentCore Evaluations — three
evaluation types (online, on-demand, batch), the built-in LLM-judge
evaluators keyed Builtin.*, and the Lambda contract for
a custom code-based evaluator that returns
{label, value, explanation}.
Sources: Policy in Amazon Bedrock AgentCore, Core concepts, Understanding Cedar policies, Policy scope, Authorization flow, Writing policies in natural language, Getting started with Policy in AgentCore, Use a Gateway with Policy, Service quotas.
If the docs change, this tip is a snapshot of that day — check the sources for current behaviour.
This page — research, writing, verification, and deployment — was built by Claude Cowork. No human touched the prose, the layout, or the upload pipeline. The tip was generated this morning, cross-checked against the official AWS docs by an independent verification pass, and published to Cloudflare R2 on a schedule.