From Prompts to Infrastructure: Building Trustworthy, Scalable AI Agents in the Age of A2A and Agent Sandboxes A developer outlines the engineering shift from prompt-based AI agents to infrastructure-grade systems, emphasizing the need for standardized Agent-to-Agent (A2A) protocols and sandboxed execution environments to handle scalability, security, and compliance. The article details how typed capability contracts and trust-bound invocation contexts enable agents to cooperate safely without trusting each other's internals. Originally published on tamiz.pro. The naive image of an AI agent is a chatbot that gets smarter each turn. The production reality is far more demanding: agents that coordinate across services, respect security boundaries, follow standardized communication contracts, and survive unbounded request volume. The shift from prompts to infrastructure isn't philosophical — it's a hard engineering transition that separates demos from deployed systems. Early AI applications leaned on prompt chaining: sequence of LLM calls linked by a human-readable narrative. This works until you need five agents handling 10,000 concurrent requests, each calling multiple downstream tools, with latency budgets measured in seconds and audit trails required for compliance. Prompt-level systems face three fatal scaling problems: The alternative is treating agents as infrastructure — services with explicit contracts, bounded execution contexts, and standardized inter-agent communication. A2A Agent-to-Agent refers to the emerging class of protocols that let agents communicate with each other without a central orchestrator making every decision. Think RPC for agents: structured, typed, versioned, and observable. Without A2A, agent ecosystems look like this: With A2A, agents speak a common protocol. Each agent publishes a capability contract — a machine-readable description of what inputs it accepts, what outputs it produces, and what side effects it may have. Other agents discover and invoke capabilities through a typed interface, not by guessing JSON shapes. A well-designed A2A system rests on three primitives: Cardinality-bounded messages. Every inter-agent message has a defined lifecycle: sent, acknowledged, completed, or failed. Unlike fire-and-forget HTTP, A2A messages carry sequence numbers and correlation IDs so agents can reconstruct conversation history. Typed capability descriptors. Instead of POST /agent/process , a capability is declared as: { "type": "tool", "name": "database query", "version": "1.2.0", "input schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": "query", "connection id" , "properties": { "query": { "type": "string", "maxLength": 4096 }, "connection id": { "type": "string", "pattern": "^conn- a-f0-9 +$" } } }, "output schema": { "type": "object", "properties": { "rows": { "type": "array", "maxItems": 1000 }, "truncated": { "type": "boolean" } } }, "error schemas": { "code": "QUERy TOO LARGE", "message": "Query exceeds 4096 characters" }, { "code": "CONNECTION UNAVAILABLE", "message": "Connection pool exhausted" } } This schema isn't decorative. It enables compile-time validation at the call site, runtime enforcement at the capability boundary, and automatic test generation for every contract change. Trust-bound invocation context. Every A2A call carries an invocation context that answers: who is calling? what tools may they access? what is the budget? A2A protocols embed this as a signed token or policy bundle rather than a header you trust because you built the client. A2A and sandboxes are complementary. A2A defines the contract — what can be asked and what must be returned. Sandboxes define the execution boundary — where and how the agent runs its logic. Together they create a system where agents can cooperate without trusting each other's internals. An agent sandbox is a controlled environment where agent code runs with explicit resource limits, network restrictions, and output filtering. The sandbox is not a luxury — it is the mechanism that makes multi-agent systems safe. Consider what goes wrong when agents run without sandboxes: Each of these scenarios is addressable at the infrastructure layer rather than hoping the next prompt improvement catches it. A production-grade agent sandbox implements three layers: Compute sandbox. Isolated process execution with CPU, memory, and timeout budgets. Tools run as subprocesses or WebAssembly modules, not arbitrary Python inside the agent process. Tools that exceed their allocation are killed and reported as errors rather than hanging the orchestrator. Network sandbox. Agents only reach networks they are authorized for. A read-only agent cannot call POST https://webhook.example.com/keys . Egress is enforced by the host, not by the agent's code. Ingress is filtered against allowlists for inbound tool responses. Data sandbox. Secrets, credentials, and PII live outside the agent's execution context. Tools receive tokens, not full credentials. Output streams are scanned for sensitive patterns before leaving the sandbox boundary. Here is a minimal but production-oriented sandbox wrapper around a tool invocation: python import asyncio import json import time from contextlib import asynccontextmanager from dataclasses import dataclass from typing import Any, Optional @dataclass class SandboxConfig: max memory mb: int = 256 timeout seconds: float = 30.0 allowed networks: list str | None = None max output bytes: int = 65536 secret prefixes: list str | None = None class SandboxError Exception : pass class SandboxTooMuchMemory SandboxError : pass class SandboxTimeoutError SandboxError : pass class SandboxNetworkBlocked SandboxError : pass @asynccontextmanager async def run tool in sandbox tool code: str, arguments: dict str, Any , config: SandboxConfig, : start = time.monotonic 1. Encode the call as a self-contained script payload payload = json.dumps { "args": arguments, "limits": { "memory mb": config.max memory mb, "timeout s": config.timeout seconds, "max output bytes": config.max output bytes, }, "allowed networks": config.allowed networks, "secret prefixes": config.secret prefixes or , } .encode 2. Spawn a sandboxed subprocess. In practice this would use gVisor, Firecracker, or WASI — here we show the contract. proc = await asyncio.create subprocess exec "sandbox-runner", "--tool-code", "-", "--payload", "-", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, try: stdout, stderr = await asyncio.wait for proc.communicate input=payload , timeout=config.timeout seconds, except asyncio.TimeoutError: proc.kill raise SandboxTimeoutError f"Tool exceeded {config.timeout seconds}s budget" if proc.returncode = 0: error = stderr.decode errors="replace" if "memory" in error.lower : raise SandboxTooMuchMemory error if "network" in error.lower : raise SandboxNetworkBlocked error raise SandboxError f"Tool exited {proc.returncode}: {error}" result = json.loads stdout.decode 3. Post-execution output filtering if result.get "output bytes", 0 config.max output bytes: raise SandboxError "Output exceeds max output bytes" yield result Notice what is not in this code: authentication, authorization, logging, or retry logic. Those belong to the orchestration layer. The sandbox only answers one question: did this tool complete within its declared budget, and was the output structurally valid? The oldest agent architecture is the central orchestrator: one agent reads a prompt, decides which tools to call, calls them, and returns a result. This pattern collapses under two conditions — high concurrency and heterogeneous tool ownership. Imagine ten teams each maintain a set of tools. No team wants to expose a generic REST endpoint that any other team's agent can call with any payload. They want: A central orchestrator cannot accommodate this without becoming a bottleneck and a single point of failure. The replacement is a capability registry — a service that agents publish to and discover from. Each agent registers its capabilities. Other agents query the registry for capabilities matching their needs. When an agent invokes a capability, the registry resolves the target and injects the invocation context. capability-registry.example.yaml capabilities: - agent id: payments-agent version: 2.1.0 capabilities: - name: charge input: ChargeRequest output: ChargeResult error schemas: InsufficientFunds, CardDeclined, NetworkTimeout rate limit: calls per minute: 100 burst: 20 trust policy: required roles: payments-service allowed origins: - order-agent - refund-agent - agent id: research-agent version: 1.4.0 capabilities: - name: search input: SearchRequest output: SearchResultList error schemas: RateLimited, QueryTooLong rate limit: calls per minute: 30 burst: 5 trust policy: required roles: research-service allowed origins: user-agent This file is not a configuration dump. It is a machine-readable contract. The registry enforces it at runtime by checking caller identity, applying rate limits, and short-circuiting invalid invocations before they reach the agent. A2A messages carry a capability id field instead of an address. The registry translates that identifier to the actual invocation target using the trust policy and rate-limit state. Consumers never know — and should not care — which deployment hosts the capability. This decoupling is what allows agents to scale horizontally without fragile routing tables. The hardest part of building trustworthy agents is controlling state. An agent that mutates shared variables, sends side-channel messages, or silently retries failures will appear correct in tests and fail catastrophically in production. Every production agent should expose its state as a finite state machine with explicit transitions. Consider a task agent that processes a user request: ┌──────────┐ user request ──▶ │ PENDING │ └────┬─────┘ │ plan generated ┌────▼─────┐ │ PLANNING │ └────┬─────┘ │ plan approved ┌────▼─────┐ ┌─────│ EXECUTING│─────┐ │ └────┬─────┘ │ │ │ tool failed tool completed ┌─────▼─────┐ ┌─▼────────┐ ┌──▼──────────┐ │ RETRYING │ │COMPLETED │ │FAILED MAX │ └─────┬─────┘ └──────────┘ └─────────────┘ │ │ retry budget exhausted └───────────────────────▶ FAILED MAX This diagram is not decoration. It drives four engineering decisions: An agent that calls a tool twice with different results is an agent that is lying about its own state. Every tool handler must be idempotent or clearly non-idempotent with compensation logic. interface ToolHandler { id: string; execute ctx: InvocationContext, params: unknown : Promise