Module B6 — Inter-Agent Trust and Communication Security

Course: 2B — Securing & Attacking Harnesses and LLMs Module: B6 — Inter-Agent Trust and Communication Security Duration: 60 minutes Level: Senior Engineer and above Prerequisites: B2 complete (the five-layer injection defense); B5 complete (non-human identities, per-task scoped credentials, credential isolation); Course 1 Module 4.2 (multi-agent coordination patterns)

When agents talk to each other, the message channel is a trust boundary. A compromised agent can forge a message that looks like it came from the orchestrator, escalating privileges, impersonating a peer, or triggering a cascade across the mesh. The fix is not better models or better prompts. It is that every inter-agent message carries a signature the recipient verifies, every message is fresh (nonces, timestamps, no replays), and the orchestrator — not the agents — enforces compartment boundaries. This is the module where the identity layer from B5 becomes the trust substrate for the inter-agent channel.


Learning Objectives

After completing this module, you will be able to:

  1. Explain why an inter-agent message channel is a trust boundary — a compromised agent can forge messages to escalate privileges, impersonate peers, or trigger cascading actions — and map this to Microsoft's Failure Mode Taxonomy v2.0 #3 (Inter-Agent Trust Escalation) and OWASP ASI06 (Cascading Hallucination).
  2. Describe the three attacks on the inter-agent channel — trust escalation (forged orchestrator message), cascade (one output triggers another's action, propagating across the mesh), and replay (capturing and re-injecting a legitimate message out of context) — and the cryptographic and structural control for each.
  3. Implement a signed inter-agent message protocol with replay protection — HMAC (or asymmetric signatures for cross-domain), per-message nonces, timestamp freshness windows, and message IDs — extending DD-16 ZeroClaw's HMAC tool receipts to the inter-agent channel.
  4. Design compartmentalization for multi-agent meshes — trust compartments enforced in the orchestrator, not in the agents themselves — so a low-trust research agent cannot direct a high-trust execution agent without an explicit, audited boundary crossing.
  5. Build cascade prevention: session-level intent tracking, per-action authorization, and blast-radius caps (cascade depth, fan-out) on multi-agent meshes, tying to B8's observability substrate.
  6. Identify the anti-patterns — implicit trust between agents, unsigned messages, agents reading their own credentials, mesh-wide blast radius — and the structural fix for each.

Why this module exists

B5 built the identity layer: agents get non-human identities, per-task scoped credentials, and credential isolation so the agent never holds a raw secret. B5 left one surface untouched: what happens when two agents talk to each other.

That surface is now the worst blast-radius surface on the B1 surface map. The B1 threat model named inter-agent edges "surface seven" and ranked its blast radius as mesh-wide — the worst case — because most multi-agent systems default to implicit trust: shared context, no message authentication, unverified peer outputs. A compromise of any one node, or a single forged message, propagates to every peer.

The inversion from single-agent security is the load-bearing point. In a single-agent system, the trust boundary is between the user and the agent, and between the agent and its tools (B2, B4, B5). In a multi-agent mesh, every peer relationship is a trust boundary. An orchestrator delegating to three executors is running three trust relationships. A supervisor reviewing a sub-agent is running a fourth. Peer agents exchanging intermediate results are running a fifth. Each one is a boundary an attacker can cross by forging a message, and each one is a channel a compromised agent can use to escalate.

This is the module where "the agent is the threat" stops being a hypothetical. An attacker who compromises one low-trust agent in the mesh — via prompt injection, memory poisoning, or a compromised tool — now has a vantage point inside the trust fabric. The question is no longer "can the attacker reach the agent" (B2 answered that) or "what can the agent do once reached" (B4, B5 answered that). The question is: can a compromised agent forge a message that makes a higher-trust agent do something it should not? In most deployed meshes, the answer is yes, because the messages are unauthenticated.

Three sub-sections, twenty minutes each:


B6.1 — The multi-agent trust problem

When the system is a mesh, the message channel between any two agents is a trust boundary as load-bearing as the boundary between the user and the agent.

The inter-agent channel as a trust boundary

Consider a representative multi-agent mesh: an orchestrator that decomposes a task, three executor agents that each own a subtask (research, code, deployment), and a supervisor agent that reviews executor outputs before committing. This is the standard pattern — Course 1 Module 4.2 covered the coordination topologies. The mesh runs on messages: the orchestrator sends a delegation; an executor returns a result; the supervisor sends an approval or rejection.

Each of those messages is a trust artifact. The recipient acts on it. The executor acts on the orchestrator's delegation (it runs the subtask). The supervisor acts on the executor's result (it approves or rejects). The orchestrator acts on the supervisor's approval (it commits). If any of those messages can be forged, the forger controls the downstream action.

Now ask the structural question: how does the recipient know the message is authentic? In most deployed meshes, the answer is: it does not. The message arrives on a shared message bus, a queue, a context object, or a function-call argument, and the recipient processes it based on its content — a field that says from: orchestrator, a role header, or nothing at all. The recipient has no way to verify that the message actually came from the claimed sender, because the channel does not authenticate senders. This is the default state of multi-agent systems, and it is the root cause of every attack in this section.

This is a trust boundary failure, and it has a precise analogue in traditional security. An HTTP request from a browser carries a Cookie header that claims the caller's identity — but the server does not trust the header; it verifies a cryptographic token. An inter-agent message that carries a from field claiming the sender's identity is the same artifact, except that most meshes trust the field without verifying it. The fix is the same: authenticate the sender cryptographically, not by reading a field.

Microsoft Failure Mode Taxonomy v2.0 #3 — Inter-Agent Trust Escalation

The canonical framing of this attack is Microsoft's AI Red Team Failure Mode Taxonomy v2.0, failure mode #3: Inter-Agent Trust Escalation. The taxonomy, published by the Microsoft AI Red Team after a year of agentic red-teaming, names the specific failure: an attacker (or a compromised agent) forges a message between agents to escalate privileges beyond what the forging agent is authorized to do.

The attack pattern:

  1. The mesh has agents with different trust levels — a low-trust research agent (read-only, no tools that commit state) and a high-trust execution agent (can deploy code, write to production stores).
  2. The low-trust agent is compromised — via prompt injection (B2), memory poisoning (B3), or a compromised tool (B4). The attacker now controls the low-trust agent's outputs.
  3. The compromised agent sends a message to the high-trust agent that claims to be from the orchestrator. The message says, in substance: "the user approved this deployment; execute it now."
  4. The high-trust agent, which trusts the orchestrator (correctly) but has no way to verify the sender (incorrectly), executes the deployment.

The escalation is real: a read-only agent has triggered a production deployment. The compromise is invisible to the high-trust agent, because the message looked authoritative. This is the multi-agent analogue of a confused deputy (B4 covered the confused-deputy problem for tools; here the deputy is another agent). The high-trust agent is the deputy; the forged orchestrator message is the abuse of its authority.

The Microsoft taxonomy is the reference because it is empirical — drawn from a year of red-teaming real agentic systems — and because it names the structural cause explicitly: inter-agent messages are trusted based on content, not on authenticated sender identity. The fix is named in the same taxonomy: authenticate messages, isolate agents into trust compartments, and cap cascade depth.

Cascade risk — one input, mesh-wide damage

The second attack is the cascade. It maps to OWASP ASI06 (Cascading Hallucination) and is the blast-radius amplifier the B1 threat model flagged.

The mechanism: one agent produces an output (a hallucinated fact, a poisoned result, a forged instruction) that is consumed by another agent as an input. That second agent acts on it and produces an output consumed by a third. The error — or the forged instruction — propagates through the mesh. A single malicious input at any node becomes N actions across M agents.

The cascade is not a single attack; it is a property of the mesh topology. Two features make it severe:

  1. Implicit trust propagates. If agents consume peer outputs as ground truth (the default), a poisoned output from agent A is treated as fact by agent B, whose output is treated as fact by agent C. There is no verification at any edge. The cascade amplifies because each hop adds confidence — agent C's output is derived from "two sources" (B and the orchestrator), even though B's input was A's poison.
  2. Fan-out is large. An orchestrator that delegates to ten executors has a fan-out of ten. A single compromised orchestrator output poisons all ten. A mesh with peer-to-peer communication has worse fan-out — each peer's output reaches every other peer.

The blast radius is mesh-wide because the mesh is connected and the trust is implicit. B1 named this the worst case on the surface map. The defense is not to detect each cascade after it starts (that is B8 observability); the defense is to prevent propagation — signed messages so a forged peer output is rejected, output verification at every edge (never consume a peer's output as fact without checking), and cascade-depth caps so a propagating chain hits a hard limit.

Replay attacks — capturing and re-injecting a legitimate message

The third attack is replay. It is subtler than forgery and is the attack most teams miss entirely.

A replay attack captures a legitimate, correctly-signed inter-agent message and re-injects it later, out of its original context. The signature verifies (the message was genuinely signed by the orchestrator); the content is benign in its original context (an approval for a specific deployment); but the context has changed (the deployment already happened, or the approval was for a different task).

The classic instance: an orchestrator sends an approve_deployment message for task 42. The deployment runs. An attacker (or a compromised agent) captures the signed message and replays it later — the high-trust agent verifies the signature (valid), reads the content ("approve deployment"), and executes a second deployment that was never authorized. The signature did not help, because the message was authentic; the problem is that it was not fresh.

Replay attacks defeat a signature-only defense. The fix is replay protection: every message carries a nonce (a unique, single-use value) and a timestamp, and the recipient rejects any message whose timestamp falls outside a freshness window or whose nonce it has already seen. Message IDs tie the message to a specific task/session so an approval for task 42 cannot be replayed as an approval for task 43. This is the same replay-protection discipline that TLS, Kerberos, and OAuth use; it is not novel, but it is absent from most multi-agent channels.

OWASP mapping: ASI06 and ASI07

Two OWASP Agentic Top 10 risks land squarely on the inter-agent channel:

The pattern across both: the inter-agent channel treats peer outputs as trusted by default, when they should be treated as untrusted data that is verified before it is acted upon.


B6.2 — The defense: signed inter-agent messages

Every inter-agent message carries a signature the recipient verifies. Every message is fresh. This is DD-16 ZeroClaw's HMAC tool receipts, extended to the inter-agent channel — and extended with durable keys, which Course 1 flagged as an open gap.

The signed-message contract

The defense against inter-agent trust escalation is a contract that every inter-agent message must satisfy before the recipient acts on it:

  1. Authenticity. The message is signed by a key only the claimed sender holds. The recipient verifies the signature with the corresponding public key (asymmetric) or shared secret (HMAC). A forged message fails verification.
  2. Integrity. The signature covers the entire message — sender, recipient, payload, task ID, timestamp, nonce. Any tampering invalidates the signature.
  3. Freshness. The message carries a timestamp and a nonce. The recipient rejects messages outside a freshness window (e.g., ±60 seconds) and tracks seen nonces to reject replays.
  4. Binding. The message is bound to a task/session ID and an intent. An approval for task 42 cannot be replayed against task 43, because the task ID is in the signed payload and the recipient checks it.

This contract is not novel. It is the same contract TLS, JWT, and mTLS enforce. What is novel — and what this module adds — is applying it to the inter-agent channel, which most deployed meshes leave entirely unprotected.

HMAC vs. asymmetric signatures

Two signature schemes, chosen by the trust topology:

The rule: HMAC within a trust domain, asymmetric across trust domains. A mesh where all agents are operated by one team and run in one process can use HMAC. A mesh with cross-team or cross-organization agents needs asymmetric signatures. This mirrors the mTLS precedent: service-to-service authentication inside a cluster can use a shared CA and mTLS; cross-cluster needs SPIFFE/SPIRE-style identity (B5's workload identity).

The durable-key gap — extending DD-16 ZeroClaw

DD-16 ZeroClaw (Course 1) implements HMAC tool receipts: every tool call and its output is signed with an HMAC so the receipt can be verified later. This is the right primitive, and it is the reference architecture for this module. But Course 1 flagged an open gap: ZeroClaw's signing keys are ephemeral — in-memory only, not persisted.

That gap is load-bearing for the inter-agent channel. An ephemeral key authenticates within a session but not across sessions. A message signed in session 1 cannot be verified in session 2 (the key is gone). A process restart invalidates every signature. And — critically for B3's sleeper attack — a poisoned message persisted to memory in session 1 and retrieved in session 2 cannot be verified, because the signing key no longer exists.

B6 closes this gap with durable, rotated signing keys. The design:

  1. Key persistence. Signing keys live in the secrets vault B5 specified (the vault the harness accesses, not the agent process). The key survives restarts and session boundaries.
  2. Key rotation. Keys are rotated on a schedule (e.g., every 24 hours) and on compromise. Old keys are retained for a verification window (e.g., 7 days) so messages signed before rotation can still be verified, then destroyed.
  3. Key IDs. Every signed message carries a key_id so the recipient knows which key to verify with — essential once rotation begins. This is the JWT kid header pattern.
  4. Asymmetric option for cross-domain. When agents cross trust domains, each agent (or each trust compartment) has its own Ed25519 keypair. The orchestrator's compartment signs with its private key; executors verify with the orchestrator's public key, distributed out-of-band.

The durable-key extension is what turns ZeroClaw's in-session HMAC receipts into a defense that survives the session boundary — which is exactly the boundary the sleeper (B3) and cascade (ASI06) attacks exploit.

Replay protection — nonces, timestamps, message IDs

A signed message that is not fresh is replayable. The replay-protection layer:

The three together — timestamp window + nonce uniqueness + task binding — close the replay surface. Drop any one and a replay path opens: drop timestamps and an attacker can replay a captured message indefinitely (the nonce cache is bounded); drop nonces and an attacker can replay within the freshness window; drop task binding and an approval for one task can be replayed against another.

The signed inter-agent message protocol — code

A runnable TypeScript implementation of the sign/verify protocol with replay protection. This extends DD-16 ZeroClaw's HMAC receipts with durable keys (key store) and full replay protection.

import { createHmac, timingSafeEqual, randomBytes } from "node:crypto";

// ── Types ──────────────────────────────────────────────────────────────────

/** A signed inter-agent message. The recipient verifies before acting. */
interface SignedMessage {
  header: {
    from: string;          // sender agent id (e.g. "orchestrator")
    to: string;            // recipient agent id (e.g. "executor-deploy")
    task_id: string;       // binds the message to a task/session
    intent: string;        // e.g. "approve_deployment", "delegate_research"
    timestamp_ms: number;  // UTC millis — freshness window check
    nonce: string;         // single-use — replay defense
    key_id: string;        // which key signed (supports rotation)
  };
  payload: unknown;        // the message body — task-specific
  signature: string;       // HMAC over header + payload (hex)
}

// ── Key store (durable — the harness accesses it, not the agent) ────────────

/** Maps key_id -> secret. In production this is the secrets vault from B5,
 *  NOT a process-local Map. Durable across restarts; rotated on schedule. */
class KeyStore {
  private keys = new Map<string, Buffer>();
  private currentKeyId: string;

  constructor(initial: Map<string, Buffer>, currentKeyId: string) {
    this.keys = initial;
    this.currentKeyId = currentKeyId;
  }

  get(keyId: string): Buffer | undefined {
    return this.keys.get(keyId);
  }

  current(): { keyId: string; secret: Buffer } {
    const secret = this.keys.get(this.currentKeyId);
    if (!secret) throw new Error(`current key ${this.currentKeyId} missing`);
    return { keyId: this.currentKeyId, secret };
  }

  /** Rotate: add a new key, set it current, retain the old for verification. */
  rotate(newKeyId: string, newSecret: Buffer): void {
    this.keys.set(newKeyId, newSecret);
    this.currentKeyId = newKeyId;
  }
}

// ── Sign ───────────────────────────────────────────────────────────────────

/** The sender (orchestrator or any agent) signs a message before sending.
 *  The signature covers header + payload — any tampering invalidates it. */
function sign(
  from: string,
  to: string,
  task_id: string,
  intent: string,
  payload: unknown,
  keys: KeyStore,
  now: number = Date.now(),
): SignedMessage {
  const { keyId, secret } = keys.current();
  const header = {
    from,
    to,
    task_id,
    intent,
    timestamp_ms: now,
    nonce: randomBytes(16).toString("hex"),
    key_id: keyId,
  };
  const canonical = JSON.stringify({ header, payload });
  const signature = createHmac("sha256", secret)
    .update(canonical)
    .digest("hex");
  return { header, payload, signature };
}

// ── Verify (with replay protection) ─────────────────────────────────────────

/** Replay-protection state: tracks seen nonces within a TTL. */
class NonceCache {
  private seen = new Map<string, number>(); // nonce -> expires_at_ms
  constructor(private ttlMs: number) {}

  /** Returns true if the nonce is fresh (not seen, or its prior entry expired). */
  private cleanup(now: number): void {
    for (const [nonce, expires] of this.seen) {
      if (expires <= now) this.seen.delete(nonce);
    }
  }

  /** Returns true if nonce is fresh; records it. Caller rejects on false. */
  check(nonce: string, now: number): boolean {
    this.cleanup(now);
    if (this.seen.has(nonce)) return false; // replay
    this.seen.set(nonce, now + this.ttlMs);
    return true;
  }
}

/** Result of verification — the recipient acts only on OK. */
type VerifyResult =
  | { ok: true; message: SignedMessage }
  | { ok: false; reason: string };

/** The recipient verifies a message BEFORE acting on it.
 *  Checks: signature, freshness window, nonce uniqueness, task binding. */
function verify(
  msg: SignedMessage,
  keys: KeyStore,
  nonces: NonceCache,
  opts: {
    expectedTaskId: string;
    expectedRecipient: string;
    freshnessMs: number;   // e.g. 60_000
    now: number;           // injected for testing
  },
): VerifyResult {
  const h = msg.header;

  // 1. Recipient check — was this message addressed to me?
  if (h.to !== opts.expectedRecipient) {
    return { ok: false, reason: `wrong recipient: ${h.to}` };
  }

  // 2. Task binding — does the message belong to the current task?
  if (h.task_id !== opts.expectedTaskId) {
    return { ok: false, reason: `task_id mismatch: ${h.task_id}` };
  }

  // 3. Freshness window — is the timestamp within tolerance?
  const skew = Math.abs(opts.now - h.timestamp_ms);
  if (skew > opts.freshnessMs) {
    return { ok: false, reason: `stale: ${skew}ms outside ${opts.freshnessMs}ms window` };
  }

  // 4. Replay — is the nonce fresh?
  if (!nonces.check(h.nonce, opts.now)) {
    return { ok: false, reason: `replayed nonce: ${h.nonce}` };
  }

  // 5. Signature — does the HMAC verify with the claimed key?
  const secret = keys.get(h.key_id);
  if (!secret) {
    return { ok: false, reason: `unknown key_id: ${h.key_id} (expired or rotated out)` };
  }
  const canonical = JSON.stringify({ header: h, payload: msg.payload });
  const expected = createHmac("sha256", secret).update(canonical).digest("hex");

  // Constant-time compare — prevents timing oracles on signature check.
  const a = Buffer.from(msg.signature, "hex");
  const b = Buffer.from(expected, "hex");
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return { ok: false, reason: "signature invalid" };
  }

  return { ok: true, message: msg };
}

This is the engineering realization of the signed-message contract. Five checks, all of which must pass before the recipient acts: recipient, task binding, freshness, nonce, signature. Drop one and a path opens. The key store is the durable extension of ZeroClaw's ephemeral keys; the nonce cache is the replay defense ZeroClaw did not have.


B6.3 — Compartmentalization and cascade prevention

Signed messages authenticate the channel. Compartmentalization limits what an authenticated agent can ask another to do. Cascade prevention limits how far an action propagates. Together they bound the blast radius of a mesh compromise.

Compartmentalization — trust boundaries enforced in the orchestrator

Signed messages solve forgery. They do not solve the problem of a legitimately signed message asking a high-trust agent to do something dangerous. An orchestrator that has been compromised (via injection) can sign perfectly valid messages directing executors to exfiltrate. The signature verifies; the action is malicious. Authentication is necessary; it is not sufficient.

The second control is compartmentalization: agents are partitioned into trust compartments, and the movement of a request from a lower-trust compartment to a higher-trust one is an explicit, audited boundary crossing that the orchestrator — not the agents — enforces.

The design:

  1. Each agent has a trust level. A research agent is low-trust (read-only, no commit authority). An executor is medium-trust (can write to staging). A deployment agent is high-trust (can write to production). These levels are assigned by the orchestrator's policy, not self-declared by the agent.
  2. The orchestrator mediates every cross-compartment request. When the research agent's output needs to drive the execution agent, the request does not go agent-to-agent; it goes through the orchestrator, which applies a policy: "a low-trust agent's output may inform a high-trust agent's task only if (a) the high-trust agent's task was independently authorized, and (b) the output is verified data, not an instruction."
  3. The boundary crossing is logged and rate-limited. Every cross-compartment request is an audit event. A burst of low→high crossings trips an alert (ties to B8 observability).

The load-bearing principle: the compartment boundary is enforced in the orchestrator, not in the agents themselves. An agent cannot be trusted to enforce its own trust level — a compromised agent will happily claim whatever level gets its message through. The orchestrator — a smaller, more auditable, more deterministic piece of code — is the policy enforcement point. This is the same principle as B4's tool gate (the harness enforces capability, not the model) and B5's scope check (the harness enforces privilege, not the agent): the deterministic component enforces the trust boundary, because the model-in-the-loop component can be coerced.

Session-level intent tracking

Cascade prevention starts with knowing what the mesh is supposed to be doing. The mechanism is session-level intent tracking: the orchestrator records the authorized intent of the session (e.g., "research topic X and summarize; do not deploy") when the session starts, and every agent action is checked against that intent.

This is the inter-agent analogue of B2's input classification (is this input data or instruction?) and B5's scope check (does this token cover this action?). The check is: does this inter-agent message's intent match the session's authorized intent? A research agent that starts emitting deploy intents is out of policy — regardless of whether its message is signed. The orchestrator rejects it.

Session-level intent tracking is what makes cascade prevention deterministic rather than heuristic. A policy that says "this session's intent is research; deploy intents are denied" is a compiled boolean — the agent cannot talk its way past it. This is the B2.3 resolution (determinism on the trust boundary) applied to the inter-agent channel.

Per-action authorization and blast-radius caps

Two structural controls bound the damage when a compromised agent does get a message through:

  1. Per-action authorization. Every action an agent takes — every tool call, every cross-agent delegation, every commit — is authorized against the session intent and the agent's trust level, at the moment of action. This is B5's scope-check middleware applied to the inter-agent channel: the orchestrator verifies that this specific action, from this specific agent, in this specific task context, is permitted. An agent that was authorized to read in turn 1 is re-checked before it writes in turn 2.
  2. Blast-radius caps. The orchestrator enforces hard limits on cascade propagation: a cascade-depth cap (no chain of agent-to-agent delegations longer than N — e.g., 3), a fan-out cap (no single agent delegates to more than M peers), and a session action budget (no more than K state-changing actions per session). These are circuit breakers: when a cascade starts, it hits a hard limit and stops, rather than propagating through the entire mesh.

These caps are blunt, and that is the point. A cascade is, by definition, a propagation you did not anticipate. The defense against an unanticipated propagation is a structural limit that does not depend on anticipating the specific attack. B8 (observability) detects the cascade after it starts; the blast-radius cap ensures the cascade stops before it consumes the mesh.

The defense-in-depth stack

The full inter-agent trust defense, in order of enforcement:

Layer Control Defeats
1. Signed messages HMAC/asymmetric signature, recipient verifies Forgery, impersonation
2. Replay protection Nonces, timestamp window, task binding Replay of captured messages
3. Compartmentalization Trust compartments, orchestrator-enforced boundary crossings A low-trust agent directing a high-trust agent
4. Session intent tracking Authorized intent recorded at session start; every message checked Out-of-policy intents (research agent emitting deploy)
5. Per-action authorization Scope check at the moment of action (B5 applied) Privilege creep within a session
6. Blast-radius caps Cascade depth, fan-out, action budget Mesh-wide propagation

Each layer addresses a different failure. A signed message that is fresh, from a low-trust agent, with an out-of-policy intent, fails at layer 4. A signed, fresh, in-policy message from a compromised high-trust agent is bounded by layer 6. The stack is defense-in-depth because no single layer closes every path — and the inter-agent channel has more paths than any other surface on the B1 map.


Anti-Patterns

Implicit trust between agents — the mesh default

Agents consume peer messages and outputs as ground truth with no authentication or verification. Cure: every inter-agent message is signed and verified (B6.2); every peer output is treated as untrusted data until checked.

Unsigned messages with a from field

A message carries from: orchestrator and the recipient trusts the field. Cure: the from field is verified by the signature, not read from the payload. The recipient never acts on an unsigned message.

Ephemeral signing keys (the ZeroClaw open gap)

Signing keys live in memory only, so signatures do not survive restarts or session boundaries. Cure: durable, rotated keys in the secrets vault (B5), with key IDs and a verification window for rotated-out keys.

Replay protection omitted

Messages are signed but carry no nonce or timestamp, so a captured valid message can be replayed indefinitely. Cure: timestamp freshness window + single-use nonces + task binding. All three; drop one and a replay path opens.

Compartment boundaries enforced in the agents

An agent is trusted to check its own trust level before acting on a peer's request. Cure: the orchestrator enforces compartment boundaries — the deterministic component holds the policy, because the agent can be coerced.

No blast-radius caps on the mesh

A cascade propagates through the entire mesh because no depth, fan-out, or action-budget limit exists. Cure: hard caps — cascade depth, fan-out, session action budget — that trip before the cascade consumes the mesh. B8 detects; the cap stops.

Agents reading their own signing keys

An agent that can read its signing key can be coerced into signing arbitrary messages (the confused deputy, again). Cure: the harness signs on the agent's behalf — the key lives in the vault (B5), the agent never sees it. This is B5's credential isolation applied to signing keys.


Key Terms

Term Definition
Inter-agent trust escalation Microsoft Failure Mode Taxonomy v2.0 #3 — forging a message between agents (e.g., a low-trust agent forging an orchestrator message) to escalate privileges beyond the forger's authority
Cascade One agent's output triggers another's action, propagating through the mesh; OWASP ASI06. Amplified by implicit trust and large fan-out
Replay attack Capturing a legitimate signed message and re-injecting it out of context; defeated by nonces, timestamp windows, and task binding
Signed inter-agent message A message carrying a cryptographic signature (HMAC or asymmetric) the recipient verifies before acting; covers header + payload
DD-16 ZeroClaw HMAC receipts Course 1 reference architecture — HMAC-signed tool receipts; the precursor to inter-agent message authentication. Open gap: ephemeral keys; B6 closes it with durable, rotated keys
Nonce A single-use random value in a signed message; the recipient tracks seen nonces to reject replays
Freshness window The timestamp tolerance (e.g., ±60s) within which a message is accepted; messages outside the window are rejected as stale
Task binding The message's task_id is in the signed payload and checked against the current task — an approval for task 42 cannot be replayed against task 43
Trust compartment A partition of agents by trust level; cross-compartment requests are mediated and audited by the orchestrator
Compartment boundary enforcement The policy that a low-trust agent cannot direct a high-trust agent without an explicit, orchestrator-mediated crossing — enforced in the orchestrator, not the agents
Session intent tracking The orchestrator records the session's authorized intent at start; every agent message is checked against it (ties to B8)
Blast-radius caps Hard limits — cascade depth, fan-out, session action budget — that stop a propagating cascade before it consumes the mesh
ASI06 OWASP Cascading Hallucination — one poisoned/hallucinated output consumed as fact propagates through the mesh
ASI07 OWASP Insecure Output Handling — an agent's output consumed by a downstream agent without sanitization; peer output treated as trusted instruction rather than untrusted data

Lab Exercise

See 07-lab-spec.md — "Build the Signed Inter-Agent Channel." You will implement (a) a sign/verify message protocol with HMAC and durable keys (extending ZeroClaw), (b) replay protection with nonces, a timestamp freshness window, and task binding, and (c) a compartment boundary the orchestrator enforces. The lab also includes a trust-escalation attack demo: a compromised low-trust agent forges an orchestrator message, and you watch the verification layer reject it. Runnable TypeScript or Python, no GPU, ~60-75 minutes.


References

  1. Microsoft AI Red TeamFailure Mode Taxonomy v2.0 (2026). Failure Mode #3 (Inter-Agent Trust Escalation) is the canonical framing of the forged-message attack. ai-redteam.com/insights/updating-the-taxonomy-of-failure-modes-in-agentic-ai-systems-what-a-year-of-red/.
  2. OWASPTop 10 for Agentic Applications (2026). ASI06 (Cascading Hallucination) and ASI07 (Insecure Output Handling). genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/.
  3. DD-16 ZeroClaw — Course 1 deep-dive. 6-layer safety model with HMAC tool receipts; the precursor architecture this module extends. Open gap (ephemeral keys) closed here with durable, rotated keys.
  4. Course 1 Module 4.2 — Multi-agent coordination patterns (orchestrator-executor, supervisor, peer-to-peer). The topologies whose trust edges this module secures.
  5. B1 Threat Model — Surface 7 (Inter-Agent Edges); blast radius rated mesh-wide, the worst case on the surface map. The motivation for this module.
  6. B5 Identity and Permission Design — Non-human identities, per-task scoped credentials, credential isolation. The identity substrate the signed-message protocol builds on (key store in the vault the harness accesses, not the agent).
  7. B3 Memory Poisoning — The sleeper attack (ASI04) across session boundaries; the reason ZeroClaw's ephemeral keys are insufficient and durable keys are required.
  8. RFC 8693 — Token Exchange; the B5 mechanism. The signed-message protocol's task binding is the inter-agent analogue of token audience binding.
  9. RFC 7519 (JWT) — JSON Web Token; the kid header, timestamp (exp/nbf), and nonce (jti) patterns this module's message header mirrors.
  10. SPIFFE/SPIRE — Workload identity (B5). The cross-domain trust model for asymmetric inter-agent signatures where agents are operated by different teams.
  11. mTLS precedents — Service-to-service mutual authentication; the established pattern for authenticated channels that the inter-agent signed-message contract applies to agent-to-agent communication.
  12. Kerberos / TLS replay protection — The nonce, timestamp, and session-key disciplines that defeat replay; directly transposed to the inter-agent channel.
  13. B8 Observability (companion module) — Session-level intent tracking, cascade detection, and audit feed that the blast-radius caps and compartment crossings report into.
# Module B6 — Inter-Agent Trust and Communication Security

**Course**: 2B — Securing & Attacking Harnesses and LLMs
**Module**: B6 — Inter-Agent Trust and Communication Security
**Duration**: 60 minutes
**Level**: Senior Engineer and above
**Prerequisites**: B2 complete (the five-layer injection defense); B5 complete (non-human identities, per-task scoped credentials, credential isolation); Course 1 Module 4.2 (multi-agent coordination patterns)

> *When agents talk to each other, the message channel is a trust boundary. A compromised agent can forge a message that looks like it came from the orchestrator, escalating privileges, impersonating a peer, or triggering a cascade across the mesh. The fix is not better models or better prompts. It is that every inter-agent message carries a signature the recipient verifies, every message is fresh (nonces, timestamps, no replays), and the orchestrator — not the agents — enforces compartment boundaries. This is the module where the identity layer from B5 becomes the trust substrate for the inter-agent channel.*

---

## Learning Objectives

After completing this module, you will be able to:

1. Explain why an inter-agent message channel is a trust boundary — a compromised agent can forge messages to escalate privileges, impersonate peers, or trigger cascading actions — and map this to Microsoft's Failure Mode Taxonomy v2.0 #3 (Inter-Agent Trust Escalation) and OWASP ASI06 (Cascading Hallucination).
2. Describe the three attacks on the inter-agent channel — trust escalation (forged orchestrator message), cascade (one output triggers another's action, propagating across the mesh), and replay (capturing and re-injecting a legitimate message out of context) — and the cryptographic and structural control for each.
3. Implement a signed inter-agent message protocol with replay protection — HMAC (or asymmetric signatures for cross-domain), per-message nonces, timestamp freshness windows, and message IDs — extending DD-16 ZeroClaw's HMAC tool receipts to the inter-agent channel.
4. Design compartmentalization for multi-agent meshes — trust compartments enforced in the orchestrator, not in the agents themselves — so a low-trust research agent cannot direct a high-trust execution agent without an explicit, audited boundary crossing.
5. Build cascade prevention: session-level intent tracking, per-action authorization, and blast-radius caps (cascade depth, fan-out) on multi-agent meshes, tying to B8's observability substrate.
6. Identify the anti-patterns — implicit trust between agents, unsigned messages, agents reading their own credentials, mesh-wide blast radius — and the structural fix for each.

---

## Why this module exists

B5 built the identity layer: agents get non-human identities, per-task scoped credentials, and credential isolation so the agent never holds a raw secret. B5 left one surface untouched: what happens when two agents talk to each other.

That surface is now the worst blast-radius surface on the B1 surface map. The B1 threat model named inter-agent edges "surface seven" and ranked its blast radius as mesh-wide — the worst case — because most multi-agent systems default to implicit trust: shared context, no message authentication, unverified peer outputs. A compromise of any one node, or a single forged message, propagates to every peer.

The inversion from single-agent security is the load-bearing point. In a single-agent system, the trust boundary is between the user and the agent, and between the agent and its tools (B2, B4, B5). In a multi-agent mesh, *every peer relationship is a trust boundary*. An orchestrator delegating to three executors is running three trust relationships. A supervisor reviewing a sub-agent is running a fourth. Peer agents exchanging intermediate results are running a fifth. Each one is a boundary an attacker can cross by forging a message, and each one is a channel a compromised agent can use to escalate.

This is the module where "the agent is the threat" stops being a hypothetical. An attacker who compromises one low-trust agent in the mesh — via prompt injection, memory poisoning, or a compromised tool — now has a vantage point *inside* the trust fabric. The question is no longer "can the attacker reach the agent" (B2 answered that) or "what can the agent do once reached" (B4, B5 answered that). The question is: **can a compromised agent forge a message that makes a higher-trust agent do something it should not?** In most deployed meshes, the answer is yes, because the messages are unauthenticated.

Three sub-sections, twenty minutes each:

- **B6.1 — The multi-agent trust problem.** The inter-agent channel as a trust boundary; the three attacks (trust escalation, cascade, replay); Microsoft Failure Mode Taxonomy v2.0 #3 as the canonical framing; OWASP ASI06 and ASI07 mapping.
- **B6.2 — The defense: signed inter-agent messages.** HMAC and asymmetric signatures, replay protection (nonces, timestamps, message IDs), extending DD-16 ZeroClaw's HMAC tool receipts to the inter-agent channel. The durable-key gap Course 1 flagged.
- **B6.3 — Compartmentalization and cascade prevention.** Trust compartments enforced in the orchestrator, not the agents; session-level intent tracking; per-action authorization; blast-radius caps. Ties to B8 observability.

---

# B6.1 — The multi-agent trust problem

*When the system is a mesh, the message channel between any two agents is a trust boundary as load-bearing as the boundary between the user and the agent.*

## The inter-agent channel as a trust boundary

Consider a representative multi-agent mesh: an orchestrator that decomposes a task, three executor agents that each own a subtask (research, code, deployment), and a supervisor agent that reviews executor outputs before committing. This is the standard pattern — Course 1 Module 4.2 covered the coordination topologies. The mesh runs on messages: the orchestrator sends a delegation; an executor returns a result; the supervisor sends an approval or rejection.

Each of those messages is a trust artifact. The recipient *acts on it*. The executor acts on the orchestrator's delegation (it runs the subtask). The supervisor acts on the executor's result (it approves or rejects). The orchestrator acts on the supervisor's approval (it commits). If any of those messages can be forged, the forger controls the downstream action.

Now ask the structural question: **how does the recipient know the message is authentic?** In most deployed meshes, the answer is: it does not. The message arrives on a shared message bus, a queue, a context object, or a function-call argument, and the recipient processes it based on its *content* — a field that says `from: orchestrator`, a `role` header, or nothing at all. The recipient has no way to verify that the message actually came from the claimed sender, because the channel does not authenticate senders. This is the default state of multi-agent systems, and it is the root cause of every attack in this section.

This is a trust boundary failure, and it has a precise analogue in traditional security. An HTTP request from a browser carries a `Cookie` header that claims the caller's identity — but the server does not trust the header; it verifies a cryptographic token. An inter-agent message that carries a `from` field claiming the sender's identity is the same artifact, except that most meshes trust the field without verifying it. The fix is the same: authenticate the sender cryptographically, not by reading a field.

## Microsoft Failure Mode Taxonomy v2.0 #3 — Inter-Agent Trust Escalation

The canonical framing of this attack is **Microsoft's AI Red Team Failure Mode Taxonomy v2.0, failure mode #3: Inter-Agent Trust Escalation**. The taxonomy, published by the Microsoft AI Red Team after a year of agentic red-teaming, names the specific failure: an attacker (or a compromised agent) forges a message between agents to escalate privileges beyond what the forging agent is authorized to do.

The attack pattern:

1. The mesh has agents with different trust levels — a low-trust research agent (read-only, no tools that commit state) and a high-trust execution agent (can deploy code, write to production stores).
2. The low-trust agent is compromised — via prompt injection (B2), memory poisoning (B3), or a compromised tool (B4). The attacker now controls the low-trust agent's outputs.
3. The compromised agent sends a message to the high-trust agent that *claims to be from the orchestrator*. The message says, in substance: "the user approved this deployment; execute it now."
4. The high-trust agent, which trusts the orchestrator (correctly) but has no way to verify the sender (incorrectly), executes the deployment.

The escalation is real: a read-only agent has triggered a production deployment. The compromise is invisible to the high-trust agent, because the message looked authoritative. This is the multi-agent analogue of a confused deputy (B4 covered the confused-deputy problem for tools; here the deputy is another agent). The high-trust agent is the deputy; the forged orchestrator message is the abuse of its authority.

The Microsoft taxonomy is the reference because it is empirical — drawn from a year of red-teaming real agentic systems — and because it names the structural cause explicitly: **inter-agent messages are trusted based on content, not on authenticated sender identity.** The fix is named in the same taxonomy: authenticate messages, isolate agents into trust compartments, and cap cascade depth.

## Cascade risk — one input, mesh-wide damage

The second attack is the cascade. It maps to **OWASP ASI06 (Cascading Hallucination)** and is the blast-radius amplifier the B1 threat model flagged.

The mechanism: one agent produces an output (a hallucinated fact, a poisoned result, a forged instruction) that is consumed by another agent as an input. That second agent acts on it and produces an output consumed by a third. The error — or the forged instruction — propagates through the mesh. A single malicious input at any node becomes N actions across M agents.

The cascade is not a single attack; it is a property of the mesh topology. Two features make it severe:

1. **Implicit trust propagates.** If agents consume peer outputs as ground truth (the default), a poisoned output from agent A is treated as fact by agent B, whose output is treated as fact by agent C. There is no verification at any edge. The cascade amplifies because *each hop adds confidence* — agent C's output is derived from "two sources" (B and the orchestrator), even though B's input was A's poison.
2. **Fan-out is large.** An orchestrator that delegates to ten executors has a fan-out of ten. A single compromised orchestrator output poisons all ten. A mesh with peer-to-peer communication has worse fan-out — each peer's output reaches every other peer.

The blast radius is mesh-wide because the mesh is connected and the trust is implicit. B1 named this the worst case on the surface map. The defense is not to detect each cascade after it starts (that is B8 observability); the defense is to prevent propagation — signed messages so a forged peer output is rejected, output verification at every edge (never consume a peer's output as fact without checking), and cascade-depth caps so a propagating chain hits a hard limit.

## Replay attacks — capturing and re-injecting a legitimate message

The third attack is replay. It is subtler than forgery and is the attack most teams miss entirely.

A replay attack captures a legitimate, correctly-signed inter-agent message and re-injects it later, out of its original context. The signature verifies (the message was genuinely signed by the orchestrator); the content is benign in its original context (an approval for a specific deployment); but the context has changed (the deployment already happened, or the approval was for a different task).

The classic instance: an orchestrator sends an `approve_deployment` message for task 42. The deployment runs. An attacker (or a compromised agent) captures the signed message and replays it later — the high-trust agent verifies the signature (valid), reads the content ("approve deployment"), and executes a *second* deployment that was never authorized. The signature did not help, because the message was authentic; the problem is that it was *not fresh*.

Replay attacks defeat a signature-only defense. The fix is replay protection: every message carries a **nonce** (a unique, single-use value) and a **timestamp**, and the recipient rejects any message whose timestamp falls outside a freshness window or whose nonce it has already seen. Message IDs tie the message to a specific task/session so an approval for task 42 cannot be replayed as an approval for task 43. This is the same replay-protection discipline that TLS, Kerberos, and OAuth use; it is not novel, but it is absent from most multi-agent channels.

## OWASP mapping: ASI06 and ASI07

Two OWASP Agentic Top 10 risks land squarely on the inter-agent channel:

- **ASI06 — Cascading Hallucination.** One agent's poisoned or hallucinated output, consumed as fact by downstream agents, propagates through the mesh. The inter-agent channel is the propagation medium; signed messages and output verification are the structural defense.
- **ASI07 — Insecure Output Handling.** An agent's output is consumed by a downstream agent (or a downstream tool) without sanitization or validation. In the inter-agent context, ASI07 means an executor's output is treated as a trusted instruction by the next agent rather than as untrusted data. The defense — output handling that treats peer outputs as data, not instructions — is the inter-agent analogue of B2's input classification.

The pattern across both: the inter-agent channel treats peer outputs as trusted by default, when they should be treated as untrusted data that is verified before it is acted upon.

---

# B6.2 — The defense: signed inter-agent messages

*Every inter-agent message carries a signature the recipient verifies. Every message is fresh. This is DD-16 ZeroClaw's HMAC tool receipts, extended to the inter-agent channel — and extended with durable keys, which Course 1 flagged as an open gap.*

## The signed-message contract

The defense against inter-agent trust escalation is a contract that every inter-agent message must satisfy before the recipient acts on it:

1. **Authenticity.** The message is signed by a key only the claimed sender holds. The recipient verifies the signature with the corresponding public key (asymmetric) or shared secret (HMAC). A forged message fails verification.
2. **Integrity.** The signature covers the entire message — sender, recipient, payload, task ID, timestamp, nonce. Any tampering invalidates the signature.
3. **Freshness.** The message carries a timestamp and a nonce. The recipient rejects messages outside a freshness window (e.g., ±60 seconds) and tracks seen nonces to reject replays.
4. **Binding.** The message is bound to a task/session ID and an intent. An approval for task 42 cannot be replayed against task 43, because the task ID is in the signed payload and the recipient checks it.

This contract is not novel. It is the same contract TLS, JWT, and mTLS enforce. What is novel — and what this module adds — is applying it to the inter-agent channel, which most deployed meshes leave entirely unprotected.

## HMAC vs. asymmetric signatures

Two signature schemes, chosen by the trust topology:

- **HMAC (symmetric).** The sender and recipient share a secret key. The sender computes `HMAC(key, message)`; the recipient recomputes and compares. Fast, simple, and the right choice when the sender and recipient are in the same trust domain (e.g., an orchestrator and its in-process executors). **This is DD-16 ZeroClaw's model** — HMAC tool receipts signed with a shared key. The limitation: HMAC requires a pre-shared secret per pair, which does not scale to cross-domain meshes where agents are operated by different teams.
- **Asymmetric (Ed25519 / ECDSA).** The sender holds a private key; recipients verify with the public key. Slower than HMAC but scales to N recipients without N shared secrets, and supports non-repudiation (the signature proves which key signed it). The right choice for cross-domain meshes — e.g., an orchestrator run by the platform team and an executor run by a product team, where sharing a symmetric secret across team boundaries is operationally and politically untenable.

The rule: **HMAC within a trust domain, asymmetric across trust domains.** A mesh where all agents are operated by one team and run in one process can use HMAC. A mesh with cross-team or cross-organization agents needs asymmetric signatures. This mirrors the mTLS precedent: service-to-service authentication inside a cluster can use a shared CA and mTLS; cross-cluster needs SPIFFE/SPIRE-style identity (B5's workload identity).

## The durable-key gap — extending DD-16 ZeroClaw

DD-16 ZeroClaw (Course 1) implements HMAC tool receipts: every tool call and its output is signed with an HMAC so the receipt can be verified later. This is the right primitive, and it is the reference architecture for this module. But Course 1 flagged an open gap: **ZeroClaw's signing keys are ephemeral — in-memory only, not persisted.**

That gap is load-bearing for the inter-agent channel. An ephemeral key authenticates within a session but not across sessions. A message signed in session 1 cannot be verified in session 2 (the key is gone). A process restart invalidates every signature. And — critically for B3's sleeper attack — a poisoned message persisted to memory in session 1 and retrieved in session 2 cannot be verified, because the signing key no longer exists.

B6 closes this gap with **durable, rotated signing keys**. The design:

1. **Key persistence.** Signing keys live in the secrets vault B5 specified (the vault the *harness* accesses, not the agent process). The key survives restarts and session boundaries.
2. **Key rotation.** Keys are rotated on a schedule (e.g., every 24 hours) and on compromise. Old keys are retained for a verification window (e.g., 7 days) so messages signed before rotation can still be verified, then destroyed.
3. **Key IDs.** Every signed message carries a `key_id` so the recipient knows which key to verify with — essential once rotation begins. This is the JWT `kid` header pattern.
4. **Asymmetric option for cross-domain.** When agents cross trust domains, each agent (or each trust compartment) has its own Ed25519 keypair. The orchestrator's compartment signs with its private key; executors verify with the orchestrator's public key, distributed out-of-band.

The durable-key extension is what turns ZeroClaw's in-session HMAC receipts into a defense that survives the session boundary — which is exactly the boundary the sleeper (B3) and cascade (ASI06) attacks exploit.

## Replay protection — nonces, timestamps, message IDs

A signed message that is not fresh is replayable. The replay-protection layer:

- **Timestamp.** Every message carries a `timestamp` (UTC, millisecond precision). The recipient rejects any message whose timestamp falls outside a freshness window — e.g., reject if `abs(now - timestamp) > 60s`. This bounds the replay window to the clock skew tolerance.
- **Nonce.** Every message carries a `nonce` — a unique, single-use random value. The recipient tracks seen nonces (in a bounded LRU cache, TTL'd to the freshness window) and rejects any message whose nonce it has already seen. This defeats replay within the freshness window.
- **Message ID and task binding.** Every message carries a `message_id` (unique) and a `task_id` / `session_id`. The recipient checks that the message's task ID matches the current task context. An approval for task 42 replayed against task 43 is rejected because the task ID does not match — even if the signature, timestamp, and nonce are all valid (the nonce was fresh in the original context, but the task binding fails).

The three together — timestamp window + nonce uniqueness + task binding — close the replay surface. Drop any one and a replay path opens: drop timestamps and an attacker can replay a captured message indefinitely (the nonce cache is bounded); drop nonces and an attacker can replay within the freshness window; drop task binding and an approval for one task can be replayed against another.

## The signed inter-agent message protocol — code

A runnable TypeScript implementation of the sign/verify protocol with replay protection. This extends DD-16 ZeroClaw's HMAC receipts with durable keys (key store) and full replay protection.

```typescript
import { createHmac, timingSafeEqual, randomBytes } from "node:crypto";

// ── Types ──────────────────────────────────────────────────────────────────

/** A signed inter-agent message. The recipient verifies before acting. */
interface SignedMessage {
  header: {
    from: string;          // sender agent id (e.g. "orchestrator")
    to: string;            // recipient agent id (e.g. "executor-deploy")
    task_id: string;       // binds the message to a task/session
    intent: string;        // e.g. "approve_deployment", "delegate_research"
    timestamp_ms: number;  // UTC millis — freshness window check
    nonce: string;         // single-use — replay defense
    key_id: string;        // which key signed (supports rotation)
  };
  payload: unknown;        // the message body — task-specific
  signature: string;       // HMAC over header + payload (hex)
}

// ── Key store (durable — the harness accesses it, not the agent) ────────────

/** Maps key_id -> secret. In production this is the secrets vault from B5,
 *  NOT a process-local Map. Durable across restarts; rotated on schedule. */
class KeyStore {
  private keys = new Map<string, Buffer>();
  private currentKeyId: string;

  constructor(initial: Map<string, Buffer>, currentKeyId: string) {
    this.keys = initial;
    this.currentKeyId = currentKeyId;
  }

  get(keyId: string): Buffer | undefined {
    return this.keys.get(keyId);
  }

  current(): { keyId: string; secret: Buffer } {
    const secret = this.keys.get(this.currentKeyId);
    if (!secret) throw new Error(`current key ${this.currentKeyId} missing`);
    return { keyId: this.currentKeyId, secret };
  }

  /** Rotate: add a new key, set it current, retain the old for verification. */
  rotate(newKeyId: string, newSecret: Buffer): void {
    this.keys.set(newKeyId, newSecret);
    this.currentKeyId = newKeyId;
  }
}

// ── Sign ───────────────────────────────────────────────────────────────────

/** The sender (orchestrator or any agent) signs a message before sending.
 *  The signature covers header + payload — any tampering invalidates it. */
function sign(
  from: string,
  to: string,
  task_id: string,
  intent: string,
  payload: unknown,
  keys: KeyStore,
  now: number = Date.now(),
): SignedMessage {
  const { keyId, secret } = keys.current();
  const header = {
    from,
    to,
    task_id,
    intent,
    timestamp_ms: now,
    nonce: randomBytes(16).toString("hex"),
    key_id: keyId,
  };
  const canonical = JSON.stringify({ header, payload });
  const signature = createHmac("sha256", secret)
    .update(canonical)
    .digest("hex");
  return { header, payload, signature };
}

// ── Verify (with replay protection) ─────────────────────────────────────────

/** Replay-protection state: tracks seen nonces within a TTL. */
class NonceCache {
  private seen = new Map<string, number>(); // nonce -> expires_at_ms
  constructor(private ttlMs: number) {}

  /** Returns true if the nonce is fresh (not seen, or its prior entry expired). */
  private cleanup(now: number): void {
    for (const [nonce, expires] of this.seen) {
      if (expires <= now) this.seen.delete(nonce);
    }
  }

  /** Returns true if nonce is fresh; records it. Caller rejects on false. */
  check(nonce: string, now: number): boolean {
    this.cleanup(now);
    if (this.seen.has(nonce)) return false; // replay
    this.seen.set(nonce, now + this.ttlMs);
    return true;
  }
}

/** Result of verification — the recipient acts only on OK. */
type VerifyResult =
  | { ok: true; message: SignedMessage }
  | { ok: false; reason: string };

/** The recipient verifies a message BEFORE acting on it.
 *  Checks: signature, freshness window, nonce uniqueness, task binding. */
function verify(
  msg: SignedMessage,
  keys: KeyStore,
  nonces: NonceCache,
  opts: {
    expectedTaskId: string;
    expectedRecipient: string;
    freshnessMs: number;   // e.g. 60_000
    now: number;           // injected for testing
  },
): VerifyResult {
  const h = msg.header;

  // 1. Recipient check — was this message addressed to me?
  if (h.to !== opts.expectedRecipient) {
    return { ok: false, reason: `wrong recipient: ${h.to}` };
  }

  // 2. Task binding — does the message belong to the current task?
  if (h.task_id !== opts.expectedTaskId) {
    return { ok: false, reason: `task_id mismatch: ${h.task_id}` };
  }

  // 3. Freshness window — is the timestamp within tolerance?
  const skew = Math.abs(opts.now - h.timestamp_ms);
  if (skew > opts.freshnessMs) {
    return { ok: false, reason: `stale: ${skew}ms outside ${opts.freshnessMs}ms window` };
  }

  // 4. Replay — is the nonce fresh?
  if (!nonces.check(h.nonce, opts.now)) {
    return { ok: false, reason: `replayed nonce: ${h.nonce}` };
  }

  // 5. Signature — does the HMAC verify with the claimed key?
  const secret = keys.get(h.key_id);
  if (!secret) {
    return { ok: false, reason: `unknown key_id: ${h.key_id} (expired or rotated out)` };
  }
  const canonical = JSON.stringify({ header: h, payload: msg.payload });
  const expected = createHmac("sha256", secret).update(canonical).digest("hex");

  // Constant-time compare — prevents timing oracles on signature check.
  const a = Buffer.from(msg.signature, "hex");
  const b = Buffer.from(expected, "hex");
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return { ok: false, reason: "signature invalid" };
  }

  return { ok: true, message: msg };
}
```

This is the engineering realization of the signed-message contract. Five checks, all of which must pass before the recipient acts: recipient, task binding, freshness, nonce, signature. Drop one and a path opens. The key store is the durable extension of ZeroClaw's ephemeral keys; the nonce cache is the replay defense ZeroClaw did not have.

---

# B6.3 — Compartmentalization and cascade prevention

*Signed messages authenticate the channel. Compartmentalization limits what an authenticated agent can ask another to do. Cascade prevention limits how far an action propagates. Together they bound the blast radius of a mesh compromise.*

## Compartmentalization — trust boundaries enforced in the orchestrator

Signed messages solve forgery. They do not solve the problem of a *legitimately signed* message asking a high-trust agent to do something dangerous. An orchestrator that has been compromised (via injection) can sign perfectly valid messages directing executors to exfiltrate. The signature verifies; the action is malicious. Authentication is necessary; it is not sufficient.

The second control is **compartmentalization**: agents are partitioned into trust compartments, and the movement of a request from a lower-trust compartment to a higher-trust one is an explicit, audited boundary crossing that the orchestrator — not the agents — enforces.

The design:

1. **Each agent has a trust level.** A research agent is low-trust (read-only, no commit authority). An executor is medium-trust (can write to staging). A deployment agent is high-trust (can write to production). These levels are assigned by the orchestrator's policy, not self-declared by the agent.
2. **The orchestrator mediates every cross-compartment request.** When the research agent's output needs to drive the execution agent, the request does not go agent-to-agent; it goes through the orchestrator, which applies a policy: "a low-trust agent's output may inform a high-trust agent's task only if (a) the high-trust agent's task was independently authorized, and (b) the output is verified data, not an instruction."
3. **The boundary crossing is logged and rate-limited.** Every cross-compartment request is an audit event. A burst of low→high crossings trips an alert (ties to B8 observability).

The load-bearing principle: **the compartment boundary is enforced in the orchestrator, not in the agents themselves.** An agent cannot be trusted to enforce its own trust level — a compromised agent will happily claim whatever level gets its message through. The orchestrator — a smaller, more auditable, more deterministic piece of code — is the policy enforcement point. This is the same principle as B4's tool gate (the harness enforces capability, not the model) and B5's scope check (the harness enforces privilege, not the agent): **the deterministic component enforces the trust boundary, because the model-in-the-loop component can be coerced.**

## Session-level intent tracking

Cascade prevention starts with knowing what the mesh is *supposed* to be doing. The mechanism is **session-level intent tracking**: the orchestrator records the authorized intent of the session (e.g., "research topic X and summarize; do not deploy") when the session starts, and every agent action is checked against that intent.

This is the inter-agent analogue of B2's input classification (is this input data or instruction?) and B5's scope check (does this token cover this action?). The check is: **does this inter-agent message's intent match the session's authorized intent?** A research agent that starts emitting `deploy` intents is out of policy — regardless of whether its message is signed. The orchestrator rejects it.

Session-level intent tracking is what makes cascade prevention deterministic rather than heuristic. A policy that says "this session's intent is research; deploy intents are denied" is a compiled boolean — the agent cannot talk its way past it. This is the B2.3 resolution (determinism on the trust boundary) applied to the inter-agent channel.

## Per-action authorization and blast-radius caps

Two structural controls bound the damage when a compromised agent does get a message through:

1. **Per-action authorization.** Every action an agent takes — every tool call, every cross-agent delegation, every commit — is authorized against the session intent and the agent's trust level, *at the moment of action*. This is B5's scope-check middleware applied to the inter-agent channel: the orchestrator verifies that this specific action, from this specific agent, in this specific task context, is permitted. An agent that was authorized to read in turn 1 is re-checked before it writes in turn 2.
2. **Blast-radius caps.** The orchestrator enforces hard limits on cascade propagation: a **cascade-depth cap** (no chain of agent-to-agent delegations longer than N — e.g., 3), a **fan-out cap** (no single agent delegates to more than M peers), and a **session action budget** (no more than K state-changing actions per session). These are circuit breakers: when a cascade starts, it hits a hard limit and stops, rather than propagating through the entire mesh.

These caps are blunt, and that is the point. A cascade is, by definition, a propagation you did not anticipate. The defense against an unanticipated propagation is a structural limit that does not depend on anticipating the specific attack. B8 (observability) detects the cascade after it starts; the blast-radius cap ensures the cascade stops before it consumes the mesh.

## The defense-in-depth stack

The full inter-agent trust defense, in order of enforcement:

| Layer | Control | Defeats |
| --- | --- | --- |
| **1. Signed messages** | HMAC/asymmetric signature, recipient verifies | Forgery, impersonation |
| **2. Replay protection** | Nonces, timestamp window, task binding | Replay of captured messages |
| **3. Compartmentalization** | Trust compartments, orchestrator-enforced boundary crossings | A low-trust agent directing a high-trust agent |
| **4. Session intent tracking** | Authorized intent recorded at session start; every message checked | Out-of-policy intents (research agent emitting deploy) |
| **5. Per-action authorization** | Scope check at the moment of action (B5 applied) | Privilege creep within a session |
| **6. Blast-radius caps** | Cascade depth, fan-out, action budget | Mesh-wide propagation |

Each layer addresses a different failure. A signed message that is fresh, from a low-trust agent, with an out-of-policy intent, fails at layer 4. A signed, fresh, in-policy message from a compromised high-trust agent is bounded by layer 6. The stack is defense-in-depth because no single layer closes every path — and the inter-agent channel has more paths than any other surface on the B1 map.

---

## Anti-Patterns

### Implicit trust between agents — the mesh default
Agents consume peer messages and outputs as ground truth with no authentication or verification. Cure: every inter-agent message is signed and verified (B6.2); every peer output is treated as untrusted data until checked.

### Unsigned messages with a `from` field
A message carries `from: orchestrator` and the recipient trusts the field. Cure: the `from` field is verified by the signature, not read from the payload. The recipient never acts on an unsigned message.

### Ephemeral signing keys (the ZeroClaw open gap)
Signing keys live in memory only, so signatures do not survive restarts or session boundaries. Cure: durable, rotated keys in the secrets vault (B5), with key IDs and a verification window for rotated-out keys.

### Replay protection omitted
Messages are signed but carry no nonce or timestamp, so a captured valid message can be replayed indefinitely. Cure: timestamp freshness window + single-use nonces + task binding. All three; drop one and a replay path opens.

### Compartment boundaries enforced in the agents
An agent is trusted to check its own trust level before acting on a peer's request. Cure: the orchestrator enforces compartment boundaries — the deterministic component holds the policy, because the agent can be coerced.

### No blast-radius caps on the mesh
A cascade propagates through the entire mesh because no depth, fan-out, or action-budget limit exists. Cure: hard caps — cascade depth, fan-out, session action budget — that trip before the cascade consumes the mesh. B8 detects; the cap stops.

### Agents reading their own signing keys
An agent that can read its signing key can be coerced into signing arbitrary messages (the confused deputy, again). Cure: the harness signs on the agent's behalf — the key lives in the vault (B5), the agent never sees it. This is B5's credential isolation applied to signing keys.

---

## Key Terms

| Term | Definition |
| --- | --- |
| **Inter-agent trust escalation** | Microsoft Failure Mode Taxonomy v2.0 #3 — forging a message between agents (e.g., a low-trust agent forging an orchestrator message) to escalate privileges beyond the forger's authority |
| **Cascade** | One agent's output triggers another's action, propagating through the mesh; OWASP ASI06. Amplified by implicit trust and large fan-out |
| **Replay attack** | Capturing a legitimate signed message and re-injecting it out of context; defeated by nonces, timestamp windows, and task binding |
| **Signed inter-agent message** | A message carrying a cryptographic signature (HMAC or asymmetric) the recipient verifies before acting; covers header + payload |
| **DD-16 ZeroClaw HMAC receipts** | Course 1 reference architecture — HMAC-signed tool receipts; the precursor to inter-agent message authentication. Open gap: ephemeral keys; B6 closes it with durable, rotated keys |
| **Nonce** | A single-use random value in a signed message; the recipient tracks seen nonces to reject replays |
| **Freshness window** | The timestamp tolerance (e.g., ±60s) within which a message is accepted; messages outside the window are rejected as stale |
| **Task binding** | The message's `task_id` is in the signed payload and checked against the current task — an approval for task 42 cannot be replayed against task 43 |
| **Trust compartment** | A partition of agents by trust level; cross-compartment requests are mediated and audited by the orchestrator |
| **Compartment boundary enforcement** | The policy that a low-trust agent cannot direct a high-trust agent without an explicit, orchestrator-mediated crossing — enforced in the orchestrator, not the agents |
| **Session intent tracking** | The orchestrator records the session's authorized intent at start; every agent message is checked against it (ties to B8) |
| **Blast-radius caps** | Hard limits — cascade depth, fan-out, session action budget — that stop a propagating cascade before it consumes the mesh |
| **ASI06** | OWASP Cascading Hallucination — one poisoned/hallucinated output consumed as fact propagates through the mesh |
| **ASI07** | OWASP Insecure Output Handling — an agent's output consumed by a downstream agent without sanitization; peer output treated as trusted instruction rather than untrusted data |

---

## Lab Exercise

See `07-lab-spec.md` — "Build the Signed Inter-Agent Channel." You will implement (a) a sign/verify message protocol with HMAC and durable keys (extending ZeroClaw), (b) replay protection with nonces, a timestamp freshness window, and task binding, and (c) a compartment boundary the orchestrator enforces. The lab also includes a trust-escalation attack demo: a compromised low-trust agent forges an orchestrator message, and you watch the verification layer reject it. Runnable TypeScript or Python, no GPU, ~60-75 minutes.

---

## References

1. **Microsoft AI Red Team** — *Failure Mode Taxonomy v2.0* (2026). Failure Mode #3 (Inter-Agent Trust Escalation) is the canonical framing of the forged-message attack. `ai-redteam.com/insights/updating-the-taxonomy-of-failure-modes-in-agentic-ai-systems-what-a-year-of-red/`.
2. **OWASP** — *Top 10 for Agentic Applications (2026)*. ASI06 (Cascading Hallucination) and ASI07 (Insecure Output Handling). `genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/`.
3. **DD-16 ZeroClaw** — Course 1 deep-dive. 6-layer safety model with HMAC tool receipts; the precursor architecture this module extends. Open gap (ephemeral keys) closed here with durable, rotated keys.
4. **Course 1 Module 4.2** — Multi-agent coordination patterns (orchestrator-executor, supervisor, peer-to-peer). The topologies whose trust edges this module secures.
5. **B1 Threat Model** — Surface 7 (Inter-Agent Edges); blast radius rated mesh-wide, the worst case on the surface map. The motivation for this module.
6. **B5 Identity and Permission Design** — Non-human identities, per-task scoped credentials, credential isolation. The identity substrate the signed-message protocol builds on (key store in the vault the harness accesses, not the agent).
7. **B3 Memory Poisoning** — The sleeper attack (ASI04) across session boundaries; the reason ZeroClaw's ephemeral keys are insufficient and durable keys are required.
8. **RFC 8693** — Token Exchange; the B5 mechanism. The signed-message protocol's task binding is the inter-agent analogue of token audience binding.
9. **RFC 7519 (JWT)** — JSON Web Token; the `kid` header, timestamp (`exp`/`nbf`), and nonce (`jti`) patterns this module's message header mirrors.
10. **SPIFFE/SPIRE** — Workload identity (B5). The cross-domain trust model for asymmetric inter-agent signatures where agents are operated by different teams.
11. **mTLS precedents** — Service-to-service mutual authentication; the established pattern for authenticated channels that the inter-agent signed-message contract applies to agent-to-agent communication.
12. **Kerberos / TLS replay protection** — The nonce, timestamp, and session-key disciplines that defeat replay; directly transposed to the inter-agent channel.
13. **B8 Observability** (companion module) — Session-level intent tracking, cascade detection, and audit feed that the blast-radius caps and compartment crossings report into.