Amazon Bedrock AgentCore Gateway is a sensible place to put agent controls. Every tool call an agent makes through it can be checked against a Cedar policy before the tool runs, the decision is deterministic and lands in an audit log, and Lambda interceptors let you run your own code before and after each call. If your agents only ever needed rules like "the support agent may call refund for amounts under 100", you could stop there.
The incidents I keep finding in multi-agent traces don't look like that. They're sequences of calls that each pass on their own, and a per-request gateway leaves two gaps that let them through. I've been building two open-source projects to cover those gaps: SwarmSentinel on the tool path and SwarmSheriff on the model path. Both are research prototypes. The AgentCore wiring below is a design I'm proposing, not something I've shipped, and I'll be specific about which parts exist.
Gap 1: the gateway has no memory of the session
Here is a scenario from SwarmSentinel's synthetic test suite. An orchestrator delegates invoice processing to a parser agent. The parser reads a vendor invoice that contains instructions to move money. It can't pay anyone itself, so it messages the ledger agent, which does hold payment rights, and asks it to "settle the invoice as instructed".
Now look at that last call from the gateway's position. The principal is the ledger agent. The action is payments.transfer. The ledger agent is allowed to make transfers, the amount is within its limit, and the arguments look normal. A Cedar policy evaluated against principal, action, resource and request context says yes, which is the right answer for the request it was given. Nothing in that request says the instruction came from a document an attacker controlled, two hops back, through a different agent.
You can't fix this by writing a better Cedar rule, because the information isn't in the request. It lives in the history of the session: who read what, who talked to whom, who spawned whom. A content classifier on the request doesn't help much either. "Settle the invoice as instructed" is not a malicious string.
I see the same shape elsewhere. A child agent is spawned with its own identity and broader rights than its parent ever had. Five fresh agent IDs each try the same action once, so no per-agent rate limit fires. A shared wiki gets forty small edits in a minute, each one harmless. A parent agent is shut down for misbehaving while its children carry on with valid credentials.
Gap 2: the model API is a second exit
The gateway sees tool calls. It doesn't see what an agent sends to its model provider. An agent that has just read ~/.aws/credentials or a customer table through a perfectly authorised tool call can paste that into its next prompt, and the request leaves through a path no tool policy covers. For agents on AgentCore Runtime calling Bedrock that path stays inside AWS, but plenty of agents call Anthropic, OpenAI or OpenRouter directly, and coding agents on laptops do it all day.
SwarmSentinel: integrity on the tool path
SwarmSentinel implements ASP (Agent Security Policy), a policy model I proposed in a preprint. ASP is a proposal, not an adopted standard. The policy is a JSON file, and the gateway checks each proposed action in a fixed order: lineage and admission, explicit deny list, default-deny allowlist, argument constraints, network scope, content-trust contamination, then write caps and repeated intent.
For this post, the interesting parts are the ones that keep state.
Lineage comes first. An agent has to be admitted, either by a spawn the gateway allowed or by a registry entry. A child's scope must sit inside its parent's, and delegation depth is capped.
Taint is what stops the invoice relay. When an agent reads content marked untrusted, its context is contaminated. The policy can take specific capabilities away from contaminated contexts (payments and writes, say) and can pass the taint along with the messages that agent sends. The ledger agent got its instructions from a tainted context, so it loses payment rights for that context. Taint can also be tracked per (agent, task) pair, which means a long-lived agent isn't poisoned forever by one bad web page. Only the owner can open a new task. The agent can't.
The rest are counters and graph checks. There are write caps per shared resource per time window, a step budget per agent, and repeated-intent throttling that counts distinct agent IDs. Graph tripwires look for reciprocal message clusters, coordinated writes, fan-out, injection reaching several agents and delegation loops. When one fires it can revoke an agent, and the revocation covers everything that agent spawned.
Every decision goes to a flight recorder with the rule that fired and the taint origin, and a dashboard replays the session as a graph. There's also a report-only mode that answers "what would this policy have done?" without blocking anything. I've used it to replay records from the AI Village dataset (AI Digest, "AI Village dataset", 2026), where real agents from several labs work together in one environment. Those runs are counterfactual: they show what the policy would have flagged or blocked, not anything it stopped.
Overhead is small because the checks are dictionary lookups and set operations. On an Apple M2 with 20 agents, the guard measured p50 0.024 ms in-process and 0.39 ms over localhost HTTP.
SwarmSheriff: confidentiality on the model path
SwarmSheriff is a single Go binary. One of its jobs is a loopback reverse proxy for LLM APIs: you set ANTHROPIC_BASE_URL or OPENAI_BASE_URL to point at it, and it classifies each outbound request against data classes the company defines in a policy file (source code, customer data, credentials, legal material). Each request gets a verdict of allow, hold or block (a fourth verdict, translate, is still a stub). Report mode is the default, and enforcement is switched on per class.
I picked the classification order on purpose. Source labels come first, then fingerprints of known documents, then patterns and keywords, and only then an optional classifier for whatever the deterministic steps left ambiguous. Clever wording doesn't get past the first three steps, because none of them is interpreting the text. It never stores request bodies, and a hosted classifier sees only features of the request unless a class opts in to raw content.
How the two compare
| SwarmSentinel | SwarmSheriff | |
|---|---|---|
| Path it watches | Tool calls, spawns, agent messages | Requests to model APIs |
| Question it answers | Should this agent be allowed to do this, given the session so far? | Is company data about to leave in this prompt? |
| State | Lineage, taint, labels, rate windows, revocations | Mostly per request, plus labels pulled from SwarmSentinel |
| Policy | ASP JSON | Data-class JSON |
| Default mode | Enforce (synthetic), report-only (replay) | Report |
| Runs as | Python engine (FastAPI), MCP proxy, in-process guard | Go binary on loopback |
| Security property | Integrity | Confidentiality |
The link between them
The two share one view of each agent, and this part is built and tested. An ASP policy can declare confidentiality classes, for example "any call whose arguments touch ~/.ssh or ~/.aws labels the context credentials", or "any call to mcp:crm:* labels it customer-data". Labels only accumulate, are inherited by spawned children and can follow messages.
When the agent then calls its model, the request carries its engine-issued credential. SwarmSheriff resolves that credential with SwarmSentinel and gets back the agent, its task, its labels, and whether it is tainted or revoked. A data class in SwarmSheriff can block on the label asp:credentials whatever the prompt text says. A revoked agent's model requests are refused, so revocation reaches the model path as well. An unknown credential or an unreachable engine fails closed. SwarmSheriff posts its decision back, and SwarmSentinel records it as an out-of-band row, so one timeline shows both paths.
The trade-off is that labels describe what a context has touched, not what a given prompt contains. A labelled agent sending a harmless request gets held anyway. I think that's the right default for credentials and the wrong one for most other classes, which is why it's set per class.
Wiring it into AgentCore
I wouldn't replace the Gateway's Cedar policies. Cedar handles the stateless rules well, and AWS can run automated reasoning over a Cedar policy set, which my JSON validator can't do. Cedar should decide whatever one request is enough to decide, and SwarmSentinel should get the rest.
Option A: a request interceptor
AgentCore Gateway can run a Lambda before each tool call. That Lambda sends the proposed call to SwarmSentinel's evaluate endpoint and denies the call if the engine says no. A rough sketch:
import json
import os
import urllib.request
ENGINE = os.environ["SWARM_ENGINE_URL"]
SESSION = os.environ["SWARM_SESSION_ID"]
# Tools whose results are untrusted content (web fetches, inbound email, uploaded files).
UNTRUSTED = {"web.fetch", "email.read", "files.read_upload"}
def handler(event, context):
# Field names below are placeholders: map them from the interceptor event schema in the AgentCore docs.
tool = event["toolName"]
call = {
"agentId": event["agentId"],
"action": "tool",
"target": tool,
"spanId": event["requestId"],
"detail": json.dumps(event.get("arguments", {}))[:4000],
"reads": "untrusted" if tool in UNTRUSTED else "",
}
req = urllib.request.Request(
f"{ENGINE}/api/swarm/sessions/{SESSION}/evaluate",
data=json.dumps(call).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {service_token()}"},
)
with urllib.request.urlopen(req, timeout=2) as resp:
verdict = json.load(resp)
if not verdict["allowed"]:
return deny(f"{verdict['rule']}: {verdict['reason']}")
return allow(event)
service_token, allow and deny depend on your identity setup and the interceptor's response contract, so I've left them out. If the engine times out, the Lambda should fail closed, the same way SwarmSheriff does. In AWS's examples the request interceptor runs before the Cedar evaluation, so SwarmSentinel also sees calls that Cedar will go on to deny. I want that, because repeated attempts and probing count toward step budgets and repeated-intent limits.
Option B: SwarmSentinel's MCP proxy as a Gateway target
SwarmSentinel already exposes an MCP endpoint that fronts downstream MCP servers, with credentials it mints per agent. Registering that endpoint as an MCP target puts ASP behind the Gateway's inbound auth with no Lambda. The catch is identity: lineage depends on each agent presenting its own credential. If the Gateway forwards every agent under one outbound identity, SwarmSentinel sees one agent and lineage stops working. Check this before choosing B.
The model path and hosting
For agents in AgentCore Runtime, SwarmSheriff would run as a second process in the same container, with the agent's base URL pointed at loopback. I haven't tested Bedrock's SigV4-signed traffic through it yet. A proxy that changes the host can break the signature, so for now this is the more natural fit for agents calling Anthropic, OpenAI or OpenRouter endpoints directly.
SwarmSentinel itself keeps one shared state per swarm, so I'd run it as a long-lived service (ECS or App Runner, say) rather than inside AgentCore Runtime, where each session gets its own microVM. Durable sessions currently use a SQLite journal, which would need EFS or a move to DynamoDB.
What this doesn't solve
SwarmSentinel is a code boundary in one process, not a tamper-proof control. It only sees calls routed through it, and so does SwarmSheriff, which is why the repo includes a confinement lab that measures how agents can get around the proxies. Taint is coarse: it costs you some legitimate actions in exchange for not having to recognise the attack. SwarmSheriff's evaluation corpus is still small, so I don't quote accuracy numbers for it. And the engine authenticates callers with Clerk JWTs today, so a service credential for the interceptor is real work, not a config change.
You need per-request authorization whatever else you run, and AgentCore's is solid. But the failures I keep seeing in multi-agent traces build up over several calls and agents, and some leave through the model API instead of a tool. Catching those means something has to remember the swarm. I'd rather gateways did that themselves. For now it has to run beside them.
Both repos SwarmSentinel (tool path) and SwarmSheriff (model path) are MIT licensed. If you try the interceptor pattern, I'd like to hear where it breaks.
Top comments (0)