A policy deployment can be syntactically valid, produce reassuring logs, and still leave the business exposed. The policy might reference the wrong action, lack a negative test, or sit behind a Gateway configuration that does not enforce its decision.
Treat Cedar policy as a versioned production dependency. Correctness requires agreement between tool contracts, identity context, policy intent, enforcement configuration, and business outcomes. A successful policy creation response proves only part of that chain.
This fourth article follows the order platform’s refund controls from review to incident response. The release objective is concrete: permitted work continues, forbidden calls produce no backend effects, and operators can prove which configuration made each decision.
Table of contents
Open Table of contents
Define the decision you intend to enforce
Policy in AgentCore applies default-deny and forbid-wins semantics. A request requires an applicable permit, while a matching forbid overrides permission. Gateway tool definitions inform the policy schema. Source: Core concepts.
Start a change with a reviewable statement of intent. For example: an authenticated support operator may submit an approved refund within their authority; a suspended operator may not submit any refund. State separately which predicates Gateway can evaluate and which belong to the refund backend.
The Gateway rule might constrain role and an available input value. The backend still verifies tenant ownership, exact approval, remaining refundable balance, and idempotency. If submission references a persisted proposal instead of passing an amount, do not invent an amount field in policy context. Decide where the trusted value actually exists.
For the order platform, that intent becomes three statements. prepare_refund takes an amount, so Cedar can bound it. submit_refund takes only a proposal reference, so its policy checks the role and leaves amount and approval to the backend. One forbid covers suspended operators on both actions, and it wins over any permit.
// Support operators may prepare refunds within their per-request limit.
permit(
principal is AgentCore::OAuthUser,
action == AgentCore::Action::"OrderTools___prepare_refund",
resource == AgentCore::Gateway::"arn:aws:bedrock-agentcore:eu-west-1:111122223333:gateway/orders-gw-a1b2c3d4e5"
)
when {
principal.hasTag("role") &&
principal.getTag("role") == "support_operator" &&
context.input.amount_minor > 0 &&
context.input.amount_minor <= 50000
};
// Submission carries a proposal reference, not an amount: the backend checks
// the approval and the persisted amount.
permit(
principal is AgentCore::OAuthUser,
action == AgentCore::Action::"OrderTools___submit_refund",
resource == AgentCore::Gateway::"arn:aws:bedrock-agentcore:eu-west-1:111122223333:gateway/orders-gw-a1b2c3d4e5"
)
when {
principal.hasTag("role") &&
principal.getTag("role") == "support_operator"
};
// Suspended operators may not prepare or submit any refund.
forbid(
principal is AgentCore::OAuthUser,
action in [
AgentCore::Action::"OrderTools___prepare_refund",
AgentCore::Action::"OrderTools___submit_refund"
],
resource == AgentCore::Gateway::"arn:aws:bedrock-agentcore:eu-west-1:111122223333:gateway/orders-gw-a1b2c3d4e5"
)
when {
principal.hasTag("account_status") &&
principal.getTag("account_status") == "suspended"
};
In AgentCore Policy, JWT claims become tags on the AgentCore::OAuthUser principal (hasTag/getTag), and tool arguments arrive as context.input. A policy naming specific actions must name a specific Gateway ARN. Source: Policy scope; Policy conditions.
Express the rule in the vocabulary generated for the deployed tool schema. Tool names, target prefixes, Gateway resource identifiers, and field types are contracts. Copying a policy from another environment without resolving those references creates avoidable ambiguity.
Record intended exceptions explicitly. An emergency operator is not automatically exempt from tenant isolation or business correctness. If an exception changes the permitted business effect, it needs an accountable owner and a test.
Test discovery and invocation separately
Tool listing asks whether there are circumstances in which the principal could invoke a tool. It lacks the full invocation context. A later call evaluates its actual inputs, so a visible tool can still be denied. Source: Use an AgentCore Gateway with Policy in AgentCore.
A test that observes the refund tool in tools/list has not tested a refund authorization. Keep separate discovery and invocation assertions for every supported role.
Figure 1. Discovery visibility and permission for a particular call are different assertions.
For a role-limited tool, test an authorized role, an unauthorized role, and missing role context. For an amount constraint, test the exact boundary, immediately below it, and immediately above it using the contract’s monetary representation. Include overlapping permit and forbid cases.
A forbidden invocation must be tested by calling it directly, even if discovery hides it. Clients can remember names from earlier sessions or another environment. Hiding a tool reduces exposure; the invocation boundary carries the enforceable decision.
Confirm enforcement coverage for the protocol operations and target mode you expose. AWS documents policy evaluation for MCP tools, not blanket coverage of MCP prompts and resources. Do not extend tool-test results to other surfaces. Source: Use an AgentCore Gateway with Policy in AgentCore.
Build policy CI around real contracts
AWS policy validation checks schema compliance and types. Semantic validation can identify overly broad, restrictive, or ineffective policies. The documented default FAIL_ON_ANY_FINDINGS runs both; IGNORE_ALL_FINDINGS skips semantic validation while retaining schema checks. Source: Validation and analysis overview.
Validation is necessary, but it cannot infer the complete intended business rule. A policy that consistently permits the wrong role may be internally coherent. Review intent and executable examples together.
Version policy text with the tool schema, expected principal/context shapes, target identifiers, and test fixtures. CI should fail when a tool rename or input-type change invalidates policy assumptions. Avoid a production release process that routinely suppresses findings to make deployment pass.
Use synthetic identities and data in an isolated Gateway integration environment. Unit-level policy checks are useful for speed; integration tests establish that the real authentication and request transformation produce the context you expected.
| Case | Required assertion |
|---|---|
| Expected operator | Intended invocation permitted |
| Unauthorized role | Invocation denied |
| Missing trusted attribute | No unintended permission |
| Boundary input | Exact intended threshold behavior |
| Permit plus matching forbid | Denied |
| Schema or tool rename | Change rejected or intentionally migrated |
| Denied call | No backend business effect |
Most of that table can run in milliseconds before anything is deployed. The open-source Cedar engine evaluates the same policy text locally if you model the request the way AgentCore does: an OAuthUser principal whose claims are tags, and the tool arguments under context.input.
# test_refund_policies.py — pip install cedarpy pytest && pytest -q
import pathlib
import cedarpy
import pytest
POLICIES = pathlib.Path("policies/refunds.cedar").read_text()
GATEWAY = ('AgentCore::Gateway::"arn:aws:bedrock-agentcore:eu-west-1:'
'111122223333:gateway/orders-gw-a1b2c3d4e5"')
OPERATOR = {"role": "support_operator"}
SUSPENDED = {"role": "support_operator", "account_status": "suspended"}
def decide(tool, claims, arguments):
request = {
"principal": 'AgentCore::OAuthUser::"user-1"',
"action": f'AgentCore::Action::"OrderTools___{tool}"',
"resource": GATEWAY,
"context": {"input": arguments},
}
entities = [{
"uid": {"__entity": {"type": "AgentCore::OAuthUser", "id": "user-1"}},
"attrs": {}, "parents": [],
"tags": claims, # JWT claims become principal tags in AgentCore Policy
}]
return cedarpy.is_authorized(request, POLICIES, entities).decision.name
def prepare(amount):
return {"order_id": "o-1", "amount_minor": amount, "currency": "EUR"}
SUBMIT = {"proposal_id": "rfp_1", "idempotency_key": "op_1"}
@pytest.mark.parametrize("tool, claims, arguments, expected", [
("prepare_refund", OPERATOR, prepare(50000), "Allow"), # at the limit
("prepare_refund", OPERATOR, prepare(50001), "Deny"), # one over
("prepare_refund", OPERATOR, prepare(0), "Deny"), # not a refund
("prepare_refund", {"role": "viewer"}, prepare(100), "Deny"),
("prepare_refund", {}, prepare(100), "Deny"), # missing claim
("submit_refund", OPERATOR, SUBMIT, "Allow"),
("submit_refund", SUSPENDED, SUBMIT, "Deny"), # forbid wins
("prepare_refund", SUSPENDED, prepare(100), "Deny"),
])
def test_refund_decisions(tool, claims, arguments, expected):
assert decide(tool, claims, arguments) == expected
Local evaluation proves the logic of the policy text, not the deployed path: it cannot show which claims your IdP actually issues or what the Gateway puts in context.input. Run the same cases against a staging Gateway with real tokens before promotion.
Run tests against the complete candidate policy set, not only the newly edited statement. Interactions can widen access or create denials that are invisible when evaluating one rule in isolation.
Figure 2. Recommended release pipeline joins static checks, real context, and observed business behavior.
Observe before promotion without removing protection
Engine-level LOG_ONLY records decisions without enforcing them; ENFORCE acts on decisions. AWS also documents per-policy LOG_ONLY and ACTIVE modes. With an enforcing engine, shadow policies do not affect its returned decision. Engine-level LOG_ONLY disables policy enforcement regardless of individual policy mode. Source: Test a policy in LOG_ONLY mode.
Distinguish two rollout situations. A new isolated Gateway can observe an entire candidate policy set before enforcement. An existing production Gateway should retain its established protections while candidate policies are observed using supported policy-level mechanisms or a separate environment.
Do not switch a protected production Gateway wholesale to engine-level LOG_ONLY merely to test one change. Backend checks remain essential, but they do not justify intentionally removing a relied-upon access control.
Observation traffic should span roles, tenants, workflows, and error paths. A quiet shadow policy may mean it is safe, never exercised, or written against the wrong context. Combine traffic observation with explicit synthetic negative tests.
Review outcomes per use case rather than choosing an arbitrary observation duration. For each proposed promotion, record covered workflows, expected denials, unexpected denials, missing telemetry, and the policy set’s negative-test results. Define a halt condition before changing enforcement.
Confirm that the deployed API/SDK version supports the chosen policy-level controls and observability fields. Verify propagation behavior and wait for confirmed configuration convergence before interpreting canary results. A successful update response is not evidence that every subsequent request used the new configuration.
With an enforcing engine, a changed policy can ship in observation first and be promoted in place:
# Observe: the new statement is evaluated and logged, but does not affect decisions.
aws bedrock-agentcore-control create-policy \
--policy-engine-id "$ENGINE_ID" \
--name refund_limit_v2 \
--definition "{\"cedar\":{\"statement\":$(jq -Rs . < policies/refund_limit_v2.cedar)}}" \
--validation-mode FAIL_ON_ANY_FINDINGS \
--enforcement-mode LOG_ONLY
# Promote after the decision review: same policy, now contributing to decisions.
aws bedrock-agentcore-control update-policy \
--policy-engine-id "$ENGINE_ID" \
--policy-id "$POLICY_ID" \
--enforcement-mode ACTIVE
Promote a bounded candidate, issue live canaries, and reconcile results. Roll back to a reviewed policy version if behavior diverges. Do not equate rollback with disabling the engine or deleting every restrictive policy.
Keep evidence that survives an incident
With tracing enabled, AWS exposes policy decision, reason, determining-policy identifiers, mismatched-policy information, and Gateway enforcement-mode attributes in structured observability data. Source: AgentCore generated Policy in AgentCore observability data.
Connect those records to an application-owned evidence chain: request correlation, authenticated tenant and subject, tool contract digest, deployed policy revision, approval reference, operation identity, and committed business result.
A Gateway allow record does not prove a refund occurred. A backend completion record does not prove the caller used the intended Gateway policy. Reconcile both directions and explain missing spans, retries, and asynchronous continuation.
Keep evidence useful without collecting every sensitive payload. Store digests and durable references where possible, protect access, and choose retention by business and incident requirements. For records requiring immutability, select storage and retention controls that prevent the relevant operators from rewriting history; ordinary log availability is not the same guarantee.
Retain deployment evidence too. Snapshot or otherwise record approved targets, schemas, credential mappings, policy associations, and enforcement settings. Compare them with the registry of approved tools. Detect drift in endpoint destinations and schemas as well as policy text.
Alert on a policy engine being detached, an enforcement mode changing, or a high-impact tool appearing without its required registry entry. Make the alert actionable by assigning an owner and an expected containment response.
Rehearse containment and protect break-glass authority
Permission to update a Gateway can switch it from ENFORCE to LOG_ONLY or remove its policy engine configuration. AWS warns that the mode field has no separate protective action or condition key beyond bedrock-agentcore:UpdateGateway. Source: Policy enforcement modes.
Separate policy authorship, release approval, and infrastructure update authority. Apply review and monitoring to the deployment identity that can change those settings. Restricting only policy-edit permissions leaves an obvious alternate way to weaken enforcement.
Figure 3. Recommended containment stops new authorization and new business effects through independent controls.
Prepare a narrowly scoped forbid and an independent backend stop for each consequential tool. Pause dispatch where appropriate, inspect queued work, and reconcile operations already sent externally. A new deny cannot reverse an effect that has already committed.
Break-glass access should be time-bounded, attributable, and limited to the needed incident action. Require a second reviewer where feasible, retain the reason, and schedule an explicit restoration review. Avoid making unrestricted administrator access the default runbook.
Rehearse the incident with production-equivalent configuration. Measure time to observed denial and time to cessation of new business effects. Include failed telemetry, delayed policy propagation, and an operator attempting to use a direct backend path.
What this means in practice
Operable Cedar policy has a versioned intent, realistic context tests, a deliberate observation stage, protected enforcement configuration, and evidence joined to business outcomes.
Select one refund policy change. Write its positive and negative cases, validate against the actual tool schema, observe the candidate without removing existing protection, and rehearse targeted containment before promotion.