Sandboxed agents
An autonomous agent runs untrusted or model-generated code and prompts. To run one safely, you need two things that work together:
- OS and network confinement, provided by a dedicated sandbox runtime
(NVIDIA OpenShell, Kubernetes
agent-sandboxwith gVisor or Kata, or Docker Sandboxdocker sbx), so the process can only talk to the local gateway. - Task-scoped authorization and credential brokering, provided by Agent Mesh
(
agentmesh-nodeoragentgateway+agentmesh-node), so the sandbox never holds a standing cloud credential or ambient workload token and can only call the services, MCP tools, HTTP paths, and cloud resources permitted for its current task.
This page shows the four deployment blueprints for connecting sandboxed agents and multi-hop sub-agents to an Agent Mesh.
Blueprint 1: NVIDIA OpenShell (zero credentials inside the sandbox)
NVIDIA OpenShell isolates untrusted agent processes using Landlock, seccomp, and network namespaces, routing all outbound HTTP through an external OpenShell egress proxy on the host that injects headers outside the sandbox boundary.
flowchart LR
subgraph OpenShell["NVIDIA OpenShell Sandbox (Landlock + seccomp + netns)"]
Harness["Unmodified Agent Process<br/>(Holds ZERO credentials)"]
end
subgraph Host["Host / Orchestrator Boundary"]
OSProxy["OpenShell Egress Proxy<br/>& Secret Injector"]
AgentMeshNode["Local agentmesh-node Gateway"]
end
Mesh(["Agent Mesh Mesh & Cloud Egress"])
Harness -- "Plain HTTP to agentmesh-node:8080/mcp & /v1" --> OSProxy
OSProxy -- "Injects X-Mesh-Authentication:<br/>Bearer <Sealed-Task-Biscuit>" --> AgentMeshNode
AgentMeshNode -- "Verified Task Biscuit" --> Mesh- Mint and seal a task token before starting the task:
The orchestrator calls
POST /oauth/tokenon the localagentmesh-nodewith aTaskAuthorizationRule(andseal=true), receiving a sealed Task Biscuit scoped to the single task:TASK_TOKEN=$(curl -sS --unix-socket ~/.config/agentmesh/agentmesh.sock \ http://localhost/oauth/token \ -d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \ -d 'subject_token_type=urn:agentmesh:params:oauth:token-type:biscuit' \ -d "subject_token=$(jq -r .biscuit ~/.config/agentmesh/credential.json 2>/dev/null || true)" \ -d 'seal=true' \ --data-urlencode 'options={"name":"tasks/pr-review-42","rules":[{"allowed_services":["mcp://github","inference://*"],"operation":{"allowed_tools":["get_pull_request","list_PullRequest_files"]}}]}' \ | jq -r .access_token) - Register the sealed token in OpenShell’s proxy:
Configure OpenShell’s secret injector to attach
X-Mesh-Authentication: Bearer $TASK_TOKENon requests toagentmesh-node:8080. The sandboxed process has no credential in its environment variables or filesystem.
Per-sandbox identity without a pre-minted token
A sandbox proxy that holds a workload identity for each sandbox can skip the
minting step and let the mesh bind the credential to that identity. OpenShell’s
token_grant sends the sandbox’s SPIFFE JWT-SVID to the node token endpoint as
an RFC 7523 client assertion (grant_type=client_credentials,
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-spiffe),
caches the Biscuit it gets back, and injects it as above. Agent Substrate’s
egress policy puts a Substrate-issued actor JWT directly on
X-Mesh-Authentication (replace_headers with actor_jwt), and the node
exchanges it on each request. In both cases the control plane trusts the
issuer with --workload-issuer and binds roles to the subject:
{ "role": "pr-reviewer", "members": ["user:spiffe://openshell.example/openshell/sandbox/*", "user:actor/reviews/*"] }
The sandbox still holds nothing. What changes is that the mesh, not the orchestrator, decides what each sandbox identity may call, and revoking the sandbox’s role takes effect without touching the proxy.
Blueprint 2: Docker Sandbox (docker sbx) & Kubernetes agent-sandbox
In docker sbx (local microVM) and Kubernetes agent-sandbox
(RuntimeClass: gvisor or kata) without an external header-injecting proxy,
the sandbox container authenticates to agentmesh-node using a task token passed as
OPENAI_API_KEY / Authorization: Bearer <token> (for /mcp and /v1/*) or
X-Mesh-Authentication: Bearer <token> (for /mesh/* and /egress/*).
How Agent Mesh bounds the token held by the sandbox
- Sealed leaf token (
Seal()): The orchestrator hands the sandbox a sealed, task-attenuated Biscuit. The sandbox cannot append blocks or widen its permissions. - Channel binding (
client_peer_id): The Biscuit’s authority block bindsclient_peer_idto the local or clusteragentmesh-node’speer_id. If a prompt-injected agent exfiltrates the token to an external attacker, the token is rejected by every other node in the mesh because the attacker cannot authenticate over libp2p as thatpeer_id. - Short TTL and explicit revocation on exit: Scope the task’s
expire_timeto the expected task duration (for example, 15 minutes) and revoke it onagentmesh-node(POST /oauth/revoke) as soon as the sandbox exits:curl -sS --unix-socket ~/.config/agentmesh/agentmesh.sock \ http://localhost/oauth/revoke \ -d "token=$TASK_TOKEN" - Standard Kubernetes isolation (
RuntimeClass: gvisor+NetworkPolicy): No/dev/net/tun,CAP_NET_ADMIN, or custom PID 1 wrapper is needed. The sandbox pod runs unprivileged undergvisororkata, and a standard KubernetesNetworkPolicyrestricts its egress to the clusteragentmesh-node(oragentgateway) Service:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: sandbox-to-agentmesh-node-only
namespace: agents
spec:
podSelector:
matchLabels:
app: agent-sandbox
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: agentmesh-system
podSelector:
matchLabels:
app: agentmesh-node
ports:
- protocol: TCP
port: 8080
Blueprint 3: agentgateway and Istio service mesh
In clusters that already route agent traffic through agentgateway,
Istio Ambient (waypoints or sidecars), or Envoy AI Gateway, traffic
continues to flow through those proxies while agentmesh-node serves as the Policy
Decision Point and Token Service:
- Envoy
ext_authzandext_proc(on the local API listeners): IstioAuthorizationPolicy (action: CUSTOM)oragentgatewaycallsagentmesh-nodewith the caller’s Biscuit, or with a platform JWT thatagentmesh-nodeexchanges at the control plane into a delegated Biscuit.agentmesh-nodeevaluates the standing Datalog policy and anyTaskAuthorizationRulechain (including MCP tool names in JSON-RPC bodies viaext_proc), and foregress://targets injects the brokered upstream credential intoAuthorizationbefore the gateway forwards the request. - RFC 8693 backend token exchange (
POST /oauth/token):agentgateway’s built-in RFC 8693 token exchange policy can point directly athttp://agentmesh-node:8080/oauth/tokento exchange workload JWTs or narrow existing Biscuits per route.
Blueprint 4: Multi-hop sub-agent delegation and cloud egress
When an agent delegates work to a sub-agent or calls a cloud API through an
egress node (egress://bigquery.googleapis.com), it attenuates its credential
offline in memory using the TypeScript or Python SDK (or POST /oauth/token on
agentmesh-node).
Example: two-hop attenuation in Python
from datetime import datetime, timedelta, timezone
from google.protobuf.timestamp_pb2 import Timestamp
from agent_mesh import AgentMesh, agentmesh_pb2
# Hop 1: Orchestrator narrows its session to read-only BigQuery sales_2026
# and two MCP tools for 15 minutes.
exp1 = Timestamp()
exp1.FromDatetime(datetime.now(timezone.utc) + timedelta(minutes=15))
hop1_session = session.attenuate(
agentmesh_pb2.TaskAuthorizationRule(
name="tasks/session-bq-read-sales",
expire_time=exp1,
rules=[
agentmesh_pb2.TaskRule(
allowed_services=["egress://bigquery.googleapis.com"],
operation=agentmesh_pb2.TaskOperation(
allowed_methods=["GET", "POST"],
allowed_paths=["/bigquery/v2/projects/my-proj/datasets/sales_2026/*"],
allowed_permissions=[
"bigquery.googleapis.com/datasets.get",
"bigquery.googleapis.com/tables.get",
"bigquery.googleapis.com/tables.getData",
],
),
allowed_resources=[
"//bigquery.googleapis.com/projects/my-proj/datasets/sales_2026/*",
],
),
agentmesh_pb2.TaskRule(
allowed_services=["mcp://bigquery"],
operation=agentmesh_pb2.TaskOperation(allowed_tools=["list_tables", "query_sales"]),
),
],
)
)
# Hop 2: Analytics agent spawns a leaf sub-agent restricted to table q1 only,
# and seals the credential so the sub-agent cannot append further blocks.
hop2_session = hop1_session.attenuate(
agentmesh_pb2.TaskAuthorizationRule(
name="tasks/subagent-q1-only",
rules=[
agentmesh_pb2.TaskRule(
allowed_services=["egress://bigquery.googleapis.com"],
operation=agentmesh_pb2.TaskOperation(
allowed_methods=["GET"],
allowed_paths=["/bigquery/v2/projects/my-proj/datasets/sales_2026/tables/q1/*"],
allowed_permissions=["bigquery.googleapis.com/tables.getData"],
),
allowed_resources=[
"//bigquery.googleapis.com/projects/my-proj/datasets/sales_2026/tables/q1",
],
)
],
)
).seal()
Example: two-hop attenuation in TypeScript
import { create } from "@bufbuild/protobuf";
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
import { TaskAuthorizationRuleSchema } from "@agentmesh-p2p/sdk/gen/agentmesh_pb.ts";
const hop1 = session.attenuate(
create(TaskAuthorizationRuleSchema, {
name: "tasks/session-bq-read-sales",
expireTime: timestampFromDate(new Date(Date.now() + 15 * 60_000)),
rules: [
{
allowedServices: ["mcp://bigquery"],
operation: { allowedTools: ["list_tables", "query_sales"] },
},
],
}),
);
const leafSession = hop1
.attenuate(
create(TaskAuthorizationRuleSchema, {
name: "tasks/subagent-q1-only",
rules: [
{
allowedServices: ["mcp://bigquery"],
operation: { allowedTools: ["query_sales"] },
},
],
}),
)
.seal();
When hop2_session calls egress://bigquery.googleapis.com, the egress
agentmesh-node verifies the Control Plane signature, the standing policy, and the
intersection of TAR_1 and TAR_2, mints an ES256 border JWT at the control
plane (POST /sts/token), and exchanges it via CloudTokenExchanger for a
downscoped cloud token.
Further reading
- Agent architecture: the two-token model, safe
tar_blockattenuation,CloudTokenExchanger, and content inspection. - Authorization: standing Datalog policy and
per-task
TaskAuthorizationRuleevaluation. - Node API:
/oauth/token,/oauth/revoke,ext_authz,ext_proc, and/egress/*.