Insecure Inter-Agent Communication: When Agents Talk, Attackers Listen (ASI07) A developer warns that multi-agent AI systems are repeating the security mistakes of early microservices, where inter-agent communication lacks authentication, integrity, and semantic validation. Attackers can intercept, spoof, or tamper with natural-language messages between agents, as demonstrated by Trustwave's Agent-in-the-Middle attack on Google's A2A protocol and Palo Alto's agent session smuggling. The developer argues that security must operate at the semantic layer, not just the transport layer. This is post 7 of the OWASP Agentic AI Top 10: What Builders on AWS Need to Know https://dev.to/aws/the-owasp-agentic-ai-top-10-what-builders-on-aws-need-to-know-cji series. You've built a multi-agent system. I've built multi-agent systems. Agent A handles customer requests. Agent B processes payments. Agent C manages inventory. They talk to each other over internal APIs, passing context, delegating tasks, sharing results. Now ask yourself: when Agent A tells Agent B "process a refund for $500 to account X," how does Agent B know that instruction actually came from Agent A? And how does it know Agent A wasn't compromised 30 seconds ago? If your answer involves the words "we trust internal traffic," you have a problem. Welcome to Insecure Inter-Agent Communication . ASI07 covers threats where the messages flowing between agents lack proper authentication, integrity verification, or semantic validation. Attackers can intercept, spoof, tamper with, or replay these messages to manipulate agent behavior across an entire system. Think about it like microservices. Ten years ago, people learned the hard way that internal service-to-service traffic needs the same rigor as external traffic. Zero trust. mTLS. Schema validation. The whole thing. Multi-agent AI systems are making the same mistake all over again. But worse. Because agent-to-agent messages aren't just API calls with typed schemas. They're natural language. Untyped. Ambiguous. And the receiving agent can't reliably distinguish a legitimate instruction from an injected one. OWASP identifies six vulnerability patterns for inter-agent communication: This isn't theoretical. Researchers are actively exploiting these patterns. In May 2026, Trustwave's SpiderLabs demonstrated the "Agent-in-the-Middle" attack https://www.trustwave.com/en-us/resources/blogs/spiderlabs-blog/agent-in-the-middle-abusing-agent-cards-in-the-agent-2-agent-protocol-to-win-all-the-tasks/ on Google's A2A protocol. A malicious agent advertises exaggerated capabilities in its agent card /.well-known/agent.json . The host agent picks it for tasks based on those fake capabilities. Sensitive requests get routed through the attacker. The beauty from the attacker's perspective : the A2A protocol trusts agent cards by default. There's no built-in verification that the capabilities advertised are real. Palo Alto's Unit 42 discovered "agent session smuggling" https://unit42.paloaltonetworks.com/agent-session-smuggling-in-agent2agent-systems/ where a malicious agent exploits an established cross-agent communication session to send covert instructions to a victim agent. Once a session is established, identity claims are taken at face value. A compromised peer can inject anything. Researchers demonstrated Agent Card Poisoning https://semiengineering.com/agent-card-poisoning-a-metadata-injection-vulnerability-in-the-systems-using-google-a2a-protocol/ where adversarial instructions are embedded directly within an agent card's metadata. When a host agent reads the card to understand capabilities, the injected instructions influence its behavior. Prompt injection, but at the protocol discovery layer. The pattern repeats with MCP. A malicious MCP endpoint advertises spoofed agent descriptors or false capabilities. When trusted, it routes sensitive data through attacker infrastructure. Multiple CVEs in 2025 documented this exact pattern. You might be thinking: "I'll just put my agents in a VPC and encrypt the traffic." That helps with transport security. But it doesn't solve the core problem. The messages themselves are the attack vector. Even over encrypted, authenticated channels, a compromised agent can send perfectly valid-looking messages that manipulate the receiving agent. The message is well-formed. The credentials are valid. The content is malicious. Agent-to-agent security needs to happen at the semantic layer , not just the transport layer. You need to validate what is being said, not just who is saying it. Here's how to build secure agent-to-agent communication on AWS. The principle: validate structure, authenticate identity, verify intent, and contain blast radius. Amazon EventBridge https://aws.amazon.com/eventbridge/?trk=d76afd77-bb62-46ac-b0a3-9dbf5ecde253 Schema Registry lets you define and document the contract for inter-agent messages. But here is the part people miss, and it is the whole ballgame: the registry documents the contract. Your code enforces it. EventBridge does not validate events against your registered schemas. Let me say that again, because it trips up almost everyone. It will happily deliver a malformed, injection-laden event straight to your target without so much as a raised eyebrow. The registry gives you a shared definition and code bindings. The enforcement is on you, at both ends. So let me show you how I wire this up. Step 1: Define the contract in the Schema Registry. python import boto3 import json from datetime import datetime, timezone from jsonschema import validate, FormatChecker, ValidationError eventbridge = boto3.client "events" schemas = boto3.client "schemas" Define the contract for inter-agent messages delegation schema = { "openapi": "3.0.0", "info": {"title": "AgentDelegation", "version": "1.0.0"}, "paths": {}, "components": { "schemas": { "DelegationMessage": { "type": "object", "required": "source agent", "target agent", "task type", "parameters", "nonce", "timestamp" , "properties": { "source agent": {"type": "string", "pattern": "^agent- a-z +- 0-9 +$"}, "target agent": {"type": "string", "pattern": "^agent- a-z +- 0-9 +$"}, "task type": {"type": "string", "enum": "query", "process", "notify" }, "parameters": { "type": "object", "properties": { "query": {"type": "string", "maxLength": 500}, "resource id": {"type": "string", "pattern": "^ a-zA-Z0-9- +$"} }, "additionalProperties": False No free-form injection fields }, "nonce": {"type": "string", "minLength": 32}, "timestamp": {"type": "string", "format": "date-time"} }, "additionalProperties": False } } } } Register the schema documentation + code generation . Note: this is a one-time bootstrap. create registry and create schema both throw if the thing already exists, so do not run this on every invocation. schemas.create registry RegistryName="agent-communications" schemas.create schema RegistryName="agent-communications", SchemaName="AgentDelegation", Type="OpenApi3", Content=json.dumps delegation schema Step 2: Validate at the producer, before anything hits the bus. Extract the JSON Schema portion for validation message schema = delegation schema "components" "schemas" "DelegationMessage" def publish agent message message: dict : """Validate against the schema before putting anything on the bus.""" try: validate instance=message, schema=message schema, format checker=FormatChecker except ValidationError as e: raise ValueError f"Message rejected at producer: {e.message}" eventbridge.put events Entries= { "Source": f"agent.{message 'source agent' }", "DetailType": "AgentDelegation", "Detail": json.dumps message , "EventBusName": "agent-bus" } Step 3: Validate again at the consumer, before you act on a single field. And here is where most examples fall over. The consumer is where the real discipline lives, because you have to assume the producer is buggy, compromised, or flat out lying to you. python def handle agent message event: dict : """Consumer validates independently. Never trust that the producer did its job.""" EventBridge delivers detail to a Lambda target already parsed into a dict. Do NOT json.loads it. If you consume via SQS or Kinesis instead, parse the record body, and pull detail out of that. message = event "detail" Schema validation. Pass a FormatChecker so date-time is actually asserted. That check needs the extras: pip install "jsonschema format " try: validate instance=message, schema=message schema, format checker=FormatChecker except ValidationError as e: print f" REJECTED Invalid message from {message.get 'source agent', 'unknown' }: {e.message}" return Drop it. Don't act on it. Anti-replay: check the nonce has not been used before if nonce store.exists message "nonce" : print f" REJECTED Replay detected: {message 'nonce' }" return nonce store.add message "nonce" , ttl=300 Freshness: parse defensively, and compare timezone-aware to timezone-aware try: msg time = datetime.fromisoformat message "timestamp" .replace "Z", "+00:00" except ValueError: print f" REJECTED Unparseable timestamp: {message 'timestamp' }" return if msg time.tzinfo is None: msg time = msg time.replace tzinfo=timezone.utc if datetime.now timezone.utc - msg time .total seconds 60: print f" REJECTED Stale message: {message 'timestamp' }" return Only now do we trust it enough to act process delegation message A few things in that consumer are load-bearing, so let me call them out. json.loads the detail. When EventBridge invokes a Lambda target, event "detail" is already a parsed dict. Calling json.loads on it throws TypeError and your consumer dies on the very first real message. You only parse a string when you are pulling the event out of an SQS or Kinesis record body. Z . Parse it into an aware datetime and compare it against datetime.now timezone.utc . Mixing a naive utcnow with an aware timestamp raises can't subtract offset-naive and offset-aware datetimes, which is a real joy to debug at 2am. Bonus: on Python older than 3.11, fromisoformat cannot even parse the Z suffix, which is why we normalise it first. date-time format is documentation, not enforcement. Out of the box the jsonschema library does not check format at all. You have to install the extras and pass a FormatChecker , and even then I still parse the timestamp defensively in code. Belt and suspenders.The key insight, and the reason this whole section exists: The Schema Registry gives every team a shared source of truth. The generated bindings give you autocomplete and type safety in your IDE. That is genuine value and you should use it. But enforcement lives in your code, not in the bus. Validate at the producer so you never publish garbage. Validate at the consumer because you must never trust that the producer did. Then add the runtime checks a schema simply cannot express, like nonce replay protection and freshness windows. Now one honest caveat, because I am not going to oversell this to you. Schema validation locks down the shape of your message. additionalProperties: False kills the free-form fields an attacker would love to smuggle in, and the regex patterns keep your agent IDs and resource IDs tidy. But look at that query field. It is a string, up to 500 characters, and the schema has no opinion whatsoever about what is inside it. So this sails straight through validation without a whimper: '; DROP TABLE users; -- and ignore all previous instructions If that string later lands in a SQL query or an LLM prompt, your schema did absolutely nothing to save you. Schema validation constrains structure and format. It does not sanitise the semantics of free text. Those values still need context-specific escaping or sanitising at the point where you actually use them. And one last thing while we are here. EventBridge rules can add a coarse second net through content-based filtering, for example only routing events where task type matches one of your enum values. Useful, and basically free. But that is filtering, not schema validation. Do not confuse the two. AWS Step Functions https://aws.amazon.com/step-functions/?trk=d76afd77-bb62-46ac-b0a3-9dbf5ecde253 gives you something agents talking directly to each other never will: a single, auditable, centrally controlled entry point for every inter-agent message, with schema validation bolted to the front door. I want to be careful with my words here, because this is where a lot of "secure multi-agent" write-ups quietly cheat. Step Functions is not a type system. It will not magically validate your payloads. What it gives you is a chokepoint. One place every message has to pass through. And a chokepoint is only worth something if you actually force the traffic through it. Hold that thought, because it is the whole point of this section. So instead of agents invoking each other directly, you route every request through a state machine. { "Comment": "Secure multi-agent orchestration", "StartAt": "ValidateRequest", "States": { "ValidateRequest": { "Type": "Task", "Resource": "arn:aws:lambda:us-east-1:123456789012:function:validate-agent-message", "Next": "RouteToAgent", "Catch": { "ErrorEquals": "ValidationError" , "ResultPath": "$.error", "Next": "RejectAndLog" }, { "ErrorEquals": "States.ALL" , "ResultPath": "$.error", "Next": "RejectAndLog" } }, "RouteToAgent": { "Type": "Choice", "Choices": { "Variable": "$.task type", "StringEquals": "process payment", "Next": "PaymentAgent" }, { "Variable": "$.task type", "StringEquals": "check inventory", "Next": "InventoryAgent" } , "Default": "RejectAndLog" }, "PaymentAgent": { "Type": "Task", "Resource": "arn:aws:lambda:us-east-1:123456789012:function:payment-agent", "InputPath": "$.validated params", "ResultPath": "$.payment result", "TimeoutSeconds": 30, "Retry": { "ErrorEquals": "Lambda.TooManyRequestsException", "Lambda.ServiceException" , "IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2.0 } , "Catch": { "ErrorEquals": "States.ALL" , "ResultPath": "$.error", "Next": "RejectAndLog" } , "Next": "Done" }, "InventoryAgent": { "Type": "Task", "Resource": "arn:aws:lambda:us-east-1:123456789012:function:inventory-agent", "InputPath": "$.validated params", "ResultPath": "$.inventory result", "TimeoutSeconds": 30, "Retry": { "ErrorEquals": "Lambda.TooManyRequestsException", "Lambda.ServiceException" , "IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2.0 } , "Catch": { "ErrorEquals": "States.ALL" , "ResultPath": "$.error", "Next": "RejectAndLog" } , "Next": "Done" }, "RejectAndLog": { "Type": "Task", "Resource": "arn:aws:lambda:us-east-1:123456789012:function:log-rejected-message", "ResultPath": "$.log result", "Next": "Rejected" }, "Rejected": { "Type": "Fail", "Error": "MessageNotProcessed", "Cause": "Message failed validation, routing, or agent execution and was never forwarded." }, "Done": {"Type": "Succeed"} } } Let me walk through what that buys you. - One front door. Every message enters at ValidateRequest. Nothing reaches an agent until it has been validated and explicitly routed. There is no side entrance. - Least-privilege payloads. InputPath: $.validated params means the payment agent receives only the parameters it needs. It never sees the full envelope, the routing metadata, or the nonce. If that Lambda gets popped, it has less to work with. - Fail closed, and fail loud. Validation errors, unknown task type values, and dead agents all funnel to RejectAndLog and then to a Fail state. This matters. In the naive version a rejected message ends in Succeed, which means a malicious payload produces a green, successful execution and your CloudWatch alarm on failed executions never fires. Do not do that. A rejection is a failure. Make it look like one. - Retries for the honest failures. Agents throw transient errors. Lambda.TooManyRequestsException is not an attack, it is a Tuesday. The Retry block backs off and tries again before giving up. - A full audit trail. Every transition, with input and output, lands in the execution history. When something goes sideways at 3am you have a complete, replayable record of exactly what happened. Execution history is kept for 90 days, so if you need audit that outlives that, turn on CloudWatch Logs or ship it to S3. And remember the message contents sit in that history, so mind what you log. Now about those timeouts, because the word "timeout" lies to you a little. TimeoutSeconds: 30 does not kill your Lambda. Let me say that again, because I got this wrong myself the first time. When the timer fires, Step Functions stops waiting and marks the task failed. The Lambda underneath keeps right on running, burning money and holding connections, until it finishes or hits its own function timeout. So set the Lambda's timeout too. The state machine timeout protects the orchestrator. The function timeout protects your wallet and your downstream. And here it comes. The part that actually makes this secure. A state machine that routes through an orchestrator does not stop an agent from picking up the phone and calling another agent directly. Read that again. The JSON above is necessary. It is not sufficient. If your payment agent's execution role has lambda:InvokeFunction on the inventory agent, then the moment someone compromises the payment agent your beautiful orchestrator becomes optional. The attacker just calls the next agent directly and skips validation, routing, and logging. All of it. The chokepoint is only a chokepoint if IAM makes it the only road. So two rules, and they are not optional. Rule 1: Only the orchestrator may invoke the agents. The state machine's execution role gets lambda:InvokeFunction, scoped to exactly these functions and nothing else. { "Version": "2012-10-17", "Statement": { "Sid": "InvokeOnlyTheseFunctions", "Effect": "Allow", "Action": "lambda:InvokeFunction", "Resource": "arn:aws:lambda:us-east-1:123456789012:function:validate-agent-message", "arn:aws:lambda:us-east-1:123456789012:function:payment-agent", "arn:aws:lambda:us-east-1:123456789012:function:inventory-agent", "arn:aws:lambda:us-east-1:123456789012:function:log-rejected-message" } } Rule 2: The agents may not invoke each other. At all. Look closely at a typical agent role. Notice what is not there. { "Version": "2012-10-17", "Statement": { "Sid": "PaymentAgentDataAccessOnly", "Effect": "Allow", "Action": "dynamodb:GetItem", "dynamodb:PutItem" , "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/payments" } } No lambda:InvokeFunction. Anywhere. The payment agent can touch its payments table and that is the entire list. It has no ability, none, to call the inventory agent or any other function. If it needs something done elsewhere, it returns a result to the orchestrator and the orchestrator decides what happens next. That is what "no direct agent-to-agent communication" actually means, and you will notice it lives in IAM, not in the state machine. Want to go one step further? Put a resource-based policy on each agent function that grants invoke permission only to the orchestrator role, then use an SCP to deny lambda:InvokeFunction on those ARNs for every other principal in the account. Now even an over-privileged admin role cannot accidentally wire two agents together. Belt, suspenders, and a second belt. The state machine gives you the shape. IAM gives you the guarantee. Most people ship only the first one and call it secure. 3. SQS Message Signing for Integrity If your agents communicate via queues, SQS https://aws.amazon.com/sqs/?trk=d76afd77-bb62-46ac-b0a3-9dbf5ecde253 with message attributes and HMAC signing prevents tampering: python import hashlib import hmac import json import time import uuid import boto3 sqs = boto3.client "sqs" secrets = boto3.client "secretsmanager" SECRET KEY = secrets.get secret value SecretId="agent-hmac-key" "SecretString" .encode MAX AGE SECONDS = 60 freshness window; bounds the replay surface class IntegrityError Exception : """Raised when a message fails signature, freshness, or replay checks.""" class InMemoryNonceStore: """Example only. In production use a SHARED store DynamoDB with a TTL attribute, or Redis . An in-memory dict does NOT stop replays across concurrent Lambda instances, because each instance has its own copy.""" def init self : self. seen = {} php def exists self, nonce: str - bool: now = time.time self. seen = {n: exp for n, exp in self. seen.items if exp now} return nonce in self. seen def add self, nonce: str, ttl: int : self. seen nonce = time.time + ttl def signing bytes source agent: str, nonce: str, timestamp: str, body: str - bytes: """Canonical, unambiguous representation of everything we sign. This covers the body AND the security-relevant attributes, so none of them can be tampered with independently of the signature.""" return json.dumps {"source agent": source agent, "nonce": nonce, "timestamp": timestamp, "body": body}, sort keys=True, separators= ",", ":" , .encode def send signed message queue url: str, message: dict, source agent: str : """Sign the body together with the attributes, then send.""" body = json.dumps message, sort keys=True, separators= ",", ":" nonce = str uuid.uuid4 timestamp = str int time.time signature = hmac.new SECRET KEY, signing bytes source agent, nonce, timestamp, body , hashlib.sha256 .hexdigest sqs.send message QueueUrl=queue url, MessageBody=body, MessageAttributes={ "source agent": {"DataType": "String", "StringValue": source agent}, "signature": {"DataType": "String", "StringValue": signature}, "nonce": {"DataType": "String", "StringValue": nonce}, "timestamp": {"DataType": "String", "StringValue": timestamp}, }, def attr attrs: dict, name: str - str: """Fetch a required string attribute, or fail closed.""" try: return attrs name "StringValue" except KeyError, TypeError : raise IntegrityError f"missing required attribute: {name}" def verify message msg: dict, nonce store - dict: """Verify a single message. Returns the parsed body or raises IntegrityError. nonce store must expose .exists nonce and .add nonce, ttl .""" body = msg "Body" attrs = msg.get "MessageAttributes" or {} source agent = attr attrs, "source agent" signature = attr attrs, "signature" nonce = attr attrs, "nonce" timestamp = attr attrs, "timestamp" 1 Integrity + binding: recompute over body AND attributes, constant-time compare. expected = hmac.new SECRET KEY, signing bytes source agent, nonce, timestamp, body , hashlib.sha256 .hexdigest if not hmac.compare digest signature, expected : raise IntegrityError f"signature check failed claimed source: {source agent} " 2 Freshness: reject stale or future-dated messages allow a little clock skew . try: age = int time.time - int timestamp except ValueError: raise IntegrityError "unparseable timestamp" if age MAX AGE SECONDS or age < -MAX AGE SECONDS: raise IntegrityError f"stale/future-dated message age {age}s " 3 Anti-replay: each nonce is accepted exactly once. if nonce store.exists nonce : raise IntegrityError f"replay detected nonce {nonce} " nonce store.add nonce, ttl=MAX AGE SECONDS 2 return json.loads body def receive and process queue url: str, nonce store, handler : """Long-poll, verify each message, process + delete on success. On failure we alert and leave the message for SQS to redrive to a dead-letter queue configure a redrive policy with maxReceiveCount on the source queue .""" response = sqs.receive message QueueUrl=queue url, MessageAttributeNames= "All" , MaxNumberOfMessages=10, WaitTimeSeconds=20, for msg in response.get "Messages", : try: payload = verify message msg, nonce store except IntegrityError as e: print f" REJECTED {e}" emit a CloudWatch metric / alarm here continue do NOT delete - redrive policy sends it to the DLQ handler payload sqs.delete message QueueUrl=queue url, ReceiptHandle=msg "ReceiptHandle" HMAC uses a shared symmetric key, so this authenticates the group, not the individual agent. Signing source agent stops an attacker who can write to the queue but does not hold the key. It does not stop a compromised agent that does hold the shared key from forging a message as any other agent. If your threat model includes a compromised agent and this whole post says it does , you need per-agent keys or asymmetric signatures — each agent signs with its own private key, verifiers hold the public keys, and now source agent is genuinely provable. Don't let "prevents tampering" imply "proves who sent it," because with one shared key it doesn't. 4. API Gateway with Mutual TLS for Agent-to-Agent Calls For agents that communicate via HTTP APIs, API Gateway https://aws.amazon.com/api-gateway/?trk=d76afd77-bb62-46ac-b0a3-9dbf5ecde253 with mutual TLS ensures both sides prove their identity: - Each agent gets its own client certificate - API Gateway validates the cert before forwarding the request - You know exactly which agent sent which message - No certificate = no communication. Period. Combined with request validation OpenAPI schema enforcement at the gateway , you get both identity verification AND structural validation before the message reaches the target agent. 5. VPC Security Groups for Network-Level Isolation Defense in depth means layering. VPC security groups https://aws.amazon.com/vpc/?trk=d76afd77-bb62-46ac-b0a3-9dbf5ecde253 restrict which agents can even attempt to communicate: - Payment agent only accepts inbound from the orchestrator - Inventory agent only accepts inbound from the orchestrator - No agent can talk directly to any other agent - All traffic must flow through the validated path If an agent gets compromised, it can't laterally move to other agents because the network itself prevents it. 6. PrivateLink: Network-Level Agent Isolation If your agents communicate across VPCs or accounts, AWS PrivateLink https://aws.amazon.com/privatelink/?trk=d76afd77-bb62-46ac-b0a3-9dbf5ecde253 ensures that traffic never traverses the public internet. Each agent exposes its API as a VPC endpoint service. Other agents connect via interface endpoints in their own VPC. Why this matters for inter-agent security: - Traffic stays on the AWS backbone, no internet exposure - You control exactly which VPCs and therefore which agents can connect - Combined with security groups, you get network-level allowlisting of agent-to-agent communication paths - An attacker who compromises one agent can't reach other agents unless the PrivateLink connection explicitly exists Think of it as the network equivalent of "deny by default" - agents can't even see each other unless you've explicitly wired up the endpoint. 7. Amazon Verified Permissions: Semantic Authorization Between Agents Network-level controls tell you which agents can connect. Amazon Verified Permissions https://aws.amazon.com/verified-permissions/?trk=d76afd77-bb62-46ac-b0a3-9dbf5ecde253 Cedar tells you what they're allowed to ask for once connected. cedar // Agent A customer-service can ask Agent B payments to look up orders // but NOT to issue refunds permit principal == Agent::"customer-service", action == Action::"invoke", resource == Tool::"payments/lookup-order" ; // Only the refund-approver agent can invoke the refund tool permit principal == Agent::"refund-approver", action == Action::"invoke", resource == Tool::"payments/process-refund" ; This stops the confused deputy problem cold. Even if Agent A gets compromised and tries to tell Agent B "process a refund," the Cedar policy rejects it because Agent A doesn't have invoke permission on payments/process-refund . The authorization check happens independently of whatever the message says . The Architecture: Secure Multi-Agent Communication Layer these together: 1. Network - VPC security groups + PrivateLink restrict who can talk to whom 2. Transport - Mutual TLS proves identity on every connection 3. Structure - EventBridge schema registry / API Gateway request validation rejects malformed messages 4. Integrity - HMAC signing detects tampering 5. Authorization - Verified Permissions Cedar controls what agents can ask each other to do 6. Orchestration - Step Functions controls the flow, filters inputs, enforces timeouts 7. Audit - Every message logged in CloudWatch/CloudTrail An attacker needs to bypass all seven layers to successfully inject a malicious inter-agent message. That's the point. Key Takeaway Don't let your agents talk to each other unsupervised. Route inter-agent communication through a validated, schema-enforced, auditable orchestration layer. Validate structure, verify identity, sign for integrity, and restrict the network paths. The same zero-trust principles that saved microservices will save multi-agent systems. Up Next Post 8: Cascading Failures ASI08 https://dev.to/aws/cascading-failures-when-one-agents-mistake-takes-down-the-whole-system-asi08-5b3j - One hallucination, ten downstream agents acting on it. How Step Functions error handling, circuit breakers, and CloudWatch composite alarms keep a single agent failure from taking down your entire system. I would be very interested to hear your thoughts or comments, so please feel free to ping me on LinkedIn https://www.linkedin.com/in/maishsk/ or Twitter https://x.com/maishsk , or drop them below. If you're building multi-agent systems and have solved the communication security problem differently, I genuinely want to hear about it. Onward