Skip to main content

Overview

The TrustBoundary enforces zero-trust isolation between agents. By default, all communication is denied unless explicitly allowed.
default_allow is False by default. You must explicitly trust agents or set default_allow=True for permissive deployments. The interceptor sets default_allow=True since it handles its own verification — but standalone trust boundary usage defaults to deny-all.

Deny-all by default

To allow communication, explicitly trust the sender:
Trust is directional for the allowlist gate. Only the sender must be in the allowlist for a message to pass the gate — trusting the receiver alone is not sufficient. This closes a “name-drop” attack where an untrusted sender addresses a trusted receiver to bypass the allowlist. However, receiver-side scoped filters (e.g. allowed_payload_types) are still enforced independently on the receiver’s entry and can block a communication even when the sender passes the allowlist gate.

Controls

Global blocklist

Block an agent from all communication:
Blocking automatically removes the agent from the trusted list.
Trust an agent for all pairs:
Trusting automatically removes the agent from the blocked list.
Restrict trust to specific receivers and payload types, and optionally set an expiry:
A message from analytics-agent is only permitted when the receiver matches allowed_receivers and the payload type matches allowed_payload_types. Any field left as None means “unrestricted for this dimension”. Once valid_until passes, the entry is treated as if it never existed and is auto-evicted on the next evaluation.
Revoke a trust grant without restarting the service:
Grants and revocations are written to the audit log with the redacted agent ID and granted_by / revoked_by attribution. block_agent() also removes any existing trust entry as a side effect.
Block a specific directional pair:

Scoped trust grants

trust_agent() accepts optional scope and expiry parameters so operators can grant narrow, time-bounded exceptions instead of permanent, unscoped bypasses. evaluate() accepts an optional payload_type. When provided, it is checked against the allowed_payload_types field of both the sender entry (outbound filter) and the receiver entry (inbound filter) — the message is only trusted if the payload type is permitted by both sides:
Expired entries are treated as non-existent — including their scope. If you need permanent scope restrictions, leave valid_until=None rather than setting a far-future expiry.
Expired trust entries are auto-evicted at most once per minute during evaluate(), so revocation via revoke_agent() or expiry via valid_until never requires a service restart.

Loading trust from environment

QWED_A2A_TRUSTED_AGENTS accepts two formats. The HTTP gateway calls load_from_env() on startup; both formats work interchangeably. Simple CSV (unrestricted, no expiry):
JSON array (scoped and/or expiring):
Each JSON entry requires a non-empty agent_id; the other fields are optional. Invalid entries (non-object, missing ID, non-string scope values, non-finite valid_until) are logged and skipped without failing the load — the remaining entries still apply.

Token-bucket rate limiting

Rate limiting uses a token-bucket algorithm (not fixed-window), providing smooth, fair enforcement:

Configuration

Automatic eviction

Cold pairs (no requests for 5 minutes) are automatically evicted from the rate-limit map to prevent unbounded memory growth. Eviction runs once per minute.
Rate-limit entries are only allocated after allowlist checks pass. This prevents malicious agents from spraying the map with one-off sender/receiver IDs in strict mode.

Evaluation order

The trust boundary evaluates requests in this exact order:
Steps 1–4 are stateless (no side effects). Rate-limit state is only allocated at step 5, after all policy checks pass.

Usage with the interceptor

The interceptor creates a TrustBoundary with default_allow=True by default, since it handles verification itself. For zero-trust deployments, inject your own: