All writing

Agent Trust Boundaries: The Security Layer Multi-Agent Systems Skip

Inter-agent calls in most production systems carry zero authentication — one agent asks another to execute, and the second just does it. That's not a trust model; that's a shared password written on a whiteboard.

Building multi-agent systems at Etera AI — where a travel planning platform coordinates research, pricing, booking, and post-sale agents in parallel — this gap shows up fast. Teams spend real effort on tool-level auth (OAuth, API keys, scoped credentials) and almost none on the trust boundary between agents. The assumption is that if an agent is running inside your infrastructure, it's inherently trusted. That assumption breaks badly once you have five or more agents, any degree of parallelism, or a sub-agent that calls an external LLM provider.

Why Agent-to-Agent Trust Is a Different Problem

Tool trust is well-understood: you call an external API, you present a credential, the API validates scope. Mature teams handle this correctly.

Agent trust is structurally different. When Agent A asks Agent B to do something, the message is a natural-language or JSON payload passed over an internal queue, a function call, or an HTTP endpoint. There's no inherent credential in that message. Agent B has no way to verify:

  • That the request actually originated from Agent A and not from prompt injection downstream
  • That Agent A was authorized to make this specific request (not just any request)
  • That the task payload hasn't been modified in transit by a compromised upstream step

This is the privilege escalation vector nobody draws on architecture diagrams. A low-privilege agent that can speak to a high-privilege agent can become a high-privilege agent if there's no boundary enforcement. In a multi-agent LLM system, "prompt injection via a tool result" is a real, documented attack path — a malicious string in a retrieved document can instruct a retrieval agent to forward a crafted payload to an execution agent.

The Four Trust Failure Modes in Production

1. Ambient authority inheritance Agent B inherits the full capability set of the orchestrator, even when invoked for a narrow subtask. If your orchestrator has write access to a database and spins up a summarization sub-agent, that sub-agent often runs with the same credentials unless you've explicitly constrained it. This violates least-privilege at the agent layer.

2. Unverified message origin Most agent frameworks pass task messages as plain dicts or JSON objects. Nothing in the message proves it came from the declared source. An injected instruction in a tool response can forge the "sender" field if agents naively trust message metadata.

3. Scope creep via re-prompting An orchestrator agent gets a user request scoped to "read customer data." It re-prompts a sub-agent with a synthesized instruction that happens to include "and update the record." The sub-agent, having write access and no scope enforcement, complies. No single agent "misbehaved" — the system as a whole exceeded intended authorization.

4. Cascade failure from a compromised agent If one agent in a chain is fed adversarial input that causes it to emit malformed or malicious downstream instructions, and there's no validation layer between agents, the blast radius is the entire downstream chain. Explicit state machines contain the control-flow problem, but they don't solve the trust problem on their own — that requires enforcing scope at the message boundary, not just at the transition.

A Practical Trust Model for Multi-Agent Systems

The right mental model is borrowed from zero-trust networking: never assume, always verify, least privilege by default. This is well-established in network security (see NIST SP 800-207); applying it to agent architectures means treating every inter-agent invocation as if it crossed a network boundary, even when it hasn't. Applied to agents:

code
Agent Request Flow:
[Orchestrator] --signed task token--> [Sub-agent]
                                          |
                               [verify: origin, scope, TTL]
                                          |
                               [execute within granted scope only]
                                          |
                               [return signed result]

Concretely, this means three layers. These aren't uniquely my invention — they're an application of well-known principles (zero-trust, least-privilege, input validation) to the agent layer. The gap isn't awareness of the principles; it's that most teams never explicitly apply them here.

Layer 1 — Task Tokens with Explicit Scope

Every inter-agent invocation carries a short-lived token that encodes the calling agent's identity, the permitted operations for this invocation, a TTL, and a request ID for traceability. JWT is a reasonable format for this — it's signed, inspectable, and has library support in every major language. The orchestrator mints the token; the sub-agent validates it before executing.

python
import jwt
import time

def mint_task_token(
    calling_agent: str,
    permitted_tools: list[str],
    secret: str,
    ttl_seconds: int = 60
) -> str:
    payload = {
        "iss": calling_agent,
        "permitted_tools": permitted_tools,
        "iat": int(time.time()),
        "exp": int(time.time()) + ttl_seconds,
    }
    return jwt.encode(payload, secret, algorithm="HS256")

def validate_task_token(
    token: str,
    required_tool: str,
    secret: str
) -> bool:
    try:
        claims = jwt.decode(token, secret, algorithms=["HS256"])
        return required_tool in claims.get("permitted_tools", [])
    except jwt.ExpiredSignatureError:
        return False  # expired tokens are hard-rejected, not retried silently
    except jwt.InvalidTokenError:
        return False

This is illustrative — production use needs async key rotation, a claims registry, and audit logging. The critical rule is the same regardless of implementation: a sub-agent that receives an invalid or missing token refuses the task and logs the attempt — it never falls back to ambient authority.

Layer 2 — Scope Manifests per Agent Role

Define a capability manifest for each agent role at deploy time, not at runtime. Think of it as a service account with explicit permissions — this is a standard IAM pattern applied to the agent layer:

Agent RolePermitted ToolsMax Token BudgetCan Spawn Sub-agents
Orchestratorroute, mint_token32kYes
Retrieval Agentvector_search, web_fetch8kNo
Summarization Agentread_context16kNo
Execution Agentdb_write, api_call8kNo

The execution agent should never have mint_token permission. That's how you prevent privilege escalation via a compromised execution path.

Scope manifests belong in version control alongside your agent definitions. If a manifest changes — say, someone adds db_write to the retrieval agent — that's a code review event, not a config tweak. The organizational forcing function matters as much as the technical control.

Layer 3 — Input/Output Validation at Agent Boundaries

Every message crossing an agent boundary should be validated against a schema before the receiving agent processes it. This is where prompt injection is actually stopped — not by hoping the LLM will ignore adversarial content, but by rejecting malformed or suspiciously oversized payloads at the boundary. Legitimate orchestration tasks don't need a 10,000-character instruction field; set hard limits and enforce them.

For the model's own output before it's forwarded downstream, structured output modes (OpenAI's response_format: json_schema, Anthropic's tool-use response format) give you machine-readable results that don't carry raw LLM prose into the next agent's context. Raw prose forwarded as instructions is the primary injection vector — structured output removes the ambiguity.

What This Costs You (and Why It's Worth It)

Token minting and validation adds roughly 2–5ms per inter-agent hop. For agentic workflows where individual LLM calls typically run 500ms–3s, this is noise. The real cost is design time: you have to enumerate permitted tools for each agent role before you deploy, which forces the architectural conversation most teams skip.

That forced conversation is actually the point. Teams that can't articulate what each agent is allowed to do are teams that will debug a privilege-escalation incident at 2am instead of in a design doc.

The observability angle matters here too. When you log every task token — issuer, scope, TTL, result — you get a full audit trail of agent-to-agent calls. Most observability tooling today captures tool calls but not inter-agent invocations as a distinct event type. Wiring in token logging fills that gap without a separate tracing layer. Trust enforcement and observability should be designed together; bolting observability on after the fact means you're reading logs that don't have the context you need when something goes wrong.

The Organizational Anti-Pattern

The reason most teams skip agent trust boundaries isn't laziness — it's sequencing. Security reviews happen at the API and infrastructure layer. The agent orchestration layer is owned by the ML or product team, which doesn't have a security-first reflex. Nobody in the process explicitly owns the inter-agent trust question.

Fix this by adding one line to every agent architecture review: "What is each agent permitted to do, and how does the receiving agent verify that permission?" If the answer is "it's inside our VPC, so it's fine," the design is incomplete.

The blast radius of getting this wrong scales with agent count. Two agents talking to each other is manageable. Imagine a system with twelve agents in a parallel workflow — some calling external LLM APIs, some with write access to production systems. Feed one of them adversarial input and you have a lateral movement scenario. That's not hypothetical architecture paranoia; it follows directly from how LLM tool-use chains propagate context.

The teams I see get this right aren't necessarily more security-conscious — they've just been forced into the conversation by a near-miss or a design review that asked the right question early.

What to Actually Do

  1. Audit your current inter-agent calls today. List every place one agent invokes another. For each, answer: does the receiving agent verify the caller's identity and scope? If the answer is no, that's your risk register.

  2. Implement scoped task tokens for any agent with write access or the ability to spawn sub-agents. Start there, not everywhere. Write-plus-spawn is the high-severity surface. JWT is a practical starting point; the signing scheme matters less than the habit of issuing and validating tokens at all.

  3. Define scope manifests for each agent role and put them in version control. Even a YAML file with a list of permitted tool names is better than nothing. It makes permission grants visible, reviewable, and auditable.

  4. Validate inputs at every agent boundary before they reach the model. Schema validation on inter-agent messages, hard limits on field sizes, rejection of malformed payloads — this is the cheapest prompt-injection mitigation in your stack.

  5. Log token issuance and validation outcomes to your existing observability stack. You want a queryable record of which agent asked which agent to do what, and whether it was authorized. Build this at the same time as the token layer, not after.

Agent trust isn't a future problem — it's the problem your current system already has, just quietly.

Working on something like this? I take on a few fractional-CTO and AI engagements at a time.

The AI CTO playbook

Get my AI playbooks — straight to your inbox

Practical notes on shipping production AI, scaling teams, and the calls a CTO actually has to make. A few times a month. No spam, no fluff.