How many of the agent developers are using this for their agents?
kubernetes-sigs/agent-sandbox
2 likes · 174 views
Autonomous agent loops with Gemini 4 Argon and GKE Agent Sandbox: Running untrusted code safely at scale
---
title: "Autonomous agent loops with Gemini 4 Argon and GKE Agent Sandbox: Running untrusted code safely at scale"
published: true
description: "Run untrusted LLM-generated Python and Bash code safely at scale using Gemini 4 Argon and GKE Agent Sandbox with gVisor isolation."
tags: kubernetes, ai, gke, googlecloud
cover_image: dev-to-uploads.s3.us-east-2.amazonaws.com
---
Autonomous AI systems are only as useful as the actions they can verify. With the release of Gemini 4 Argon, multi-step reasoning and native tool calling allow agents to write data transformation scripts, generate infrastructure migrations, and debug failing tests across long execution horizons. However, a fundamental architectural bottleneck emerges the moment the model outputs a block of Python or Bash: something has to execute that code and return the output so the model can iterate.
Running LLM-generated code inside the same Kubernetes Pod as your orchestrator application is an immediate security liability. A hallucinated command, a prompt-injection payload, or a dependency conflict can corrupt the container filesystem, exfiltrate mounted service account credentials, or starve adjacent workloads of memory. Conversely, spinning up a fresh Kubernetes Job or virtual machine for every turn of a multi-step reasoning loop introduces 5 to 30 seconds of cold-start latency per iteration, stalling the agent workflow.
By pairing a Gemini 4 Argon reasoning loop with GKE Agent Sandbox (Generally Available on GKE 1.32 and later across both GKE Autopilot and GKE Standard), platform teams can isolate every code execution step inside a kernel-hardened gVisor runtime with sub-second allocation from pre-warmed Pod pools.
In this article, I will walk through the architecture of a self-correcting Gemini 4 Argon code execution loop on Google Kubernetes Engine (GKE), show how to configure warm sandbox pools across GKE Autopilot and Standard, and implement a Python orchestrator that claims, executes, and recycles isolated sandboxes.
Architectural tension in multi-turn code execution
When a Gemini 4 Argon agent solves an analytical or engineering task, it rarely stops at a single code snippet. Instead, it operates in an iterative loop:
1. Task submission: The client sends an analytical query or code generation request to the GKE orchestrator Pod.
2. Model reasoning and tool dispatch: The orchestrator invokes Gemini 4 Argon with an execute_python tool declaration. The model emits a tool call containing the proposed script.
3. Warm pool allocation: The orchestrator requests a pre-warmed sandbox from the GKE Sandbox Router, which allocates an initialized gVisor Pod in under 200ms.
4. Isolated execution: The untrusted script runs inside the sandbox, capturing stdout, stderr, and exit codes without host kernel access.
5. Recycling and feedback: The used sandbox is returned to the router for teardown, and execution outputs are fed back to Gemini 4 Argon for iterative refinement until the final answer is ready.
Evaluating where to execute that generated code requires balancing isolation boundaries against allocation latency:
| Execution pattern | Isolation boundary | Allocation latency | Operational trade-off |
| :--- | :--- | :--- | :--- |
| In-process subprocess in orchestrator Pod | Shared container kernel and filesystem | < 10ms | Exposes Workload Identity tokens, application memory, and internal cluster networking to arbitrary code |
| Per-invocation Kubernetes Job | Standard container (runc) or RuntimeClass | 3s to 20s | Scheduling, image pulling, and container initialization add seconds of delay to every turn of the agent loop |
| GKE Agent Sandbox with SandboxWarmPool | User-space kernel via gVisor + default-deny network | < 200ms (p90) | Requires maintaining a small footprint of pre-warmed idle Pods in exchange for sub-second isolation |
GKE Agent Sandbox resolves this tension by combining four open-source custom resources from the kubernetes-sigs/agent-sandbox project with GKE kernel isolation:
- Kernel-level isolation via Sandbox and gVisor: Each Sandbox (agents.x-k8s.io/v1alpha1) manages an isolated Pod running inside GKE Sandbox, intercepting application system calls in user space so untrusted code never interacts directly with the host Linux kernel.
- Reusable blueprints via SandboxTemplate: A SandboxTemplate (extensions.agents.x-k8s.io/v1alpha1) defines the Pod specification, resource boundaries, and security context once for a class of agent tools.
- Pre-warmed capacity via SandboxWarmPool: Rather than waiting for the Kubernetes scheduler and container runtime on each tool call, a SandboxWarmPool (extensions.agents.x-k8s.io/v1alpha1) maintains a standing buffer of initialized Sandbox Pods ready to be claimed in under 200 milliseconds.
- Declarative allocation via SandboxClaim and zero-trust posture: When an agent needs to run code, the Sandbox Router creates a SandboxClaim (extensions.agents.x-k8s.io/v1alpha1) to bind a warm Sandbox Pod and deletes the claim on release. Sandbox Pods disable Kubernetes service account token automounting (automountServiceAccountToken: false) and enforce strict network egress policies, keeping your orchestrator's Workload Identity Federation for GKE credentials unreachable from the code execution environment.
Configuring the cluster and sandbox node pools
Before deploying the sandbox templates and the Gemini orchestrator, you need a GKE cluster with gVisor (GKE Sandbox) support and the Agent Sandbox controller installed.
Enabling Agent Sandbox on GKE Autopilot
On GKE Autopilot, GKE Sandbox is supported out of the box and automatically provisions gVisor-enabled compute capacity whenever Pods request runtimeClassName: gvisor. If you are using the managed GKE Agent Sandbox addon preview, pass --enable-agent-sandbox via gcloud beta; otherwise, create a standard Autopilot cluster and install the upstream controller manifests in the next step:
export PROJECT_ID="production-ai-platform"
export LOCATION="us-central1"
export CLUSTER_NAME="agent-execution-cluster"
gcloud beta container clusters create-auto "${CLUSTER_NAME}" \
--project="${PROJECT_ID}" \
--location="${LOCATION}" \
--enable-agent-sandbox
Enabling Agent Sandbox on GKE Standard
On GKE Standard, your cluster must keep at least one regular node pool for system Pods while isolating untrusted execution onto a dedicated node pool configured with --image-type=cos_containerd and --sandbox=type=gvisor. Separating the untrusted execution node pool from the node pool hosting your orchestrator Pods also prevents noisy-neighbor resource contention:
# Enable the Agent Sandbox addon on an existing Standard cluster (beta)
gcloud beta container clusters update "${CLUSTER_NAME}" \
--project="${PROJECT_ID}" \
--location="${LOCATION}" \
--enable-agent-sandbox
# Create a dedicated gVisor node pool for untrusted code execution
gcloud container node-pools create sandbox-execution-pool \
--cluster="${CLUSTER_NAME}" \
--project="${PROJECT_ID}" \
--location="${LOCATION}" \
--machine-type=e2-standard-8 \
--image-type=cos_containerd \
--sandbox=type=gvisor \
--num-nodes=3 \
--enable-autoscaling \
--min-nodes=2 \
--max-nodes=10
If you are installing the open-source kubernetes-sigs/agent-sandbox controller and extension CRDs (Sandbox, SandboxTemplate, SandboxClaim, and SandboxWarmPool) directly onto any GKE Autopilot or Standard cluster, apply the release manifests:
export AGENT_SANDBOX_VERSION="v0.1.0"
kubectl apply -f "https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${AGENT_SANDBOX_VERSION}/manifest.yaml"
kubectl apply -f "https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${AGENT_SANDBOX_VERSION}/extensions.yaml"
Defining the SandboxTemplate, SandboxWarmPool, and SandboxClaim
Once the cluster and CRDs are ready, you define the execution environment using SandboxTemplate and SandboxWarmPool. When you specify runtimeClassName: gvisor on GKE, the GKE admission controller automatically injects the required sandbox.gke.io/runtime: gvisor node selector and taint tolerations on both Autopilot and Standard clusters.
Create a manifest named analytics-sandbox-pool.yaml in a dedicated agent-sandboxes namespace. This configuration enforces non-root execution, drops all Linux capabilities, disables service account token mounting, and keeps 5 pre-warmed Python runtime Pods ready for tool calls:
apiVersion: v1
kind: Namespace
metadata:
name: agent-sandboxes
---
apiVersion: extensions.agents.x-k8s.io/v1alpha1
kind: SandboxTemplate
metadata:
name: python-analytics-template
namespace: agent-sandboxes
spec:
podTemplate:
metadata:
labels:
app.kubernetes.io/component: execution-sandbox
sandbox-tier: untrusted-python
spec:
# GKE automatically adds the gVisor nodeSelector and toleration for this RuntimeClass
runtimeClassName: gvisor
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: python-runtime
image: registry.k8s.io/agent-sandbox/python-runtime-sandbox:v0.1.0
ports:
- containerPort: 8888
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
resources:
requests:
cpu: "500m"
memory: "1Gi"
ephemeral-storage: "2Gi"
limits:
cpu: "1000m"
memory: "1Gi"
ephemeral-storage: "2Gi"
---
apiVersion: extensions.agents.x-k8s.io/v1alpha1
kind: SandboxWarmPool
metadata:
name: python-analytics-warmpool
namespace: agent-sandboxes
spec:
replicas: 5
sandboxTemplateRef:
name: python-analytics-template
Under the hood, whenever the Sandbox Router (or a Kubernetes client) allocates a sandbox from this warm pool, it creates a SandboxClaim resource targeting python-analytics-template:
apiVersion: extensions.agents.x-k8s.io/v1alpha1
kind: SandboxClaim
metadata:
name: analytics-task-claim-example
namespace: agent-sandboxes
spec:
sandboxTemplateRef:
name: python-analytics-template
To prevent untrusted code from scanning internal cluster services or calling the GKE metadata server, apply a default-deny NetworkPolicy (isolate-untrusted-sandboxes.yaml) in the agent-sandboxes namespace that blocks all egress and only permits ingress on port 8888 from the Sandbox Router:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: isolate-untrusted-sandboxes
namespace: agent-sandboxes
spec:
podSelector:
matchLabels:
app.kubernetes.io/component: execution-sandbox
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: agent-sandbox-system
- podSelector:
matchLabels:
app.kubernetes.io/component: sandbox-router
ports:
- protocol: TCP
port: 8888
egress: []
Apply the manifests to your cluster:
kubectl apply -f analytics-sandbox-pool.yaml
kubectl apply -f isolate-untrusted-sandboxes.yaml
Building the Gemini orchestrator loop
With the sandbox pool running in agent-sandboxes, you can deploy the orchestrator service in a separate agent-orchestrator namespace. The orchestrator uses Workload Identity Federation for GKE to authenticate with the Gemini platform and calls the Sandbox Router whenever Gemini invokes the code execution tool.
Here is the core implementation (orchestrator.py) using the google-genai SDK and the agentic_sandbox client. In the snippet below, GEMINI_MODEL defaults to gemini-4-argon:
import os
from google import genai
from google.genai import types
from agentic_sandbox import SandboxClient
# Defaults to gemini-4-argon on the Gemini platform:
MODEL_ID = os.environ.get("GEMINI_MODEL", "gemini-4-argon")
# Initialize the Gemini platform client using Workload Identity credentials
genai_client = genai.Client(
vertexai=True,
project=os.environ["GOOGLE_CLOUD_PROJECT"],
location=os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1"),
)
# Initialize the GKE Agent Sandbox client (communicates via the Sandbox Router)
sandbox_client = SandboxClient(namespace="agent-sandboxes")
def execute_python_in_sandbox(code: str) -> dict[str, str | int]:
"""Executes Python code inside an isolated GKE gVisor sandbox and returns stdout, stderr, and exit code."""
sandbox = sandbox_client.claim_sandbox(
template_name="python-analytics-template",
timeout_seconds=5,
)
try:
result = sandbox.execute(code, timeout_seconds=30)
return {
"stdout": result.output or "",
"stderr": result.error or "",
"exit_code": result.exit_code,
}
finally:
# Always release the sandbox so the SandboxClaim is deleted and replaced
sandbox.release()
def run_autonomous_task(user_prompt: str) -> str:
"""Runs a multi-turn Gemini 4 Argon reasoning loop backed by GKE Agent Sandbox."""
config = types.GenerateContentConfig(
system_instruction=(
"You are a quantitative engineering agent. When solving data or systems problems, "
"write Python scripts and run them using execute_python_in_sandbox. "
"If execution returns a non-zero exit_code or stderr traceback, inspect the error, "
"adjust your code, and re-run until the output is verified."
),
tools=[execute_python_in_sandbox],
automatic_function_calling=types.AutomaticFunctionCallingConfig(
maximum_remote_calls=10,
),
temperature=0.1,
)
chat = genai_client.chats.create(
model=MODEL_ID,
config=config,
)
response = chat.send_message(user_prompt)
return response.text or ""
When genai_client.chats.create receives a tool declaration with a typed Python function, the google-genai SDK automatically manages the multi-turn tool-calling loop (bounded by maximum_remote_calls=10), invoking execute_python_in_sandbox each time the model emits a function call and feeding the structured stdout, stderr, and exit_code dictionary back into the conversation context. Because sandbox.release() runs inside a finally block, every execution turn gets a clean, ephemeral container filesystem, preventing state pollution across unrelated tasks.
Verifying sandbox allocation and observability
After deploying the warm pool and orchestrator, verify that the pre-warmed Pods are scheduled under the gVisor runtime and that the warm pool controller is maintaining target capacity:
# Inspect the warm pool status, active sandboxes, and claims
kubectl get sandboxwarmpool,sandbox,sandboxclaim -n agent-sandboxes
# Confirm that sandbox Pods are running with RuntimeClass gvisor
kubectl get pods -n agent-sandboxes \
-l app.kubernetes.io/component=execution-sandbox \
-o custom-columns="NAME:.metadata.name,STATUS:.status.phase,RUNTIME:.spec.runtimeClassName,NODE:.spec.nodeName"
Expected output:
NAME STATUS RUNTIME NODE
python-analytics-warmpool-6d8f9b7c4-2x9kl Running gvisor gke-agent-execution-sandbox-pool-8a12f-9b2c
python-analytics-warmpool-6d8f9b7c4-7m4pq Running gvisor gke-agent-execution-sandbox-pool-8a12f-9b2c
python-analytics-warmpool-6d8f9b7c4-9v1zr Running gvisor gke-agent-execution-sandbox-pool-8a12f-c41d
To inspect sandbox claim latency and catch container OOM kills triggered by runaway agent scripts in Cloud Logging, run the following query:
resource.type="k8s_pod"
resource.labels.namespace_name="agent-sandboxes"
(reason="OOMKilling" OR reason="SandboxClaimed" OR reason="SandboxReleased")
In Cloud Monitoring, track kubernetes.io/pod/ephemeral_storage/used_bytes and kubernetes.io/container/memory/used_bytes filtered by namespace_name="agent-sandboxes" to rightsize your SandboxTemplate resource limits based on the actual memory footprint of the scripts Gemini 4 Argon generates.
Operational trade-offs and sharp edges
When running GKE Agent Sandbox with Gemini 4 Argon in production, keep these operational boundaries in mind:
- Warm pool sizing vs. burst concurrency: If 20 concurrent Gemini 4 Argon requests trigger tool execution simultaneously and your SandboxWarmPool only holds replicas: 5, the remaining 15 requests will wait for cold Pod creation. Size replicas to cover your p95 burst concurrency during peak traffic hours.
- Stateful multi-step sessions: In the example above, releasing the sandbox inside execute_python_in_sandbox destroys the Pod after every turn. If your Gemini 4 Argon agent writes files to disk in turn 1 and analyzes them in turn 2, bind the sandbox instance to the chat session lifecycle and invoke sandbox.release() only after the entire conversation completes.
- gVisor syscall overhead for heavy I/O: Because gVisor intercepts system calls in user space, compute-bound Python scripts (such as NumPy or Pandas array operations) run near native speed, whereas workloads performing thousands of small file opens or network socket creations incur additional CPU overhead.
- Hard execution timeouts: LLM-generated code can inadvertently introduce infinite while loops. Always enforce a strict timeout_seconds parameter on sandbox.execute() so the orchestrator returns a timeout error to Gemini 4 Argon rather than blocking the request worker indefinitely.
Next steps
Pairing Gemini 4 Argon with GKE Agent Sandbox gives your agents the ability to test and self-correct their own code without exposing your cluster or credentials to untrusted execution. By isolating every tool invocation inside a pre-warmed gVisor Pod with zero network egress, you get both kernel-level safety and sub-second iteration latency.
To build this pattern in your own environment, explore the GKE Agent Sandbox conceptual guide, follow the GKE Agent Sandbox installation instructions, and check out the upstream kubernetes-sigs/agent-sandbox repository.
6 reactions
Agent Sandboxの概要
はじめに
LeaderWorkerSetと同じくSIG Appsが管理するKubernetesサブプロジェクトに、Agent Sandboxがあります。LWS が leader と worker の Pod グループを扱うのに対し、Agent Sandbox は、安定した ID を持つ単一 Pod の実行環境を宣言的に管理する Sandbox CRD を提供します。AIエージェントの実行環境や開発者向けの使い捨て環境、強化学習の評価ループなど、用途は異なりますが根っこにある「分散推論・分散エージェントをKubernetesでどう扱うか」という問題意識はLWSと共通しています。
Google Cloud は、Agent Substrate と Kubernetes 上のワークロードの位置づけを、以下の図で示しています。
!Agent Substrateを中心とした、エージェント基盤のレイヤー構成図。最上位にAgent Runtime/Agent Harnesses/Custom Agents&Toolsからなる Agent Application Layer、その下にSandbox Isolation(gVisor/MicroVM)を含むAgent Substrateと、Deployments/StatefulSets/LWSなどのOther k8s workloads、最下層にKubernetesとIaaS/Bare metal、Blob storageが並ぶ。
(参考: Bringing you Agent Sandbox on GKE and Agent Substrate | Google Cloud Blog, 2026-05-20)
この図は、Agent Substrate が sandbox の分離基盤を利用する一方、LWS が Other k8s workloads に並ぶことを示しています。ただし、図中の Sandbox Isolation (gVisor / MicroVM) はランタイムによる分離を表しており、OSS の Sandbox CRD やそのコントローラーと同一ではありません。本記事では、Agent Sandbox の CRD が管理する Pod と、その Pod に指定する隔離ランタイムを分けて説明します。
関連する仕組みについては、Agent Substrate の概要と LeaderWorkerSet と DisaggregatedSet の概要も執筆しています。
この記事では、Agent Sandboxがどういう課題を解決しようとしているAPIなのか、Sandboxのライフサイクルがどう実装されているか、そしてSandboxWarmPool/SandboxClaimという拡張APIが何をしているのかを整理します。
対象コンポーネントとバージョン
| コンポーネント | バージョン | リポジトリ | 本記事での役割 |
|---|---|---|---|
| Agent Sandbox | v1.0.7 | kubernetes-sigs/agent-sandbox | 本記事全体のソースコード調査対象 |
| kubernetes/kubernetes | v1.37.1 | kubernetes/kubernetes | Agent Sandboxのgo.modが参照するk8s.io/api v0.37.1に対応するリリース |
| kind | v0.33.0 | kubernetes-sigs/kind | 検証環境。ノードイメージはkindest/node:v1.37.0 |
本記事の実装・設定は OSS の v1.0.7 に固定します。GKE のマネージド機能は、OSS のコントローラーや API の動作とは分けて説明します。
TL;DR
- Sandbox は単一 Pod の実行環境を管理します。Pod の作成・停止・再作成に、必要に応じて PVC と Service を組み合わせます。
- 新規 Sandbox の Service は明示的に有効化します。spec.service: true で、同名の headless Service を作ります。
- Suspended は Pod を削除し、PVC を保持します。プロセスメモリは保存されず、再開時は Pod の新規起動になります。
- WarmPool は事前起動、Claim は払い出しを担当します。取得された Sandbox は Claim の管理に移り、プールは不足分を補充します。
- 分離と通信制御には、ランタイムとネットワークの設定も必要です。CRD を導入しただけで、すべての Pod が gVisor で動くわけではありません。
第1章: Agent Sandboxが埋めようとしているギャップ
この章では、Agent SandboxがDeploymentやStatefulSetではなく新しいCRDを作った理由を整理します。
Deployment は交換可能な Pod のレプリカを、StatefulSet は安定した識別子を持つ Pod の集合を管理します。Agent Sandbox は、単一の実行環境の払い出し・停止・期限管理をまとめて扱うための API です。README は、対象とする用途を次のように挙げています。
> Kubernetes excels at managing stateless, replicated applications (Deployments) and stable, numbered sets of stateful pods (StatefulSets). However, there's a growing need for an abstraction to handle use cases such as:
>
> * Development Environments: Isolated, persistent, network-accessible cloud environments for developers.
> * AI Agent Runtimes: Isolated environments for executing untrusted, LLM-generated code.
> * Reinforcement Learning (RL) & Evaluation: High-throughput, isolated execution sandboxes for training and evaluation loops (e.g., SWE-bench, R2E-Gym, Ray/RLlib) with low-latency warm-pool claims.
> * Notebooks and Research Tools: Persistent, single-container sessions for tools like Jupyter Notebooks.
> * Stateful Single-Pod Services: Hosting single-instance applications (e.g., build agents, small databases) needing a stable identity without StatefulSet overhead.
(参考: README.md#L221-L227)
これらは、個々の実行環境を識別し、必要に応じてデータを永続化したい用途です。永続ストレージは必須ではなく、設定に応じて利用します。StatefulSetのreplicas=1で代用することもできますが、READMEはこれを次のように評価しています。
> While these can be approximated by combining StatefulSets (size 1), Services, and PersistentVolumeClaims, this approach is cumbersome and lacks specialized lifecycle management like hibernation.
(参考: README.md#L229)
Sandbox CRDが提供する中心的な機能は、READMEで次の3点にまとめられています。
> * Stable Identity: Each Sandbox has a stable hostname and network identity.
> * Persistent Storage: Sandboxes can be configured with persistent storage that survives restarts.
> * Lifecycle Management: The Sandbox controller manages the lifecycle of the pod, including creation, scheduled deletion, pausing and resuming.
(参考: README.md#L30-L32)
2章以降で、この3点が実際にどう実装されているかを1つずつ見ていきます。
第2章: 全体アーキテクチャ
この章では、Agent Sandboxが提供するCRD群の全体像を整理します。
Agent Sandboxは、コアのSandbox CRDと、その上に乗るextensionsモジュール(SandboxTemplate/SandboxClaim/SandboxWarmPool)で構成されています。
> * SandboxTemplate: Provides a way to define reusable templates for creating Sandboxes, making it easier to manage large numbers of similar Sandboxes.
> * SandboxClaim: Allows users to create Sandboxes from a SandboxWarmPool, abstracting away the details of the underlying Sandbox configuration.
> * SandboxWarmPool: Manages a pool of pre-warmed Sandboxes that can be quickly allocated to users, reducing the time it takes to get a new Sandbox up and running.
(参考: README.md#L38-L40)
これらの関係は、READMEのアーキテクチャ図にまとめられています。
flowchart LR
User[User]
Claim[SandboxClaim]
Template[SandboxTemplate]
Sandbox[Sandbox]
Pod[Pod]
Runtime[Sandbox Runtime]
WarmPool[SandboxWarmPool]
subgraph Extensions[Extensions]
Claim
Template
WarmPool
end
%% User paths
User -->|creates| Sandbox
User -->|creates| Claim
%% Claim workflow
WarmPool -->|references| Template
Claim -->|adopts| Sandbox
%% Pod handling
Claim -->|adopts sandboxes from| WarmPool
Sandbox -->|creates Pod| Pod
%% Runtime
Pod --> Runtime
%% Warm pool
WarmPool -->|pre-warms sandboxes| Sandbox
(参考: README.md#L48-L85)
利用者は Sandbox を直接作ることも、SandboxClaim 経由でプール内の Sandbox を取得することもできます。実行中の Sandbox は原則として1個の Pod を管理し、停止中は Pod を持ちません。gVisor や Kata Containers による分離を使うには、対応するランタイムをノードに導入し、Pod テンプレートで runtimeClassName を指定します。
> Scope: Agent Sandbox is a sandbox orchestrator. It delegates low-level container isolation to secure "Sandbox Runtimes" (like gVisor or Kata Containers) by managing Pods configured to use these runtimes (via RuntimeClass).
(参考: README.md#L19-L20)
第3章: Sandboxのライフサイクル
この章では、Sandbox の operatingMode と期限設定が、実際にPodに対して何をしているかをソースコードから確認します。先に、3.1節(operatingMode)と3.2節(shutdownTime/shutdownPolicy)で見る内容をまとめた状態遷移図を示します。
stateDiagram-v2
[*] --> Running : Sandbox作成<br/>(operatingMode未指定時のデフォルト)
Running --> Suspended : operatingMode = Suspended<br/>(Podを削除、PVCは保持)
Suspended --> Running : operatingMode = Running<br/>(Podを再作成)
Running --> Expired : shutdownTimeに到達<br/>(Pod/Serviceを削除)
Suspended --> Expired : shutdownTimeに到達<br/>(Serviceを削除)
Expired --> [*] : shutdownPolicy = Delete<br/>(Sandboxオブジェクトも削除)
Running --> [*] : kubectlで明示的に削除
Suspended --> [*] : kubectlで明示的に削除
Expired --> [*] : kubectlで明示的に削除
note right of Expired
shutdownPolicy = Retain(デフォルト)の場合、
Sandboxオブジェクトは
Ready=False / SandboxExpired のまま残る
end note
(参考: 3.1節で引用したsandbox_types.go#L228-L248・sandbox_controller.go#L1328-L1357、3.2節で引用したsandbox_types.go#L328-L334・sandbox_types.go#L340-L345)
図の Expired は説明用の状態名で、operatingMode の値ではありません。期限に達すると、コントローラーが所有する Pod と Service の削除を要求します。shutdownPolicy: Retain では Sandbox が残り、Delete では Sandbox 自体も削除対象になります。削除は非同期で進むため、指定時刻にすべてのリソースが消えることを保証する仕組みではありません。
3.1 operatingMode: RunningとSuspended
SandboxSpec.operatingMode は Running / Suspended の2値で、デフォルトは Running です。指定値は望ましい状態を表し、実際の準備完了は Ready Condition、停止の進捗は Suspended Condition で確認します。
// SandboxOperatingMode defines the desired operational state of the Sandbox.
// +kubebuilder:validation:Enum=Running;Suspended
type SandboxOperatingMode string
const (
// SandboxOperatingModeRunning indicates the Sandbox should be actively running:
// the controller ensures a backing Pod (and Service, if requested) is created and
// kept running. ...
SandboxOperatingModeRunning SandboxOperatingMode = "Running"
// SandboxOperatingModeSuspended indicates the Sandbox should be suspended: the
// controller terminates the backing Pod while retaining the Sandbox object and its
// volumes. ...
SandboxOperatingModeSuspended SandboxOperatingMode = "Suspended"
)
(参考: sandbox_types.go#L228-L248)
Suspended の処理では、管理対象の Pod を削除します。プロセスの checkpoint は取得しません。
if sandbox.Spec.OperatingMode == sandboxv1beta1.SandboxOperatingModeSuspended {
if pod != nil {
...
logger.Info("Deleting Pod because .Spec.OperatingMode is Suspended", ...)
if err := r.Delete(ctx, pod); err != nil {
return pod, fmt.Errorf("failed to delete pod: %w", err)
}
...
}
...
}
(参考: sandbox_controller.go#L1328-L1357)
通常の reconcile では、PVC の作成・維持を担う reconcilePVCs が停止中も呼ばれるため、volumeClaimTemplates から作った PVC は残ります。Service を有効にしている場合は Service も保持されますが、停止中は接続先の Pod がありません。8.2節で、実際にファイルを書き込んだ後にSuspend→Resumeして、ファイルが消えないことを確認します。
:::note warn
READMEの「Desired Sandbox Characteristics」には、永続ストレージへの状態保存を伴う「Deep hibernation」や、ネットワーク接続をきっかけにした「Automatic resume」が将来像として挙げられています。
> * Deep hibernation: Saving state to persistent storage and potentially archiving the Sandbox object.
> * Automatic resume: Resuming a sandbox on network connection.
(参考: README.md#L236-L237)
ただし、v1.0.7のソースコードを探した範囲では、これに対応する実装(プロセスのメモリ状態を含めたスナップショット保存や、接続をトリガーにした自動resume)は見つかりませんでした。Suspendedで実際に起きるのは、あくまで「Pod を削除して PVC を保持する」という軽量なサスペンドです。メモリ状態まで含めたcheckpoint/restoreによるSuspend/Resumeは、Agent Substrateが実装しています(詳細はAgent Substrateの概要の2.3節・3.3節を参照)。
この「Deep hibernation」に近い機能を、GKEはPod Snapshotsという名前のGKE専用機能として別途提供しています。
!GKEクラスタ内で、稼働中のPod 1からPod Snapshot Agent経由でGCS上にスナップショットを保存し(snapshot)、停止中のPod 2/3/nをそのスナップショットから復元する(restore)様子を示した図。
(参考: Agent Sandbox のご紹介: Kubernetes と GKE 上のエージェント AI 向けの強力なガードレール | Google Cloud 公式ブログ, 2025-11-19)
> Pod Snapshots は、実行中の Pod の完全なチェックポイントと復元を可能にする、GKE 専用の新機能です。(中略)Pod Snapshots 使ってアイドル状態のサンドボックスのスナップショットを作成して一時停止できるため、エンドユーザーへの影響を最小限に抑えながらコンピューティング サイクルを大幅に節約できます。
(参考: Agent Sandbox のご紹介 | Google Cloud 公式ブログ)
GKE の Pod Snapshots は、スナップショットの取得・復元の設定を別途行う機能です。OSS の operatingMode: Suspended を指定するだけで、メモリ状態まで自動的に保存されるわけではありません。利用には対応する GKE バージョン、gVisor、保存先ストレージなどの要件があります。
(参考: Save and restore Agent Sandbox on GKE environments with Pod snapshots | Google Cloud)
:::
3.2 shutdownTime/shutdownPolicy: 期限付きの自動削除
Sandbox では spec.shutdownTime に RFC3339 形式の絶対時刻を指定します。Go の Lifecycle はインラインで埋め込まれているため、Sandbox の YAML では spec.lifecycle.shutdownTime ではありません。SandboxClaim の spec.lifecycle とは構造が異なります。
> shutdownTime is the absolute time at which the Sandbox expires. When the current time reaches shutdownTime, the controller tears down the underlying resources (Pod and Service) and then applies shutdownPolicy to the Sandbox object itself. If unset, the Sandbox never expires and lives until it is explicitly deleted.
(参考: sandbox_types.go#L285-L314、sandbox_types.go#L328-L334)
たとえば、期限付きの Sandbox の設定部分は次のようになります。時刻は利用時の将来時刻に置き換えます。
spec:
shutdownTime: "2026-12-31T23:59:59Z"
shutdownPolicy: Retain
# podTemplate などの設定は省略
shutdownPolicy(デフォルト Retain)は、期限切れ時に Sandbox 自体を削除対象にするかを制御します。
> * Retain (default): the Sandbox object is kept after its resources are torn down. Its live status fields are cleared and a Ready=False condition with reason SandboxExpired is set so the expiry is observable.
> * Delete: the Sandbox object is deleted once its underlying resources are removed.
(参考: sandbox_types.go#L340-L345)
Retain の期限切れ処理では PVC を直接削除しません。一方、volumeClaimTemplates から作った PVC は Sandbox を owner reference に持つため、Sandbox 自体を削除するとガベージコレクションの対象になります。Suspend 中の保持と、Sandbox 削除後のデータ保持は別です。PV の扱いはストレージの reclaim policy にも依存します。
(参考: sandbox_controller.go#L1711-L1788、sandbox_controller.go#L1793-L1870)
期限管理は使い捨て環境の片付けに使えますが、コントローラーの停止や Pod の終了待ちもあるため、未信頼コードの実行時間を厳密に制限する仕組みとしては扱えません。
第4章: 安定したIDとネットワーク
この章では、READMEが「Stable Identity」と呼んでいる機能の実体を確認します。
SandboxBlueprint.serviceは、*bool型でデフォルトは未設定です。コメントには、新規Sandboxでは明示的にtrueにしない限りServiceを作らないと明記されています。
> When unset, the controller preserves existing Services for backward compatibility but does not create new ones. Set to true to enable or false to explicitly disable and remove the Service.
(参考: sandbox_types.go#L275-L280)
service: trueを指定した場合に作られるのは、ClusterIP: Noneのheadless Serviceです。
service = &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: sandbox.Name,
Namespace: sandbox.Namespace,
...
},
Spec: corev1.ServiceSpec{
ClusterIP: "None",
Selector: map[string]string{
sandboxLabel: nameHash,
},
...
},
}
(参考: sandbox_controller.go#L1061-L1090)
Service 名は Sandbox 名と同じなので、<Sandbox名>.<namespace>.svc.<cluster-domain> という安定した FQDN が得られます。Pod 名も通常は Sandbox 名と同じですが、Pod の再作成時に UID や IP が維持されるわけではありません。また、headless Service は Pod のアドレスを返す仕組みなので、停止中や Ready な Pod がない間の接続性は保証されません。8.1節で、このservice: trueの有無による違いを実際に確認します。
第5章: SandboxWarmPoolとSandboxClaimで高速に払い出す
この章では、拡張APIであるSandboxWarmPool/SandboxClaimが、あらかじめ起動しておいたSandboxをどう払い出しているかを見ていきます。
Google Cloudのブログでは、この払い出しの流れを次の図で説明しています。
!Agent OrchestratorのPodがExecutor(API/SDK)経由でinit()を呼ぶと、K8s API経由でAgent Sandbox ControllerがPoolからAllocateし、Endpoint/Execution Process(gVisor)/Ephemeral Storage/Network Policyを持つTask Execution(Agent Sandbox)が払い出される図。引き取られた分はReplenish Poolで補充される。
(参考: Agent Sandbox のご紹介: Kubernetes と GKE 上のエージェント AI 向けの強力なガードレール | Google Cloud 公式ブログ, 2025-11-19)
図中の「Allocate from Pool」が本章で扱うSandboxClaimによるプールからの引き取り、「Replenish Pool」が引き取られた分をSandboxWarmPoolが補充する動きに対応しています。ソースコードレベルでの実装は、以降の5.1〜5.2節で確認します。
5.1 SandboxWarmPool: 事前に並べておく数
SandboxWarmPoolSpec.replicas は、未割り当ての Sandbox をプール内に維持する目標数です。デフォルトは1で、実際に Ready な数とは異なります。status.replicas と status.readyReplicas を分けて確認します。
type SandboxWarmPoolSpec struct {
// replicas is the desired number of sandboxes in the pool.
// This field is controlled by an HPA if specified.
// +kubebuilder:default=1
Replicas *int32 `json:"replicas,omitempty"`
// sandboxTemplateRef - name of the SandboxTemplate to be used for creating a Sandbox
// +required
TemplateRef SandboxTemplateRef `json:"sandboxTemplateRef,omitempty"`
...
}
(参考: sandboxwarmpool_types.go#L40-L51)
5.2 SandboxClaim: プールから1つ引き取る
SandboxClaimSpec.warmPoolRefで、どのWarmPoolから払い出すかを指定します。
type SandboxClaimSpec struct {
// warmPoolRef targets the specific pre-warmed infrastructure pool to check out from.
// +required
WarmPoolRef SandboxWarmPoolRef `json:"warmPoolRef"`
...
}
(参考: sandboxclaim_types.go#L109-L112)
SandboxClaimコントローラーは、プール内の候補Sandboxを見つけると、WarmPool専用のラベルを外してClaimをowner referenceに設定し直します。
delete(adopted.Labels, warmPoolSandboxLabel)
...
adopted.Labels[v1beta1.SandboxLaunchTypeLabel] = v1beta1.SandboxLaunchTypeWarm
...
adopted.OwnerReferences = nil
if err := controllerutil.SetControllerReference(claim, adopted, r.Scheme); err != nil {
...
}
(参考: sandboxclaim_controller.go#L1311-L1403)
引き取られた分は、WarmPool コントローラーが新しい Sandbox を作って補充します。払い出した Sandbox を、利用後に別の Claim 用としてプールへ自動返却する仕組みではありません。Claim を削除すると、その Claim が所有する Sandbox もガベージコレクションの対象になります。
候補があれば Ready な Sandbox を優先しますが、準備中の候補を選ぶ場合もあり、プールが空ならテンプレートから新規作成する処理もあります。プールを設定しただけで、すべての Claim が待ち時間なしに利用できるわけではありません。
(参考: sandboxclaim_controller.go#L1180-L1227)
8.3節で、準備済みの Sandbox を取得し、不足分が補充される流れを確認します。
第6章: Sandbox内部の実行基盤: sandboxdとsandbox-router
この章では、Pod 内に配置できる実行用デーモン sandboxd と、オプションの sandbox-router の役割を整理します。Sandbox CRD を作ると、これらが必ず自動で注入されるという意味ではありません。8章の nginx 例では、どちらも使いません。
6.1 sandboxd: gRPCとRESTのハイブリッドAPI
sandboxdは、プロセス実行をgRPC、ファイル操作・ヘルスチェックをRESTという、2つのプロトコルを使い分けるハイブリッドなAPIを提供します。
> ```text
> sandboxd (runtime daemon)
> ├── gRPC :9090 → ProcessService (streaming process I/O)
> └── HTTP :8080 → FilesystemService (stateless file operations & runtime probes)
> ```
>
> - Process execution is served over gRPC because Start is a long-lived server-streaming RPC: stdout/stderr flow continuously until the process exits.
> - Filesystem transfers are served over REST so file bytes move as raw application/octet-stream payloads with no protobuf wrapping, and any plain HTTP client works without generated stubs.
(参考: packages/sandboxd/USER_GUIDE.md#L6-L17)
プロセス実行とファイル操作を、異なる API で提供する構成です。プロセス実行にはストリーミングの Start に加え、結果をまとめて返す Execute もあります。コマンドは sandboxd が動くコンテナ内で実行されます。別コンテナとボリュームを共有しても、そのコンテナのバイナリやプロセスを直接操作できるわけではありません。
(参考: USER_GUIDE.md#L28-L32、USER_GUIDE.md#L99-L108)
6.2 sandbox-router: データプレーンだけを担うリバースプロキシ
sandbox-router は、既存の Sandbox Pod に HTTP トラフィックを転送します。宛先が停止中でも Sandbox の作成や Resume は行いません。
> Agent Sandbox splits creation from routing. The router only handles the data plane
(参考: sandbox-router/README.md#L7-L19)
ルーティングは X-Sandbox-ID などの HTTP ヘッダーに基づいて行われます。名前と namespace から Service の DNS 名を組み立てる方式に加え、Pod IP の指定や、--cache-enabled=true による Sandbox 情報のキャッシュ参照もあります。README 冒頭の「Sandbox を参照しない」という説明は、キャッシュを使う実装には当てはまりません。
| Header | Required | Default |
|---|---|---|
| X-Sandbox-ID | yes | — |
| X-Sandbox-UID | no | — |
| X-Sandbox-Namespace | no | default |
| X-Sandbox-Port | no | 8888 |
| X-Sandbox-Pod-IP | no | — |
(参考: sandbox-router/README.md#L29-L35)
:::note warn
sandbox-routerは、6.1節で見たsandboxdのgRPC(ProcessService)を中継できません。
> The current sandbox-router accepts HTTP/2 client connections, but disables HTTP/2 on its upstream connections and therefore cannot carry the gRPC ProcessService, so it is not a complete sandboxd transport.
(参考: packages/sandboxd/USER_GUIDE.md#L20-L23)
sandboxd のプロセス実行を SDK から使う場合は、Pod へのポートフォワードや Service など、gRPC を転送できる接続方法が必要です。ルーターのデフォルト宛先ポート 8888 も、sandboxd の 9090 / 8080 とは異なります。
:::
第7章: セキュリティモデル
この章では、docs/security/threat_model.mdが整理している信頼境界を見ます。
脅威モデルは、コントローラー・Sandbox Pod・Sandbox Router・kube-apiserver を主要コンポーネントとして整理しています。下の図はシステム側とテナント側の境界を示し、本文では利用者と API、テナント間、ワークロードとノードなどの信頼境界も挙げています。
flowchart TD
User[User / Client]
Gateway[Ingress / Gateway]
Router[Sandbox Router]
K8sAPI[Kubernetes API Server]
Controller[Sandbox Controller]
PodA["Sandbox Pod A (Tenant A)"]
PodB["Sandbox Pod B (Tenant B - Malicious)"]
User -->|API Requests| K8sAPI
User -->|Data Plane Traffic| Gateway
Gateway -->|Forward| Router
Router -->|Proxy| PodA
Router -->|Proxy| PodB
Controller -->|Watch / Reconcile| K8sAPI
Controller -->|Creates| PodA
Controller -->|Creates| PodB
subgraph SystemControl["Trust Boundary: System Control Plane"]
Controller
Router
end
subgraph TenantIsolation["Trust Boundary: Tenant Isolation"]
subgraph TenantA["Tenant A"]
PodA
end
subgraph TenantB["Tenant B"]
PodB
end
end
(参考: docs/security/threat_model.md#L12-L41)
> - Sandbox Controller (Core & Extensions): Runs in the cluster (typically in a system namespace like agent-sandbox-system) with privileges to manage Pods, Services, PVCs, and Sandbox CRDs.
> - Sandbox Pods (Workloads): Ephemeral Pods created by the controller to run user workloads (e.g., AI agents). These Pods often run untrusted, LLM-generated code.
> - Sandbox Router (Optional): A reverse proxy that routes external traffic to the correct Sandbox Pod based on HTTP headers (e.g., X-Sandbox-ID).
(参考: docs/security/threat_model.md#L7-L10)
7.1 OSS で設定する分離とアクセス制御
図の境界は設計上の分離を表しており、CRD の導入だけで通信や権限がすべて制限されるわけではありません。設定する対象を分けて確認します。
| 対象 | v1.0.7 で確認すること |
|---|---|
| 隔離ランタイム | gVisor / Kata Containers の導入と runtimeClassName。通常のコンテナ実行と区別する |
| Kubernetes API | Sandbox / Claim / Template を操作する利用者の RBAC と、ワークロードの ServiceAccount 権限 |
| ネットワーク | SandboxTemplate はデフォルトの Managed モードで共有 NetworkPolicy を管理する。実際の通信制御には対応する CNI が必要。直接作る Sandbox に同じポリシーが自動適用されるわけではない |
| ServiceAccount トークン | 拡張コントローラーは未指定の automountServiceAccountToken を false にする。直接作る Sandbox では明示的な設定を確認する |
| sandboxd | デフォルトは 0.0.0.0 で待ち受ける。--auth-token-sha256 または --auth-token-file で Bearer トークン認証を有効化でき、どちらも未設定なら認証しない |
| sandbox-router | デフォルトの認可は allow-all。ヘッダーの宛先指定自体は利用権限の証明にならないため、公開方法に応じた認証・認可を設定する |
(参考: sandboxtemplate_types.go#L102-L125、utils.go#L20-L34、sandboxd User Guide、Sandbox Router)
7.2 GKE のマネージド機能との違い
GKE には、Agent Sandbox をクラスタの機能として有効化する手順もあります。2026年10月20日時点の公式手順では、既存クラスタは v1.36.3-gke.1767000 以降が対象です。Standard クラスタでは gVisor 対応のノードプールも必要です。
gcloud beta container clusters update "${CLUSTER_NAME}" \
--location="${LOCATION}" \
--enable-agent-sandbox
有効化に加え、ワークロード側にも runtimeClassName: gvisor、非 root 実行、ServiceAccount トークンの自動マウント無効化、CPU・メモリ limit などの要件があります。8章の OSS / kind 用 nginx 例をそのまま適用する手順とは異なります。また、3.1節の Pod Snapshots には別途設定と要件があります。
(参考: Enable Agent Sandbox on GKE | Google Cloud)
第8章: kindクラスタで実際に動かす
この章では、kind での既存の検証記録に、再現に必要な YAML と待機コマンドを補って整理します。通常の nginx コンテナでリソースの生成と PVC の保持を確認するもので、gVisor / Kata Containers の分離、sandboxd、GKE の Pod Snapshots は検証対象に含めていません。
8.1 インストールと、安定したネットワークIDの有無
Docker・kind・kubectl を用意し、専用の検証クラスタを作ります。agent-sandbox-demo は既存クラスタと重複しない名前にしてください。
kind create cluster --name agent-sandbox-demo --image kindest/node:v1.37.0
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/download/v1.0.7/sandbox-with-extensions.yaml
kubectl wait deploy/agent-sandbox-controller -n agent-sandbox-system --for=condition=available --timeout=120s
(参考: README.md#L93-L100)
READMEの「Getting Started」通りにspec.serviceを指定せずSandboxを作ると、Podは作られますが、Serviceは作られませんでした。
$ kubectl apply -f - <<'EOF'
apiVersion: agents.x-k8s.io/v1beta1
kind: Sandbox
metadata:
name: my-sandbox
spec:
podTemplate:
spec:
containers:
- name: my-container
image: nginx:1.27
EOF
$ kubectl get sandbox,pods,svc
NAME READY REASON
sandbox.agents.x-k8s.io/my-sandbox True DependenciesReady
NAME READY STATUS
pod/my-sandbox 1/1 Running
NAME TYPE CLUSTER-IP
service/kubernetes ClusterIP 10.96.0.1
上の Sandbox が Ready になるまで待ちます。
kubectl wait sandbox/my-sandbox --for=condition=Ready --timeout=120s
次に、別名の Sandbox を service: true 付きで作ります。以下を sandbox-service.yaml として保存して適用します。
apiVersion: agents.x-k8s.io/v1beta1
kind: Sandbox
metadata:
name: my-sandbox-svc
spec:
service: true
podTemplate:
spec:
automountServiceAccountToken: false
containers:
- name: my-container
image: nginx:1.27
kubectl apply -f sandbox-service.yaml
kubectl wait sandbox/my-sandbox-svc --for=condition=Ready --timeout=120s
既存の検証では、4章で見た通り headless Service が作られ、status.serviceFQDN に FQDN が入りました。以下の出力は抜粋です。
$ kubectl get sandbox my-sandbox-svc -o jsonpath='{.status.serviceFQDN}'
my-sandbox-svc.default.svc.cluster.local
$ kubectl get svc my-sandbox-svc
NAME TYPE CLUSTER-IP PORT(S)
my-sandbox-svc ClusterIP None <none>
8.2 Suspend/ResumeでのPVC永続化
PVC 上のファイルが Suspend / Resume 後も残ることを確認します。動的プロビジョニングに対応する StorageClass が必要です。以下は kind のデフォルトの standard を使う例で、最初に存在を確認します。
kubectl get storageclass standard
以下を sandbox-pvc.yaml として保存します。data の PVC はコントローラーが Pod のボリュームへ追加するため、volumeMounts に同名を指定します。
apiVersion: agents.x-k8s.io/v1beta1
kind: Sandbox
metadata:
name: my-sandbox-pvc
spec:
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ReadWriteOnce]
storageClassName: standard
resources:
requests:
storage: 1Gi
podTemplate:
spec:
automountServiceAccountToken: false
containers:
- name: my-container
image: nginx:1.27
volumeMounts:
- name: data
mountPath: /data
kubectl apply -f sandbox-pvc.yaml
kubectl wait sandbox/my-sandbox-pvc --for=condition=Ready --timeout=120s
以下は既存の検証記録です。Suspend の指定後は非同期に Pod が終了します。
$ kubectl exec my-sandbox-pvc -- sh -c 'echo "hello from before suspend" > /data/note.txt'
$ kubectl patch sandbox my-sandbox-pvc --type=merge -p '{"spec":{"operatingMode":"Suspended"}}'
$ kubectl wait sandbox/my-sandbox-pvc --for=condition=Suspended --timeout=120s
$ kubectl get pods
NAME READY STATUS
my-sandbox 1/1 Running
my-sandbox-svc 1/1 Running
# my-sandbox-pvc の Pod は消えている
$ kubectl get pvc
NAME STATUS CAPACITY
data-my-sandbox-pvc Bound 1Gi
# PVCはBoundのまま残っている
operatingMode: Runningに戻すと、Podが再作成され、Suspend前に書き込んだファイルがそのまま読み出せました。
$ kubectl patch sandbox my-sandbox-pvc --type=merge -p '{"spec":{"operatingMode":"Running"}}'
$ kubectl wait sandbox/my-sandbox-pvc --for=condition=Ready --timeout=120s
$ kubectl exec my-sandbox-pvc -- cat /data/note.txt
hello from before suspend
3.1節で確認した通り、ここで起きているのは「Podの削除・再作成」だけで、プロセスのメモリ状態などは保存されません。今回のnginxコンテナはファイルシステムに書き込んだデータだけを扱っているため、PVCさえ残っていれば復元できましたが、プロセス内部の状態を引き継ぎたい場合はアプリケーション側でファイルに書き出しておく必要があります。
8.3 SandboxWarmPoolからSandboxClaimが1つ引き取る
WarmPool / Claim の関係を確認するため、次の3ファイルを用意します。テンプレートの nginx は API の動作確認用で、sandboxd の API は提供しません。
sandboxtemplate.yaml:
apiVersion: extensions.agents.x-k8s.io/v1beta1
kind: SandboxTemplate
metadata:
name: demo-template
spec:
podTemplate:
spec:
automountServiceAccountToken: false
containers:
- name: nginx
image: nginx:1.27
sandboxwarmpool.yaml:
apiVersion: extensions.agents.x-k8s.io/v1beta1
kind: SandboxWarmPool
metadata:
name: demo-pool
spec:
replicas: 2
sandboxTemplateRef:
name: demo-template
claim.yaml:
apiVersion: extensions.agents.x-k8s.io/v1beta1
kind: SandboxClaim
metadata:
name: my-claim
spec:
warmPoolRef:
name: demo-pool
3つとも同じ namespace に作成します。まずテンプレートとプールを適用し、status.readyReplicas が2になるまで確認します。
kubectl apply -f sandboxtemplate.yaml -f sandboxwarmpool.yaml
kubectl get sandboxwarmpool demo-pool -w
確認後は Ctrl-C で監視を終了します。以下は既存の検証記録の抜粋で、Sandbox 名の接尾辞は実行ごとに変わります。
$ kubectl apply -f sandboxtemplate.yaml -f sandboxwarmpool.yaml
$ kubectl get sandbox -l agents.x-k8s.io/warm-pool-sandbox
NAME READY
demo-pool-6zcpf True
demo-pool-8pzkl True
ここへSandboxClaimを作ると、2個のうち1個(demo-pool-8pzkl)が引き取られ、WarmPool用のラベルが外れてowner referenceがSandboxClaimに変わりました。
$ kubectl apply -f claim.yaml
$ kubectl wait sandboxclaim/my-claim --for=condition=Ready --timeout=120s
$ kubectl get sandboxclaim my-claim -o jsonpath='{.status.sandbox.name}'
demo-pool-8pzkl
$ kubectl get sandbox demo-pool-8pzkl -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}'
SandboxClaim/my-claim
同時に、プールの数を維持するため、新しいSandbox(demo-pool-fnfs2)が自動で補充されていました。
$ kubectl get sandbox -l agents.x-k8s.io/warm-pool-sandbox
NAME READY
demo-pool-6zcpf True
demo-pool-fnfs2 True
# demo-pool-8pzkl はClaimに引き取られたのでこの一覧から外れている
再現時は、固定の Sandbox 名を使わず、Claim の status から取得できます。
CLAIMED_SANDBOX=$(kubectl get sandboxclaim my-claim -o jsonpath='{.status.sandbox.name}')
kubectl get sandbox "${CLAIMED_SANDBOX}" \
-o 'jsonpath={.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
ここでは、準備済みの Pod の取得とプールの補充を確認しています。起動時間の短縮幅は測定していません。プールが空の場合などは新規作成になるため、常に同じ待ち時間になるわけではありません。
検証が終わったら、作成したクラスタを削除します。この手順では PVC 上の検証データも失われます。
kind delete cluster --name agent-sandbox-demo
おわりに
Agent Sandbox は、単一 Pod の実行環境に、停止・再開やプールからの払い出しを組み合わせる API として整理できました。Agent Substrate とは停止・再開という言葉が共通する一方、保存する状態や実行単位に違いがあり、使い分けを考えるうえで重要だと感じました。
GKE にマネージドの導入手順が用意されている点も印象に残りました。OSS の機能と GKE 固有の機能を分けながら、引き続き確認していきたいと思います。
参考
参考リンク
本文で登場する順に並べています。
- LeaderWorkerSet と DisaggregatedSet の概要
- kubernetes-sigs/agent-sandbox
- Bringing you Agent Sandbox on GKE and Agent Substrate | Google Cloud Blog
- Agent Substrate の概要
- Agent Sandbox のご紹介: Kubernetes と GKE 上のエージェント AI 向けの強力なガードレール | Google Cloud 公式ブログ
- Save and restore Agent Sandbox on GKE environments with Pod snapshots | Google Cloud
- sandboxd User Guide | agent-sandbox
- Sandbox Router (Go) | agent-sandbox
- Threat Model | agent-sandbox
- Enable Agent Sandbox on GKE | Google Cloud
agent-sandbox: easy management of isolated, stateful, singleton workloads on Kubernetes (4.3k stars)
What it does
All text below is quoted directly from the project README.
>agent-sandbox enables easy management of isolated, stateful, singleton workloads, ideal for use cases like AI agent runtimes and reinforcement learning (RL).
Key features of the Sandbox CRD include:
- Stable Identity: Each Sandbox has a stable hostname and network identity.
- Persistent Storage: Sandboxes can be configured with persistent storage that survives restarts.
- Lifecycle Management: The Sandbox controller manages the lifecycle of the pod, including creation, scheduled deletion, pausing and resuming.
Extensions:
- SandboxTemplate: Provides a way to define reusable templates for creating Sandboxes, making it easier to manage large numbers of similar Sandboxes.
- SandboxClaim: Allows users to create Sandboxes from a SandboxWarmPool, abstracting away the details of the underlying Sandbox configuration.
- SandboxWarmPool: Manages a pool of pre-warmed Sandboxes that can be quickly allocated to users, reducing the time it takes to get a new Sandbox up and running.
>This project is developing a Sandbox Custom Resource Definition (CRD) and controller for Kubernetes, under the umbrella of SIG Apps. The goal is to provide a declarative, standardized API for managing workloads that require the characteristics of a long-running, stateful, singleton container with a stable identity, much like a lightweight, single-container VM experience built on Kubernetes primitives.
>kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/sandbox-with-extensions.yaml
Repository
kubernetes-sigs/agent-sandbox
Repository stats
|Metric|Value|
|:-|:-|
|Stars|4,296|
|Forks|568|
|Last release|v1.0.7 (2026-10-15)|
|Languages|Go 51.5%, Python 32.7%, TypeScript 10.4%|
|Contributors|168|
|AI contributor accounts (Claude / Codex / Cursor / Copilot)|None|
|AI co-authored commits|None|
Main contributor
|Field|Value|
|:-|:-|
|Name|Janet Kuo|
|GitHub|@janetkuo|
|Commits to this repo|109 (top contributor)|
|Bio|@kubernetes Steering Committee, SIG Apps chair TL, Kubernetes AI Conformance lead|
|Company / Location|Google / Mountain View, CA|
|X / Twitter|@janet_kuo|
|Followers|1,100|
16 points
How AI Agents Sharing a Sandbox Leak Each Other's Data
Originally published at webofmike.com on 2026-10-13. The demo repo and every command in it were run before publishing.
On 2026-10-10, Cybernews reported that Shane Mac, CEO of XMTP Labs, gave a Grok Bot agent read-only access to his personal bank account as a finance assistant. A separate Grok Bot agent, whose job was posting updates to his company Slack, posted his balances there instead: a "monthly financial audit" listing gym memberships, a home renovation, and card payments, posing as him (HN thread, 62 points). Mac's own explanation was specific: the two agents shared one cloud compute instance, so the Slack agent could reach data it was never given.
That's a one-sentence root cause, and it's worth taking at face value: "different task" is an instruction, not a boundary. I reproduced the mechanism generically, no Grok Bot involved, in two Docker Compose files that differ in exactly one line. Repo: themsquared/shared-sandbox-agent-leak.
Disclosure: I work at Solo.io, which created kagent, one of the projects mentioned below. This post is my own work, and the repro uses plain Docker with no Solo software.
The setup
Two toy agents, neither one LLM-driven, because the point is the compute boundary, not agent reasoning:
- finance-agent writes a balance snapshot to /secrets/finance.json. That's its entire job.
- slack-agent posts messages to a webhook. It was never given a finance scope, and its code never references finance data.
- mock-slack is a 20-line http.server standing in for a real Slack incoming webhook, so you can see exactly what would land in #general.
slack_agent.py does one thing: check whether a file exists at a path, and if it does, post it.
def main():
time.sleep(2)
try:
with open(SECRET_PATH) as f:
leaked = json.load(f)
print(f"[slack-agent] found a file at {SECRET_PATH} I was never given, posting it, because that's my job")
post_to_slack({"channel": "#general", "text": "monthly financial audit", "attachment": leaked})
print("[slack-agent] LEAKED: posted finance data to Slack")
except FileNotFoundError:
print(f"[slack-agent] no file at {SECRET_PATH}, nothing to post, nothing leaked")
There's no finance-specific logic to remove, no prompt to rewrite, no permission check that was skipped. Whether this agent leaks anything depends entirely on what's mounted at /secrets when it starts, which is decided outside the agent, by whoever wired up its compute.
Reproducing the leak
docker-compose.vulnerable.yml mounts the same named volume into both agents at /secrets:
services:
finance-agent:
volumes:
- shared-secrets:/secrets
command: ["python", "/app/finance_agent.py"]
slack-agent:
volumes:
- shared-secrets:/secrets
command: ["python", "/app/slack_agent.py"]
volumes:
shared-secrets:
Both containers mount shared-secrets. Run it:
docker compose -f docker-compose.vulnerable.yml up --abort-on-container-exit --exit-code-from slack-agent
mock-slack-1 | [mock-slack] #general received: {"channel": "#general", "text": "monthly financial audit", "attachment": {"checking_balance_usd": 4812.33, "line_items": ["gym membership -45.00", "home renovation -1200.00", "card payment -389.12"] }}
slack-agent-1 | [slack-agent] found a file at /secrets/finance.json I was never given, posting it, because that's my job
slack-agent-1 | [slack-agent] LEAKED: posted finance data to Slack
slack-agent did exactly what it was built to do: look for something to post, find it, post it. The leak isn't a bug in either agent's code. It's the absence of a boundary between them.
The fix is one line, at the compute layer
docker-compose.isolated.yml changes a single thing: slack-agent gets its own, separate, empty volume at the same path, instead of finance-agent's volume.
services:
finance-agent:
volumes:
- finance-secrets:/secrets
slack-agent:
volumes:
- slack-agent-own-sandbox:/secrets # was: finance-secrets
volumes:
finance-secrets:
slack-agent-own-sandbox:
Same two agents. Same two Python files, byte for byte. Same two tasks. Run it:
docker compose -f docker-compose.isolated.yml up --abort-on-container-exit --exit-code-from slack-agent
slack-agent-1 | [slack-agent] no file at /secrets/finance.json, nothing to post, nothing leaked
slack-agent looks in exactly the same place and finds nothing, because there's nothing there to find. Neither agent's code changed. Neither agent's prompt or task description changed. Neither agent is aware the other exists, before or after the fix. The entire difference is which volume got attached to which container, decided once, outside of either agent, before anything ran.
./run.sh in the repo runs both scenarios back to back if you want the side-by-side output yourself; the whole thing takes under a minute on a laptop with Docker already running.
Why this isn't just a Docker-volume toy problem
The shared-sandbox failure mode is common enough that the Kubernetes agent ecosystem is actively standardizing the fix, because it doesn't live in the agent's prompt or code; it lives in whatever schedules the agent's compute:
- kubernetes-sigs/agent-sandbox shipped v1.0.6 on 2026-10-08. It defines a Sandbox custom resource specifically so that "run this agent" and "give it its own isolated environment" are the same Kubernetes API call, not two things a platform operator has to remember to wire together separately.
- kagent shipped gVisor-backed microVM sandboxes in v1.0.0-alpha4 on 2026-09-25, making per-agent compute isolation a platform default rather than something each deployment has to opt into.
Neither of those is exercised in the repo; the repo is deliberately the smallest possible reproduction, runnable by anyone with Docker and no cluster. If you're already running agents on Kubernetes, that's where the equivalent boundary belongs, and I wrote about a related failure, an agent escaping its sandbox's egress controls entirely over DNS, in Your Agent Egress Proxy Never Saw the DNS Query. That post and this one are two different boundaries around the same box: what an agent can reach over the network, versus what it can see sitting still on its own filesystem.
Worth being precise about what this repo does not claim: it doesn't model credential injection, token scoping, or an egress gateway deciding what an agent is allowed to call out to. That's a different, complementary boundary (what an agent can reach) from the one here (what an agent can see on its own filesystem). Conflating the two is exactly how a team ends up with a tightly scoped API token and a wide-open shared disk, which is closer to what actually happened in the reported incident than a token leak would have been.
Honest limits
- Both agents here are a dozen lines of Python each, not LLM-driven. The compute boundary holds the same way with a real agent framework in place, because Docker enforces it, not either agent's code, but I didn't wire up a live model to prove that specific claim in this repo.
- mock-slack is a stand-in http.server, not real Slack. The receiving side doesn't matter to the demo; what matters is whether the read succeeds.
- Only one failure mode is modeled: a shared named volume. A shared environment variable, a shared process namespace, or a shared cloud VM's local disk outside Docker entirely would leak the same way for the same reason. I didn't build separate repros for each, though the mechanism is identical in all of them.
Try it
git clone https://github.com/themsquared/shared-sandbox-agent-leak.git
cd shared-sandbox-agent-leak
./run.sh
Two agents, two jobs, one shared filesystem: one of them posts the other's secrets to Slack. Give them separate sandboxes instead, change nothing else, and the same agent that leaked finds nothing to leak. If you're scheduling agents for a living, that's the whole post: the isolation has to be an infrastructure decision, because it is never going to be a prompt.
Frequently asked questions
Why did a Grok Bot AI agent leak bank account data into Slack?
Per Cybernews' 2026-10-10 report, XMTP Labs CEO Shane Mac gave a Grok Bot agent read-only access to his personal bank account. A separate Grok Bot agent, configured only to post updates to his company Slack, posted his balances there instead. Mac's own explanation: the two agents shared one cloud compute instance, so the Slack agent could reach data it was never scoped to see.
Does giving two AI agents different tasks keep their data separate?
No. A task description is a prompt-level instruction, not a compute boundary. If two agents share a filesystem, environment, or process namespace, either one can read whatever the other wrote there, regardless of what either agent was told to do. Isolation has to be enforced below the prompt, at the compute layer.
How do you isolate AI agents running on the same host or cluster?
Give each agent its own sandbox: a separate container, volume, or microVM, so there is no shared filesystem, environment variable set, or process namespace between agents. kubernetes-sigs/agent-sandbox defines a Sandbox CRD for this on Kubernetes, and kagent ships gVisor-backed microVM sandboxes as a platform default in its v1.0.0-alpha series.
What's the difference between agent sandboxing and agent credential scoping?
Sandboxing controls what an agent can see on its own machine: its filesystem, its environment, and whether another agent's data is reachable at all. Credential scoping and egress gateways control what an agent can reach over the network once it's running. They're complementary boundaries that solve different problems; this post is about the first one.
---
*Canonical version, with machine-readable markdown at https://webofmike.com/shared-sandbox-agent-leak/index.md: webofmike.com
Adds a docker-sbx example that runs Docker Sandboxes (sbx) in a Sandbox on a KVM node. The pod hosts the sbx daemon and each agent runs in its own microVM. It follows the KVM-in-a-pod pattern of windows-sandbox.
agent-sandbox#1831 t.co
2 likes · 169 views
I'm honestly getting lost between what primary usecase/workflow each of these is intended for:
1. kagent.dev
2. kubernetes-sigs/agent-sandbox
3. docker/docker-agent (this project)
4. docs.langchain.com
5. cloudflare.com
Mentions
How many of the agent developers are using this for their agents? kubernetes-sigs/agent-sandbox
Autonomous agent loops with Gemini 4 Argon and GKE Agent Sandbox: Running untrusted code safely at scale --- title: "Autonomous agent loops with Gemini 4 Argon and GKE Agent Sandbox: Running untrusted code safely at scale" published: true description: "Run untrusted LLM-generated Python and Bash code safely at scale using Gemini 4 Argon and GKE Agent Sandbox with gVisor isolation." tags: kubernetes, ai, gke, googlecloud cover_image: dev-to-uploads.s3.us-east-2.amazonaws.com --- Autonomous AI systems are only as useful as the actions they can verify. With the release of Gemini 4 Argon, multi-step reasoning and native tool calling allow agents to write data transformation scripts, generate infrastructure migrations, and debug failing tests across long execution horizons. However, a fundamental architectural bottleneck emerges the moment the model outputs a block of Python or Bash: something has to execute that code and return the output so the model can iterate. Running LLM-generated code inside the same Kubernetes Pod as your orchestrator application is an immediate security liability. A hallucinated command, a prompt-injection payload, or a dependency conflict can corrupt the container filesystem, exfiltrate mounted service account credentials, or starve adjacent workloads of memory. Conversely, spinning up a fresh Kubernetes Job or virtual machine for every turn of a multi-step reasoning loop introduces 5 to 30 seconds of cold-start latency per iteration, stalling the agent workflow. By pairing a Gemini 4 Argon reasoning loop with GKE Agent Sandbox (Generally Available on GKE 1.32 and later across both GKE Autopilot and GKE Standard), platform teams can isolate every code execution step inside a kernel-hardened gVisor runtime with sub-second allocation from pre-warmed Pod pools. In this article, I will walk through the architecture of a self-correcting Gemini 4 Argon code execution loop on Google Kubernetes Engine (GKE), show how to configure warm sandbox pools across GKE Autopilot and Standard, and implement a Python orchestrator that claims, executes, and recycles isolated sandboxes. Architectural tension in multi-turn code execution When a Gemini 4 Argon agent solves an analytical or engineering task, it rarely stops at a single code snippet. Instead, it operates in an iterative loop: 1. Task submission: The client sends an analytical query or code generation request to the GKE orchestrator Pod. 2. Model reasoning and tool dispatch: The orchestrator invokes Gemini 4 Argon with an execute_python tool declaration. The model emits a tool call containing the proposed script. 3. Warm pool allocation: The orchestrator requests a pre-warmed sandbox from the GKE Sandbox Router, which allocates an initialized gVisor Pod in under 200ms. 4. Isolated execution: The untrusted script runs inside the sandbox, capturing stdout, stderr, and exit codes without host kernel access. 5. Recycling and feedback: The used sandbox is returned to the router for teardown, and execution outputs are fed back to Gemini 4 Argon for iterative refinement until the final answer is ready. Evaluating where to execute that generated code requires balancing isolation boundaries against allocation latency: | Execution pattern | Isolation boundary | Allocation latency | Operational trade-off | | :--- | :--- | :--- | :--- | | In-process subprocess in orchestrator Pod | Shared container kernel and filesystem | < 10ms | Exposes Workload Identity tokens, application memory, and internal cluster networking to arbitrary code | | Per-invocation Kubernetes Job | Standard container (runc) or RuntimeClass | 3s to 20s | Scheduling, image pulling, and container initialization add seconds of delay to every turn of the agent loop | | GKE Agent Sandbox with SandboxWarmPool | User-space kernel via gVisor + default-deny network | < 200ms (p90) | Requires maintaining a small footprint of pre-warmed idle Pods in exchange for sub-second isolation | GKE Agent Sandbox resolves this tension by combining four open-source custom resources from the kubernetes-sigs/agent-sandbox project with GKE kernel isolation: - Kernel-level isolation via Sandbox and gVisor: Each Sandbox (agents.x-k8s.io/v1alpha1) manages an isolated Pod running inside GKE Sandbox, intercepting application system calls in user space so untrusted code never interacts directly with the host Linux kernel. - Reusable blueprints via SandboxTemplate: A SandboxTemplate (extensions.agents.x-k8s.io/v1alpha1) defines the Pod specification, resource boundaries, and security context once for a class of agent tools. - Pre-warmed capacity via SandboxWarmPool: Rather than waiting for the Kubernetes scheduler and container runtime on each tool call, a SandboxWarmPool (extensions.agents.x-k8s.io/v1alpha1) maintains a standing buffer of initialized Sandbox Pods ready to be claimed in under 200 milliseconds. - Declarative allocation via SandboxClaim and zero-trust posture: When an agent needs to run code, the Sandbox Router creates a SandboxClaim (extensions.agents.x-k8s.io/v1alpha1) to bind a warm Sandbox Pod and deletes the claim on release. Sandbox Pods disable Kubernetes service account token automounting (automountServiceAccountToken: false) and enforce strict network egress policies, keeping your orchestrator's Workload Identity Federation for GKE credentials unreachable from the code execution environment. Configuring the cluster and sandbox node pools Before deploying the sandbox templates and the Gemini orchestrator, you need a GKE cluster with gVisor (GKE Sandbox) support and the Agent Sandbox controller installed. Enabling Agent Sandbox on GKE Autopilot On GKE Autopilot, GKE Sandbox is supported out of the box and automatically provisions gVisor-enabled compute capacity whenever Pods request runtimeClassName: gvisor. If you are using the managed GKE Agent Sandbox addon preview, pass --enable-agent-sandbox via gcloud beta; otherwise, create a standard Autopilot cluster and install the upstream controller manifests in the next step: export PROJECT_ID="production-ai-platform" export LOCATION="us-central1" export CLUSTER_NAME="agent-execution-cluster" gcloud beta container clusters create-auto "${CLUSTER_NAME}" \ --project="${PROJECT_ID}" \ --location="${LOCATION}" \ --enable-agent-sandbox Enabling Agent Sandbox on GKE Standard On GKE Standard, your cluster must keep at least one regular node pool for system Pods while isolating untrusted execution onto a dedicated node pool configured with --image-type=cos_containerd and --sandbox=type=gvisor. Separating the untrusted execution node pool from the node pool hosting your orchestrator Pods also prevents noisy-neighbor resource contention: # Enable the Agent Sandbox addon on an existing Standard cluster (beta) gcloud beta container clusters update "${CLUSTER_NAME}" \ --project="${PROJECT_ID}" \ --location="${LOCATION}" \ --enable-agent-sandbox # Create a dedicated gVisor node pool for untrusted code execution gcloud container node-pools create sandbox-execution-pool \ --cluster="${CLUSTER_NAME}" \ --project="${PROJECT_ID}" \ --location="${LOCATION}" \ --machine-type=e2-standard-8 \ --image-type=cos_containerd \ --sandbox=type=gvisor \ --num-nodes=3 \ --enable-autoscaling \ --min-nodes=2 \ --max-nodes=10 If you are installing the open-source kubernetes-sigs/agent-sandbox controller and extension CRDs (Sandbox, SandboxTemplate, SandboxClaim, and SandboxWarmPool) directly onto any GKE Autopilot or Standard cluster, apply the release manifests: export AGENT_SANDBOX_VERSION="v0.1.0" kubectl apply -f "https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${AGENT_SANDBOX_VERSION}/manifest.yaml" kubectl apply -f "https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${AGENT_SANDBOX_VERSION}/extensions.yaml" Defining the SandboxTemplate, SandboxWarmPool, and SandboxClaim Once the cluster and CRDs are ready, you define the execution environment using SandboxTemplate and SandboxWarmPool. When you specify runtimeClassName: gvisor on GKE, the GKE admission controller automatically injects the required sandbox.gke.io/runtime: gvisor node selector and taint tolerations on both Autopilot and Standard clusters. Create a manifest named analytics-sandbox-pool.yaml in a dedicated agent-sandboxes namespace. This configuration enforces non-root execution, drops all Linux capabilities, disables service account token mounting, and keeps 5 pre-warmed Python runtime Pods ready for tool calls: apiVersion: v1 kind: Namespace metadata: name: agent-sandboxes --- apiVersion: extensions.agents.x-k8s.io/v1alpha1 kind: SandboxTemplate metadata: name: python-analytics-template namespace: agent-sandboxes spec: podTemplate: metadata: labels: app.kubernetes.io/component: execution-sandbox sandbox-tier: untrusted-python spec: # GKE automatically adds the gVisor nodeSelector and toleration for this RuntimeClass runtimeClassName: gvisor automountServiceAccountToken: false securityContext: runAsNonRoot: true runAsUser: 10001 seccompProfile: type: RuntimeDefault containers: - name: python-runtime image: registry.k8s.io/agent-sandbox/python-runtime-sandbox:v0.1.0 ports: - containerPort: 8888 securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL resources: requests: cpu: "500m" memory: "1Gi" ephemeral-storage: "2Gi" limits: cpu: "1000m" memory: "1Gi" ephemeral-storage: "2Gi" --- apiVersion: extensions.agents.x-k8s.io/v1alpha1 kind: SandboxWarmPool metadata: name: python-analytics-warmpool namespace: agent-sandboxes spec: replicas: 5 sandboxTemplateRef: name: python-analytics-template Under the hood, whenever the Sandbox Router (or a Kubernetes client) allocates a sandbox from this warm pool, it creates a SandboxClaim resource targeting python-analytics-template: apiVersion: extensions.agents.x-k8s.io/v1alpha1 kind: SandboxClaim metadata: name: analytics-task-claim-example namespace: agent-sandboxes spec: sandboxTemplateRef: name: python-analytics-template To prevent untrusted code from scanning internal cluster services or calling the GKE metadata server, apply a default-deny NetworkPolicy (isolate-untrusted-sandboxes.yaml) in the agent-sandboxes namespace that blocks all egress and only permits ingress on port 8888 from the Sandbox Router: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: isolate-untrusted-sandboxes namespace: agent-sandboxes spec: podSelector: matchLabels: app.kubernetes.io/component: execution-sandbox policyTypes: - Ingress - Egress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: agent-sandbox-system - podSelector: matchLabels: app.kubernetes.io/component: sandbox-router ports: - protocol: TCP port: 8888 egress: [] Apply the manifests to your cluster: kubectl apply -f analytics-sandbox-pool.yaml kubectl apply -f isolate-untrusted-sandboxes.yaml Building the Gemini orchestrator loop With the sandbox pool running in agent-sandboxes, you can deploy the orchestrator service in a separate agent-orchestrator namespace. The orchestrator uses Workload Identity Federation for GKE to authenticate with the Gemini platform and calls the Sandbox Router whenever Gemini invokes the code execution tool. Here is the core implementation (orchestrator.py) using the google-genai SDK and the agentic_sandbox client. In the snippet below, GEMINI_MODEL defaults to gemini-4-argon: import os from google import genai from google.genai import types from agentic_sandbox import SandboxClient # Defaults to gemini-4-argon on the Gemini platform: MODEL_ID = os.environ.get("GEMINI_MODEL", "gemini-4-argon") # Initialize the Gemini platform client using Workload Identity credentials genai_client = genai.Client( vertexai=True, project=os.environ["GOOGLE_CLOUD_PROJECT"], location=os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1"), ) # Initialize the GKE Agent Sandbox client (communicates via the Sandbox Router) sandbox_client = SandboxClient(namespace="agent-sandboxes") def execute_python_in_sandbox(code: str) -> dict[str, str | int]: """Executes Python code inside an isolated GKE gVisor sandbox and returns stdout, stderr, and exit code.""" sandbox = sandbox_client.claim_sandbox( template_name="python-analytics-template", timeout_seconds=5, ) try: result = sandbox.execute(code, timeout_seconds=30) return { "stdout": result.output or "", "stderr": result.error or "", "exit_code": result.exit_code, } finally: # Always release the sandbox so the SandboxClaim is deleted and replaced sandbox.release() def run_autonomous_task(user_prompt: str) -> str: """Runs a multi-turn Gemini 4 Argon reasoning loop backed by GKE Agent Sandbox.""" config = types.GenerateContentConfig( system_instruction=( "You are a quantitative engineering agent. When solving data or systems problems, " "write Python scripts and run them using execute_python_in_sandbox. " "If execution returns a non-zero exit_code or stderr traceback, inspect the error, " "adjust your code, and re-run until the output is verified." ), tools=[execute_python_in_sandbox], automatic_function_calling=types.AutomaticFunctionCallingConfig( maximum_remote_calls=10, ), temperature=0.1, ) chat = genai_client.chats.create( model=MODEL_ID, config=config, ) response = chat.send_message(user_prompt) return response.text or "" When genai_client.chats.create receives a tool declaration with a typed Python function, the google-genai SDK automatically manages the multi-turn tool-calling loop (bounded by maximum_remote_calls=10), invoking execute_python_in_sandbox each time the model emits a function call and feeding the structured stdout, stderr, and exit_code dictionary back into the conversation context. Because sandbox.release() runs inside a finally block, every execution turn gets a clean, ephemeral container filesystem, preventing state pollution across unrelated tasks. Verifying sandbox allocation and observability After deploying the warm pool and orchestrator, verify that the pre-warmed Pods are scheduled under the gVisor runtime and that the warm pool controller is maintaining target capacity: # Inspect the warm pool status, active sandboxes, and claims kubectl get sandboxwarmpool,sandbox,sandboxclaim -n agent-sandboxes # Confirm that sandbox Pods are running with RuntimeClass gvisor kubectl get pods -n agent-sandboxes \ -l app.kubernetes.io/component=execution-sandbox \ -o custom-columns="NAME:.metadata.name,STATUS:.status.phase,RUNTIME:.spec.runtimeClassName,NODE:.spec.nodeName" Expected output: NAME STATUS RUNTIME NODE python-analytics-warmpool-6d8f9b7c4-2x9kl Running gvisor gke-agent-execution-sandbox-pool-8a12f-9b2c python-analytics-warmpool-6d8f9b7c4-7m4pq Running gvisor gke-agent-execution-sandbox-pool-8a12f-9b2c python-analytics-warmpool-6d8f9b7c4-9v1zr Running gvisor gke-agent-execution-sandbox-pool-8a12f-c41d To inspect sandbox claim latency and catch container OOM kills triggered by runaway agent scripts in Cloud Logging, run the following query: resource.type="k8s_pod" resource.labels.namespace_name="agent-sandboxes" (reason="OOMKilling" OR reason="SandboxClaimed" OR reason="SandboxReleased") In Cloud Monitoring, track kubernetes.io/pod/ephemeral_storage/used_bytes and kubernetes.io/container/memory/used_bytes filtered by namespace_name="agent-sandboxes" to rightsize your SandboxTemplate resource limits based on the actual memory footprint of the scripts Gemini 4 Argon generates. Operational trade-offs and sharp edges When running GKE Agent Sandbox with Gemini 4 Argon in production, keep these operational boundaries in mind: - Warm pool sizing vs. burst concurrency: If 20 concurrent Gemini 4 Argon requests trigger tool execution simultaneously and your SandboxWarmPool only holds replicas: 5, the remaining 15 requests will wait for cold Pod creation. Size replicas to cover your p95 burst concurrency during peak traffic hours. - Stateful multi-step sessions: In the example above, releasing the sandbox inside execute_python_in_sandbox destroys the Pod after every turn. If your Gemini 4 Argon agent writes files to disk in turn 1 and analyzes them in turn 2, bind the sandbox instance to the chat session lifecycle and invoke sandbox.release() only after the entire conversation completes. - gVisor syscall overhead for heavy I/O: Because gVisor intercepts system calls in user space, compute-bound Python scripts (such as NumPy or Pandas array operations) run near native speed, whereas workloads performing thousands of small file opens or network socket creations incur additional CPU overhead. - Hard execution timeouts: LLM-generated code can inadvertently introduce infinite while loops. Always enforce a strict timeout_seconds parameter on sandbox.execute() so the orchestrator returns a timeout error to Gemini 4 Argon rather than blocking the request worker indefinitely. Next steps Pairing Gemini 4 Argon with GKE Agent Sandbox gives your agents the ability to test and self-correct their own code without exposing your cluster or credentials to untrusted execution. By isolating every tool invocation inside a pre-warmed gVisor Pod with zero network egress, you get both kernel-level safety and sub-second iteration latency. To build this pattern in your own environment, explore the GKE Agent Sandbox conceptual guide, follow the GKE Agent Sandbox installation instructions, and check out the upstream kubernetes-sigs/agent-sandbox repository.
Agent Sandboxの概要 はじめに LeaderWorkerSetと同じくSIG Appsが管理するKubernetesサブプロジェクトに、Agent Sandboxがあります。LWS が leader と worker の Pod グループを扱うのに対し、Agent Sandbox は、安定した ID を持つ単一 Pod の実行環境を宣言的に管理する Sandbox CRD を提供します。AIエージェントの実行環境や開発者向けの使い捨て環境、強化学習の評価ループなど、用途は異なりますが根っこにある「分散推論・分散エージェントをKubernetesでどう扱うか」という問題意識はLWSと共通しています。 Google Cloud は、Agent Substrate と Kubernetes 上のワークロードの位置づけを、以下の図で示しています。 !Agent Substrateを中心とした、エージェント基盤のレイヤー構成図。最上位にAgent Runtime/Agent Harnesses/Custom Agents&Toolsからなる Agent Application Layer、その下にSandbox Isolation(gVisor/MicroVM)を含むAgent Substrateと、Deployments/StatefulSets/LWSなどのOther k8s workloads、最下層にKubernetesとIaaS/Bare metal、Blob storageが並ぶ。 (参考: Bringing you Agent Sandbox on GKE and Agent Substrate | Google Cloud Blog, 2026-05-20) この図は、Agent Substrate が sandbox の分離基盤を利用する一方、LWS が Other k8s workloads に並ぶことを示しています。ただし、図中の Sandbox Isolation (gVisor / MicroVM) はランタイムによる分離を表しており、OSS の Sandbox CRD やそのコントローラーと同一ではありません。本記事では、Agent Sandbox の CRD が管理する Pod と、その Pod に指定する隔離ランタイムを分けて説明します。 関連する仕組みについては、Agent Substrate の概要と LeaderWorkerSet と DisaggregatedSet の概要も執筆しています。 この記事では、Agent Sandboxがどういう課題を解決しようとしているAPIなのか、Sandboxのライフサイクルがどう実装されているか、そしてSandboxWarmPool/SandboxClaimという拡張APIが何をしているのかを整理します。 対象コンポーネントとバージョン | コンポーネント | バージョン | リポジトリ | 本記事での役割 | |---|---|---|---| | Agent Sandbox | v1.0.7 | kubernetes-sigs/agent-sandbox | 本記事全体のソースコード調査対象 | | kubernetes/kubernetes | v1.37.1 | kubernetes/kubernetes | Agent Sandboxのgo.modが参照するk8s.io/api v0.37.1に対応するリリース | | kind | v0.33.0 | kubernetes-sigs/kind | 検証環境。ノードイメージはkindest/node:v1.37.0 | 本記事の実装・設定は OSS の v1.0.7 に固定します。GKE のマネージド機能は、OSS のコントローラーや API の動作とは分けて説明します。 TL;DR - Sandbox は単一 Pod の実行環境を管理します。Pod の作成・停止・再作成に、必要に応じて PVC と Service を組み合わせます。 - 新規 Sandbox の Service は明示的に有効化します。spec.service: true で、同名の headless Service を作ります。 - Suspended は Pod を削除し、PVC を保持します。プロセスメモリは保存されず、再開時は Pod の新規起動になります。 - WarmPool は事前起動、Claim は払い出しを担当します。取得された Sandbox は Claim の管理に移り、プールは不足分を補充します。 - 分離と通信制御には、ランタイムとネットワークの設定も必要です。CRD を導入しただけで、すべての Pod が gVisor で動くわけではありません。 第1章: Agent Sandboxが埋めようとしているギャップ この章では、Agent SandboxがDeploymentやStatefulSetではなく新しいCRDを作った理由を整理します。 Deployment は交換可能な Pod のレプリカを、StatefulSet は安定した識別子を持つ Pod の集合を管理します。Agent Sandbox は、単一の実行環境の払い出し・停止・期限管理をまとめて扱うための API です。README は、対象とする用途を次のように挙げています。 > Kubernetes excels at managing stateless, replicated applications (Deployments) and stable, numbered sets of stateful pods (StatefulSets). However, there's a growing need for an abstraction to handle use cases such as: > > * Development Environments: Isolated, persistent, network-accessible cloud environments for developers. > * AI Agent Runtimes: Isolated environments for executing untrusted, LLM-generated code. > * Reinforcement Learning (RL) & Evaluation: High-throughput, isolated execution sandboxes for training and evaluation loops (e.g., SWE-bench, R2E-Gym, Ray/RLlib) with low-latency warm-pool claims. > * Notebooks and Research Tools: Persistent, single-container sessions for tools like Jupyter Notebooks. > * Stateful Single-Pod Services: Hosting single-instance applications (e.g., build agents, small databases) needing a stable identity without StatefulSet overhead. (参考: README.md#L221-L227) これらは、個々の実行環境を識別し、必要に応じてデータを永続化したい用途です。永続ストレージは必須ではなく、設定に応じて利用します。StatefulSetのreplicas=1で代用することもできますが、READMEはこれを次のように評価しています。 > While these can be approximated by combining StatefulSets (size 1), Services, and PersistentVolumeClaims, this approach is cumbersome and lacks specialized lifecycle management like hibernation. (参考: README.md#L229) Sandbox CRDが提供する中心的な機能は、READMEで次の3点にまとめられています。 > * Stable Identity: Each Sandbox has a stable hostname and network identity. > * Persistent Storage: Sandboxes can be configured with persistent storage that survives restarts. > * Lifecycle Management: The Sandbox controller manages the lifecycle of the pod, including creation, scheduled deletion, pausing and resuming. (参考: README.md#L30-L32) 2章以降で、この3点が実際にどう実装されているかを1つずつ見ていきます。 第2章: 全体アーキテクチャ この章では、Agent Sandboxが提供するCRD群の全体像を整理します。 Agent Sandboxは、コアのSandbox CRDと、その上に乗るextensionsモジュール(SandboxTemplate/SandboxClaim/SandboxWarmPool)で構成されています。 > * SandboxTemplate: Provides a way to define reusable templates for creating Sandboxes, making it easier to manage large numbers of similar Sandboxes. > * SandboxClaim: Allows users to create Sandboxes from a SandboxWarmPool, abstracting away the details of the underlying Sandbox configuration. > * SandboxWarmPool: Manages a pool of pre-warmed Sandboxes that can be quickly allocated to users, reducing the time it takes to get a new Sandbox up and running. (参考: README.md#L38-L40) これらの関係は、READMEのアーキテクチャ図にまとめられています。 flowchart LR User[User] Claim[SandboxClaim] Template[SandboxTemplate] Sandbox[Sandbox] Pod[Pod] Runtime[Sandbox Runtime] WarmPool[SandboxWarmPool] subgraph Extensions[Extensions] Claim Template WarmPool end %% User paths User -->|creates| Sandbox User -->|creates| Claim %% Claim workflow WarmPool -->|references| Template Claim -->|adopts| Sandbox %% Pod handling Claim -->|adopts sandboxes from| WarmPool Sandbox -->|creates Pod| Pod %% Runtime Pod --> Runtime %% Warm pool WarmPool -->|pre-warms sandboxes| Sandbox (参考: README.md#L48-L85) 利用者は Sandbox を直接作ることも、SandboxClaim 経由でプール内の Sandbox を取得することもできます。実行中の Sandbox は原則として1個の Pod を管理し、停止中は Pod を持ちません。gVisor や Kata Containers による分離を使うには、対応するランタイムをノードに導入し、Pod テンプレートで runtimeClassName を指定します。 > Scope: Agent Sandbox is a sandbox orchestrator. It delegates low-level container isolation to secure "Sandbox Runtimes" (like gVisor or Kata Containers) by managing Pods configured to use these runtimes (via RuntimeClass). (参考: README.md#L19-L20) 第3章: Sandboxのライフサイクル この章では、Sandbox の operatingMode と期限設定が、実際にPodに対して何をしているかをソースコードから確認します。先に、3.1節(operatingMode)と3.2節(shutdownTime/shutdownPolicy)で見る内容をまとめた状態遷移図を示します。 stateDiagram-v2 [*] --> Running : Sandbox作成<br/>(operatingMode未指定時のデフォルト) Running --> Suspended : operatingMode = Suspended<br/>(Podを削除、PVCは保持) Suspended --> Running : operatingMode = Running<br/>(Podを再作成) Running --> Expired : shutdownTimeに到達<br/>(Pod/Serviceを削除) Suspended --> Expired : shutdownTimeに到達<br/>(Serviceを削除) Expired --> [*] : shutdownPolicy = Delete<br/>(Sandboxオブジェクトも削除) Running --> [*] : kubectlで明示的に削除 Suspended --> [*] : kubectlで明示的に削除 Expired --> [*] : kubectlで明示的に削除 note right of Expired shutdownPolicy = Retain(デフォルト)の場合、 Sandboxオブジェクトは Ready=False / SandboxExpired のまま残る end note (参考: 3.1節で引用したsandbox_types.go#L228-L248・sandbox_controller.go#L1328-L1357、3.2節で引用したsandbox_types.go#L328-L334・sandbox_types.go#L340-L345) 図の Expired は説明用の状態名で、operatingMode の値ではありません。期限に達すると、コントローラーが所有する Pod と Service の削除を要求します。shutdownPolicy: Retain では Sandbox が残り、Delete では Sandbox 自体も削除対象になります。削除は非同期で進むため、指定時刻にすべてのリソースが消えることを保証する仕組みではありません。 3.1 operatingMode: RunningとSuspended SandboxSpec.operatingMode は Running / Suspended の2値で、デフォルトは Running です。指定値は望ましい状態を表し、実際の準備完了は Ready Condition、停止の進捗は Suspended Condition で確認します。 // SandboxOperatingMode defines the desired operational state of the Sandbox. // +kubebuilder:validation:Enum=Running;Suspended type SandboxOperatingMode string const ( // SandboxOperatingModeRunning indicates the Sandbox should be actively running: // the controller ensures a backing Pod (and Service, if requested) is created and // kept running. ... SandboxOperatingModeRunning SandboxOperatingMode = "Running" // SandboxOperatingModeSuspended indicates the Sandbox should be suspended: the // controller terminates the backing Pod while retaining the Sandbox object and its // volumes. ... SandboxOperatingModeSuspended SandboxOperatingMode = "Suspended" ) (参考: sandbox_types.go#L228-L248) Suspended の処理では、管理対象の Pod を削除します。プロセスの checkpoint は取得しません。 if sandbox.Spec.OperatingMode == sandboxv1beta1.SandboxOperatingModeSuspended { if pod != nil { ... logger.Info("Deleting Pod because .Spec.OperatingMode is Suspended", ...) if err := r.Delete(ctx, pod); err != nil { return pod, fmt.Errorf("failed to delete pod: %w", err) } ... } ... } (参考: sandbox_controller.go#L1328-L1357) 通常の reconcile では、PVC の作成・維持を担う reconcilePVCs が停止中も呼ばれるため、volumeClaimTemplates から作った PVC は残ります。Service を有効にしている場合は Service も保持されますが、停止中は接続先の Pod がありません。8.2節で、実際にファイルを書き込んだ後にSuspend→Resumeして、ファイルが消えないことを確認します。 :::note warn READMEの「Desired Sandbox Characteristics」には、永続ストレージへの状態保存を伴う「Deep hibernation」や、ネットワーク接続をきっかけにした「Automatic resume」が将来像として挙げられています。 > * Deep hibernation: Saving state to persistent storage and potentially archiving the Sandbox object. > * Automatic resume: Resuming a sandbox on network connection. (参考: README.md#L236-L237) ただし、v1.0.7のソースコードを探した範囲では、これに対応する実装(プロセスのメモリ状態を含めたスナップショット保存や、接続をトリガーにした自動resume)は見つかりませんでした。Suspendedで実際に起きるのは、あくまで「Pod を削除して PVC を保持する」という軽量なサスペンドです。メモリ状態まで含めたcheckpoint/restoreによるSuspend/Resumeは、Agent Substrateが実装しています(詳細はAgent Substrateの概要の2.3節・3.3節を参照)。 この「Deep hibernation」に近い機能を、GKEはPod Snapshotsという名前のGKE専用機能として別途提供しています。 !GKEクラスタ内で、稼働中のPod 1からPod Snapshot Agent経由でGCS上にスナップショットを保存し(snapshot)、停止中のPod 2/3/nをそのスナップショットから復元する(restore)様子を示した図。 (参考: Agent Sandbox のご紹介: Kubernetes と GKE 上のエージェント AI 向けの強力なガードレール | Google Cloud 公式ブログ, 2025-11-19) > Pod Snapshots は、実行中の Pod の完全なチェックポイントと復元を可能にする、GKE 専用の新機能です。(中略)Pod Snapshots 使ってアイドル状態のサンドボックスのスナップショットを作成して一時停止できるため、エンドユーザーへの影響を最小限に抑えながらコンピューティング サイクルを大幅に節約できます。 (参考: Agent Sandbox のご紹介 | Google Cloud 公式ブログ) GKE の Pod Snapshots は、スナップショットの取得・復元の設定を別途行う機能です。OSS の operatingMode: Suspended を指定するだけで、メモリ状態まで自動的に保存されるわけではありません。利用には対応する GKE バージョン、gVisor、保存先ストレージなどの要件があります。 (参考: Save and restore Agent Sandbox on GKE environments with Pod snapshots | Google Cloud) ::: 3.2 shutdownTime/shutdownPolicy: 期限付きの自動削除 Sandbox では spec.shutdownTime に RFC3339 形式の絶対時刻を指定します。Go の Lifecycle はインラインで埋め込まれているため、Sandbox の YAML では spec.lifecycle.shutdownTime ではありません。SandboxClaim の spec.lifecycle とは構造が異なります。 > shutdownTime is the absolute time at which the Sandbox expires. When the current time reaches shutdownTime, the controller tears down the underlying resources (Pod and Service) and then applies shutdownPolicy to the Sandbox object itself. If unset, the Sandbox never expires and lives until it is explicitly deleted. (参考: sandbox_types.go#L285-L314、sandbox_types.go#L328-L334) たとえば、期限付きの Sandbox の設定部分は次のようになります。時刻は利用時の将来時刻に置き換えます。 spec: shutdownTime: "2026-12-31T23:59:59Z" shutdownPolicy: Retain # podTemplate などの設定は省略 shutdownPolicy(デフォルト Retain)は、期限切れ時に Sandbox 自体を削除対象にするかを制御します。 > * Retain (default): the Sandbox object is kept after its resources are torn down. Its live status fields are cleared and a Ready=False condition with reason SandboxExpired is set so the expiry is observable. > * Delete: the Sandbox object is deleted once its underlying resources are removed. (参考: sandbox_types.go#L340-L345) Retain の期限切れ処理では PVC を直接削除しません。一方、volumeClaimTemplates から作った PVC は Sandbox を owner reference に持つため、Sandbox 自体を削除するとガベージコレクションの対象になります。Suspend 中の保持と、Sandbox 削除後のデータ保持は別です。PV の扱いはストレージの reclaim policy にも依存します。 (参考: sandbox_controller.go#L1711-L1788、sandbox_controller.go#L1793-L1870) 期限管理は使い捨て環境の片付けに使えますが、コントローラーの停止や Pod の終了待ちもあるため、未信頼コードの実行時間を厳密に制限する仕組みとしては扱えません。 第4章: 安定したIDとネットワーク この章では、READMEが「Stable Identity」と呼んでいる機能の実体を確認します。 SandboxBlueprint.serviceは、*bool型でデフォルトは未設定です。コメントには、新規Sandboxでは明示的にtrueにしない限りServiceを作らないと明記されています。 > When unset, the controller preserves existing Services for backward compatibility but does not create new ones. Set to true to enable or false to explicitly disable and remove the Service. (参考: sandbox_types.go#L275-L280) service: trueを指定した場合に作られるのは、ClusterIP: Noneのheadless Serviceです。 service = &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: sandbox.Name, Namespace: sandbox.Namespace, ... }, Spec: corev1.ServiceSpec{ ClusterIP: "None", Selector: map[string]string{ sandboxLabel: nameHash, }, ... }, } (参考: sandbox_controller.go#L1061-L1090) Service 名は Sandbox 名と同じなので、<Sandbox名>.<namespace>.svc.<cluster-domain> という安定した FQDN が得られます。Pod 名も通常は Sandbox 名と同じですが、Pod の再作成時に UID や IP が維持されるわけではありません。また、headless Service は Pod のアドレスを返す仕組みなので、停止中や Ready な Pod がない間の接続性は保証されません。8.1節で、このservice: trueの有無による違いを実際に確認します。 第5章: SandboxWarmPoolとSandboxClaimで高速に払い出す この章では、拡張APIであるSandboxWarmPool/SandboxClaimが、あらかじめ起動しておいたSandboxをどう払い出しているかを見ていきます。 Google Cloudのブログでは、この払い出しの流れを次の図で説明しています。 !Agent OrchestratorのPodがExecutor(API/SDK)経由でinit()を呼ぶと、K8s API経由でAgent Sandbox ControllerがPoolからAllocateし、Endpoint/Execution Process(gVisor)/Ephemeral Storage/Network Policyを持つTask Execution(Agent Sandbox)が払い出される図。引き取られた分はReplenish Poolで補充される。 (参考: Agent Sandbox のご紹介: Kubernetes と GKE 上のエージェント AI 向けの強力なガードレール | Google Cloud 公式ブログ, 2025-11-19) 図中の「Allocate from Pool」が本章で扱うSandboxClaimによるプールからの引き取り、「Replenish Pool」が引き取られた分をSandboxWarmPoolが補充する動きに対応しています。ソースコードレベルでの実装は、以降の5.1〜5.2節で確認します。 5.1 SandboxWarmPool: 事前に並べておく数 SandboxWarmPoolSpec.replicas は、未割り当ての Sandbox をプール内に維持する目標数です。デフォルトは1で、実際に Ready な数とは異なります。status.replicas と status.readyReplicas を分けて確認します。 type SandboxWarmPoolSpec struct { // replicas is the desired number of sandboxes in the pool. // This field is controlled by an HPA if specified. // +kubebuilder:default=1 Replicas *int32 `json:"replicas,omitempty"` // sandboxTemplateRef - name of the SandboxTemplate to be used for creating a Sandbox // +required TemplateRef SandboxTemplateRef `json:"sandboxTemplateRef,omitempty"` ... } (参考: sandboxwarmpool_types.go#L40-L51) 5.2 SandboxClaim: プールから1つ引き取る SandboxClaimSpec.warmPoolRefで、どのWarmPoolから払い出すかを指定します。 type SandboxClaimSpec struct { // warmPoolRef targets the specific pre-warmed infrastructure pool to check out from. // +required WarmPoolRef SandboxWarmPoolRef `json:"warmPoolRef"` ... } (参考: sandboxclaim_types.go#L109-L112) SandboxClaimコントローラーは、プール内の候補Sandboxを見つけると、WarmPool専用のラベルを外してClaimをowner referenceに設定し直します。 delete(adopted.Labels, warmPoolSandboxLabel) ... adopted.Labels[v1beta1.SandboxLaunchTypeLabel] = v1beta1.SandboxLaunchTypeWarm ... adopted.OwnerReferences = nil if err := controllerutil.SetControllerReference(claim, adopted, r.Scheme); err != nil { ... } (参考: sandboxclaim_controller.go#L1311-L1403) 引き取られた分は、WarmPool コントローラーが新しい Sandbox を作って補充します。払い出した Sandbox を、利用後に別の Claim 用としてプールへ自動返却する仕組みではありません。Claim を削除すると、その Claim が所有する Sandbox もガベージコレクションの対象になります。 候補があれば Ready な Sandbox を優先しますが、準備中の候補を選ぶ場合もあり、プールが空ならテンプレートから新規作成する処理もあります。プールを設定しただけで、すべての Claim が待ち時間なしに利用できるわけではありません。 (参考: sandboxclaim_controller.go#L1180-L1227) 8.3節で、準備済みの Sandbox を取得し、不足分が補充される流れを確認します。 第6章: Sandbox内部の実行基盤: sandboxdとsandbox-router この章では、Pod 内に配置できる実行用デーモン sandboxd と、オプションの sandbox-router の役割を整理します。Sandbox CRD を作ると、これらが必ず自動で注入されるという意味ではありません。8章の nginx 例では、どちらも使いません。 6.1 sandboxd: gRPCとRESTのハイブリッドAPI sandboxdは、プロセス実行をgRPC、ファイル操作・ヘルスチェックをRESTという、2つのプロトコルを使い分けるハイブリッドなAPIを提供します。 > ```text > sandboxd (runtime daemon) > ├── gRPC :9090 → ProcessService (streaming process I/O) > └── HTTP :8080 → FilesystemService (stateless file operations & runtime probes) > ``` > > - Process execution is served over gRPC because Start is a long-lived server-streaming RPC: stdout/stderr flow continuously until the process exits. > - Filesystem transfers are served over REST so file bytes move as raw application/octet-stream payloads with no protobuf wrapping, and any plain HTTP client works without generated stubs. (参考: packages/sandboxd/USER_GUIDE.md#L6-L17) プロセス実行とファイル操作を、異なる API で提供する構成です。プロセス実行にはストリーミングの Start に加え、結果をまとめて返す Execute もあります。コマンドは sandboxd が動くコンテナ内で実行されます。別コンテナとボリュームを共有しても、そのコンテナのバイナリやプロセスを直接操作できるわけではありません。 (参考: USER_GUIDE.md#L28-L32、USER_GUIDE.md#L99-L108) 6.2 sandbox-router: データプレーンだけを担うリバースプロキシ sandbox-router は、既存の Sandbox Pod に HTTP トラフィックを転送します。宛先が停止中でも Sandbox の作成や Resume は行いません。 > Agent Sandbox splits creation from routing. The router only handles the data plane (参考: sandbox-router/README.md#L7-L19) ルーティングは X-Sandbox-ID などの HTTP ヘッダーに基づいて行われます。名前と namespace から Service の DNS 名を組み立てる方式に加え、Pod IP の指定や、--cache-enabled=true による Sandbox 情報のキャッシュ参照もあります。README 冒頭の「Sandbox を参照しない」という説明は、キャッシュを使う実装には当てはまりません。 | Header | Required | Default | |---|---|---| | X-Sandbox-ID | yes | — | | X-Sandbox-UID | no | — | | X-Sandbox-Namespace | no | default | | X-Sandbox-Port | no | 8888 | | X-Sandbox-Pod-IP | no | — | (参考: sandbox-router/README.md#L29-L35) :::note warn sandbox-routerは、6.1節で見たsandboxdのgRPC(ProcessService)を中継できません。 > The current sandbox-router accepts HTTP/2 client connections, but disables HTTP/2 on its upstream connections and therefore cannot carry the gRPC ProcessService, so it is not a complete sandboxd transport. (参考: packages/sandboxd/USER_GUIDE.md#L20-L23) sandboxd のプロセス実行を SDK から使う場合は、Pod へのポートフォワードや Service など、gRPC を転送できる接続方法が必要です。ルーターのデフォルト宛先ポート 8888 も、sandboxd の 9090 / 8080 とは異なります。 ::: 第7章: セキュリティモデル この章では、docs/security/threat_model.mdが整理している信頼境界を見ます。 脅威モデルは、コントローラー・Sandbox Pod・Sandbox Router・kube-apiserver を主要コンポーネントとして整理しています。下の図はシステム側とテナント側の境界を示し、本文では利用者と API、テナント間、ワークロードとノードなどの信頼境界も挙げています。 flowchart TD User[User / Client] Gateway[Ingress / Gateway] Router[Sandbox Router] K8sAPI[Kubernetes API Server] Controller[Sandbox Controller] PodA["Sandbox Pod A (Tenant A)"] PodB["Sandbox Pod B (Tenant B - Malicious)"] User -->|API Requests| K8sAPI User -->|Data Plane Traffic| Gateway Gateway -->|Forward| Router Router -->|Proxy| PodA Router -->|Proxy| PodB Controller -->|Watch / Reconcile| K8sAPI Controller -->|Creates| PodA Controller -->|Creates| PodB subgraph SystemControl["Trust Boundary: System Control Plane"] Controller Router end subgraph TenantIsolation["Trust Boundary: Tenant Isolation"] subgraph TenantA["Tenant A"] PodA end subgraph TenantB["Tenant B"] PodB end end (参考: docs/security/threat_model.md#L12-L41) > - Sandbox Controller (Core & Extensions): Runs in the cluster (typically in a system namespace like agent-sandbox-system) with privileges to manage Pods, Services, PVCs, and Sandbox CRDs. > - Sandbox Pods (Workloads): Ephemeral Pods created by the controller to run user workloads (e.g., AI agents). These Pods often run untrusted, LLM-generated code. > - Sandbox Router (Optional): A reverse proxy that routes external traffic to the correct Sandbox Pod based on HTTP headers (e.g., X-Sandbox-ID). (参考: docs/security/threat_model.md#L7-L10) 7.1 OSS で設定する分離とアクセス制御 図の境界は設計上の分離を表しており、CRD の導入だけで通信や権限がすべて制限されるわけではありません。設定する対象を分けて確認します。 | 対象 | v1.0.7 で確認すること | |---|---| | 隔離ランタイム | gVisor / Kata Containers の導入と runtimeClassName。通常のコンテナ実行と区別する | | Kubernetes API | Sandbox / Claim / Template を操作する利用者の RBAC と、ワークロードの ServiceAccount 権限 | | ネットワーク | SandboxTemplate はデフォルトの Managed モードで共有 NetworkPolicy を管理する。実際の通信制御には対応する CNI が必要。直接作る Sandbox に同じポリシーが自動適用されるわけではない | | ServiceAccount トークン | 拡張コントローラーは未指定の automountServiceAccountToken を false にする。直接作る Sandbox では明示的な設定を確認する | | sandboxd | デフォルトは 0.0.0.0 で待ち受ける。--auth-token-sha256 または --auth-token-file で Bearer トークン認証を有効化でき、どちらも未設定なら認証しない | | sandbox-router | デフォルトの認可は allow-all。ヘッダーの宛先指定自体は利用権限の証明にならないため、公開方法に応じた認証・認可を設定する | (参考: sandboxtemplate_types.go#L102-L125、utils.go#L20-L34、sandboxd User Guide、Sandbox Router) 7.2 GKE のマネージド機能との違い GKE には、Agent Sandbox をクラスタの機能として有効化する手順もあります。2026年10月20日時点の公式手順では、既存クラスタは v1.36.3-gke.1767000 以降が対象です。Standard クラスタでは gVisor 対応のノードプールも必要です。 gcloud beta container clusters update "${CLUSTER_NAME}" \ --location="${LOCATION}" \ --enable-agent-sandbox 有効化に加え、ワークロード側にも runtimeClassName: gvisor、非 root 実行、ServiceAccount トークンの自動マウント無効化、CPU・メモリ limit などの要件があります。8章の OSS / kind 用 nginx 例をそのまま適用する手順とは異なります。また、3.1節の Pod Snapshots には別途設定と要件があります。 (参考: Enable Agent Sandbox on GKE | Google Cloud) 第8章: kindクラスタで実際に動かす この章では、kind での既存の検証記録に、再現に必要な YAML と待機コマンドを補って整理します。通常の nginx コンテナでリソースの生成と PVC の保持を確認するもので、gVisor / Kata Containers の分離、sandboxd、GKE の Pod Snapshots は検証対象に含めていません。 8.1 インストールと、安定したネットワークIDの有無 Docker・kind・kubectl を用意し、専用の検証クラスタを作ります。agent-sandbox-demo は既存クラスタと重複しない名前にしてください。 kind create cluster --name agent-sandbox-demo --image kindest/node:v1.37.0 kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/download/v1.0.7/sandbox-with-extensions.yaml kubectl wait deploy/agent-sandbox-controller -n agent-sandbox-system --for=condition=available --timeout=120s (参考: README.md#L93-L100) READMEの「Getting Started」通りにspec.serviceを指定せずSandboxを作ると、Podは作られますが、Serviceは作られませんでした。 $ kubectl apply -f - <<'EOF' apiVersion: agents.x-k8s.io/v1beta1 kind: Sandbox metadata: name: my-sandbox spec: podTemplate: spec: containers: - name: my-container image: nginx:1.27 EOF $ kubectl get sandbox,pods,svc NAME READY REASON sandbox.agents.x-k8s.io/my-sandbox True DependenciesReady NAME READY STATUS pod/my-sandbox 1/1 Running NAME TYPE CLUSTER-IP service/kubernetes ClusterIP 10.96.0.1 上の Sandbox が Ready になるまで待ちます。 kubectl wait sandbox/my-sandbox --for=condition=Ready --timeout=120s 次に、別名の Sandbox を service: true 付きで作ります。以下を sandbox-service.yaml として保存して適用します。 apiVersion: agents.x-k8s.io/v1beta1 kind: Sandbox metadata: name: my-sandbox-svc spec: service: true podTemplate: spec: automountServiceAccountToken: false containers: - name: my-container image: nginx:1.27 kubectl apply -f sandbox-service.yaml kubectl wait sandbox/my-sandbox-svc --for=condition=Ready --timeout=120s 既存の検証では、4章で見た通り headless Service が作られ、status.serviceFQDN に FQDN が入りました。以下の出力は抜粋です。 $ kubectl get sandbox my-sandbox-svc -o jsonpath='{.status.serviceFQDN}' my-sandbox-svc.default.svc.cluster.local $ kubectl get svc my-sandbox-svc NAME TYPE CLUSTER-IP PORT(S) my-sandbox-svc ClusterIP None <none> 8.2 Suspend/ResumeでのPVC永続化 PVC 上のファイルが Suspend / Resume 後も残ることを確認します。動的プロビジョニングに対応する StorageClass が必要です。以下は kind のデフォルトの standard を使う例で、最初に存在を確認します。 kubectl get storageclass standard 以下を sandbox-pvc.yaml として保存します。data の PVC はコントローラーが Pod のボリュームへ追加するため、volumeMounts に同名を指定します。 apiVersion: agents.x-k8s.io/v1beta1 kind: Sandbox metadata: name: my-sandbox-pvc spec: volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] storageClassName: standard resources: requests: storage: 1Gi podTemplate: spec: automountServiceAccountToken: false containers: - name: my-container image: nginx:1.27 volumeMounts: - name: data mountPath: /data kubectl apply -f sandbox-pvc.yaml kubectl wait sandbox/my-sandbox-pvc --for=condition=Ready --timeout=120s 以下は既存の検証記録です。Suspend の指定後は非同期に Pod が終了します。 $ kubectl exec my-sandbox-pvc -- sh -c 'echo "hello from before suspend" > /data/note.txt' $ kubectl patch sandbox my-sandbox-pvc --type=merge -p '{"spec":{"operatingMode":"Suspended"}}' $ kubectl wait sandbox/my-sandbox-pvc --for=condition=Suspended --timeout=120s $ kubectl get pods NAME READY STATUS my-sandbox 1/1 Running my-sandbox-svc 1/1 Running # my-sandbox-pvc の Pod は消えている $ kubectl get pvc NAME STATUS CAPACITY data-my-sandbox-pvc Bound 1Gi # PVCはBoundのまま残っている operatingMode: Runningに戻すと、Podが再作成され、Suspend前に書き込んだファイルがそのまま読み出せました。 $ kubectl patch sandbox my-sandbox-pvc --type=merge -p '{"spec":{"operatingMode":"Running"}}' $ kubectl wait sandbox/my-sandbox-pvc --for=condition=Ready --timeout=120s $ kubectl exec my-sandbox-pvc -- cat /data/note.txt hello from before suspend 3.1節で確認した通り、ここで起きているのは「Podの削除・再作成」だけで、プロセスのメモリ状態などは保存されません。今回のnginxコンテナはファイルシステムに書き込んだデータだけを扱っているため、PVCさえ残っていれば復元できましたが、プロセス内部の状態を引き継ぎたい場合はアプリケーション側でファイルに書き出しておく必要があります。 8.3 SandboxWarmPoolからSandboxClaimが1つ引き取る WarmPool / Claim の関係を確認するため、次の3ファイルを用意します。テンプレートの nginx は API の動作確認用で、sandboxd の API は提供しません。 sandboxtemplate.yaml: apiVersion: extensions.agents.x-k8s.io/v1beta1 kind: SandboxTemplate metadata: name: demo-template spec: podTemplate: spec: automountServiceAccountToken: false containers: - name: nginx image: nginx:1.27 sandboxwarmpool.yaml: apiVersion: extensions.agents.x-k8s.io/v1beta1 kind: SandboxWarmPool metadata: name: demo-pool spec: replicas: 2 sandboxTemplateRef: name: demo-template claim.yaml: apiVersion: extensions.agents.x-k8s.io/v1beta1 kind: SandboxClaim metadata: name: my-claim spec: warmPoolRef: name: demo-pool 3つとも同じ namespace に作成します。まずテンプレートとプールを適用し、status.readyReplicas が2になるまで確認します。 kubectl apply -f sandboxtemplate.yaml -f sandboxwarmpool.yaml kubectl get sandboxwarmpool demo-pool -w 確認後は Ctrl-C で監視を終了します。以下は既存の検証記録の抜粋で、Sandbox 名の接尾辞は実行ごとに変わります。 $ kubectl apply -f sandboxtemplate.yaml -f sandboxwarmpool.yaml $ kubectl get sandbox -l agents.x-k8s.io/warm-pool-sandbox NAME READY demo-pool-6zcpf True demo-pool-8pzkl True ここへSandboxClaimを作ると、2個のうち1個(demo-pool-8pzkl)が引き取られ、WarmPool用のラベルが外れてowner referenceがSandboxClaimに変わりました。 $ kubectl apply -f claim.yaml $ kubectl wait sandboxclaim/my-claim --for=condition=Ready --timeout=120s $ kubectl get sandboxclaim my-claim -o jsonpath='{.status.sandbox.name}' demo-pool-8pzkl $ kubectl get sandbox demo-pool-8pzkl -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}' SandboxClaim/my-claim 同時に、プールの数を維持するため、新しいSandbox(demo-pool-fnfs2)が自動で補充されていました。 $ kubectl get sandbox -l agents.x-k8s.io/warm-pool-sandbox NAME READY demo-pool-6zcpf True demo-pool-fnfs2 True # demo-pool-8pzkl はClaimに引き取られたのでこの一覧から外れている 再現時は、固定の Sandbox 名を使わず、Claim の status から取得できます。 CLAIMED_SANDBOX=$(kubectl get sandboxclaim my-claim -o jsonpath='{.status.sandbox.name}') kubectl get sandbox "${CLAIMED_SANDBOX}" \ -o 'jsonpath={.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}' ここでは、準備済みの Pod の取得とプールの補充を確認しています。起動時間の短縮幅は測定していません。プールが空の場合などは新規作成になるため、常に同じ待ち時間になるわけではありません。 検証が終わったら、作成したクラスタを削除します。この手順では PVC 上の検証データも失われます。 kind delete cluster --name agent-sandbox-demo おわりに Agent Sandbox は、単一 Pod の実行環境に、停止・再開やプールからの払い出しを組み合わせる API として整理できました。Agent Substrate とは停止・再開という言葉が共通する一方、保存する状態や実行単位に違いがあり、使い分けを考えるうえで重要だと感じました。 GKE にマネージドの導入手順が用意されている点も印象に残りました。OSS の機能と GKE 固有の機能を分けながら、引き続き確認していきたいと思います。 参考 参考リンク 本文で登場する順に並べています。 - LeaderWorkerSet と DisaggregatedSet の概要 - kubernetes-sigs/agent-sandbox - Bringing you Agent Sandbox on GKE and Agent Substrate | Google Cloud Blog - Agent Substrate の概要 - Agent Sandbox のご紹介: Kubernetes と GKE 上のエージェント AI 向けの強力なガードレール | Google Cloud 公式ブログ - Save and restore Agent Sandbox on GKE environments with Pod snapshots | Google Cloud - sandboxd User Guide | agent-sandbox - Sandbox Router (Go) | agent-sandbox - Threat Model | agent-sandbox - Enable Agent Sandbox on GKE | Google Cloud
agent-sandbox: easy management of isolated, stateful, singleton workloads on Kubernetes (4.3k stars) What it does All text below is quoted directly from the project README. >agent-sandbox enables easy management of isolated, stateful, singleton workloads, ideal for use cases like AI agent runtimes and reinforcement learning (RL). Key features of the Sandbox CRD include: - Stable Identity: Each Sandbox has a stable hostname and network identity. - Persistent Storage: Sandboxes can be configured with persistent storage that survives restarts. - Lifecycle Management: The Sandbox controller manages the lifecycle of the pod, including creation, scheduled deletion, pausing and resuming. Extensions: - SandboxTemplate: Provides a way to define reusable templates for creating Sandboxes, making it easier to manage large numbers of similar Sandboxes. - SandboxClaim: Allows users to create Sandboxes from a SandboxWarmPool, abstracting away the details of the underlying Sandbox configuration. - SandboxWarmPool: Manages a pool of pre-warmed Sandboxes that can be quickly allocated to users, reducing the time it takes to get a new Sandbox up and running. >This project is developing a Sandbox Custom Resource Definition (CRD) and controller for Kubernetes, under the umbrella of SIG Apps. The goal is to provide a declarative, standardized API for managing workloads that require the characteristics of a long-running, stateful, singleton container with a stable identity, much like a lightweight, single-container VM experience built on Kubernetes primitives. >kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/sandbox-with-extensions.yaml Repository kubernetes-sigs/agent-sandbox Repository stats |Metric|Value| |:-|:-| |Stars|4,296| |Forks|568| |Last release|v1.0.7 (2026-10-15)| |Languages|Go 51.5%, Python 32.7%, TypeScript 10.4%| |Contributors|168| |AI contributor accounts (Claude / Codex / Cursor / Copilot)|None| |AI co-authored commits|None| Main contributor |Field|Value| |:-|:-| |Name|Janet Kuo| |GitHub|@janetkuo| |Commits to this repo|109 (top contributor)| |Bio|@kubernetes Steering Committee, SIG Apps chair TL, Kubernetes AI Conformance lead| |Company / Location|Google / Mountain View, CA| |X / Twitter|@janet_kuo| |Followers|1,100|
How AI Agents Sharing a Sandbox Leak Each Other's Data Originally published at webofmike.com on 2026-10-13. The demo repo and every command in it were run before publishing. On 2026-10-10, Cybernews reported that Shane Mac, CEO of XMTP Labs, gave a Grok Bot agent read-only access to his personal bank account as a finance assistant. A separate Grok Bot agent, whose job was posting updates to his company Slack, posted his balances there instead: a "monthly financial audit" listing gym memberships, a home renovation, and card payments, posing as him (HN thread, 62 points). Mac's own explanation was specific: the two agents shared one cloud compute instance, so the Slack agent could reach data it was never given. That's a one-sentence root cause, and it's worth taking at face value: "different task" is an instruction, not a boundary. I reproduced the mechanism generically, no Grok Bot involved, in two Docker Compose files that differ in exactly one line. Repo: themsquared/shared-sandbox-agent-leak. Disclosure: I work at Solo.io, which created kagent, one of the projects mentioned below. This post is my own work, and the repro uses plain Docker with no Solo software. The setup Two toy agents, neither one LLM-driven, because the point is the compute boundary, not agent reasoning: - finance-agent writes a balance snapshot to /secrets/finance.json. That's its entire job. - slack-agent posts messages to a webhook. It was never given a finance scope, and its code never references finance data. - mock-slack is a 20-line http.server standing in for a real Slack incoming webhook, so you can see exactly what would land in #general. slack_agent.py does one thing: check whether a file exists at a path, and if it does, post it. def main(): time.sleep(2) try: with open(SECRET_PATH) as f: leaked = json.load(f) print(f"[slack-agent] found a file at {SECRET_PATH} I was never given, posting it, because that's my job") post_to_slack({"channel": "#general", "text": "monthly financial audit", "attachment": leaked}) print("[slack-agent] LEAKED: posted finance data to Slack") except FileNotFoundError: print(f"[slack-agent] no file at {SECRET_PATH}, nothing to post, nothing leaked") There's no finance-specific logic to remove, no prompt to rewrite, no permission check that was skipped. Whether this agent leaks anything depends entirely on what's mounted at /secrets when it starts, which is decided outside the agent, by whoever wired up its compute. Reproducing the leak docker-compose.vulnerable.yml mounts the same named volume into both agents at /secrets: services: finance-agent: volumes: - shared-secrets:/secrets command: ["python", "/app/finance_agent.py"] slack-agent: volumes: - shared-secrets:/secrets command: ["python", "/app/slack_agent.py"] volumes: shared-secrets: Both containers mount shared-secrets. Run it: docker compose -f docker-compose.vulnerable.yml up --abort-on-container-exit --exit-code-from slack-agent mock-slack-1 | [mock-slack] #general received: {"channel": "#general", "text": "monthly financial audit", "attachment": {"checking_balance_usd": 4812.33, "line_items": ["gym membership -45.00", "home renovation -1200.00", "card payment -389.12"] }} slack-agent-1 | [slack-agent] found a file at /secrets/finance.json I was never given, posting it, because that's my job slack-agent-1 | [slack-agent] LEAKED: posted finance data to Slack slack-agent did exactly what it was built to do: look for something to post, find it, post it. The leak isn't a bug in either agent's code. It's the absence of a boundary between them. The fix is one line, at the compute layer docker-compose.isolated.yml changes a single thing: slack-agent gets its own, separate, empty volume at the same path, instead of finance-agent's volume. services: finance-agent: volumes: - finance-secrets:/secrets slack-agent: volumes: - slack-agent-own-sandbox:/secrets # was: finance-secrets volumes: finance-secrets: slack-agent-own-sandbox: Same two agents. Same two Python files, byte for byte. Same two tasks. Run it: docker compose -f docker-compose.isolated.yml up --abort-on-container-exit --exit-code-from slack-agent slack-agent-1 | [slack-agent] no file at /secrets/finance.json, nothing to post, nothing leaked slack-agent looks in exactly the same place and finds nothing, because there's nothing there to find. Neither agent's code changed. Neither agent's prompt or task description changed. Neither agent is aware the other exists, before or after the fix. The entire difference is which volume got attached to which container, decided once, outside of either agent, before anything ran. ./run.sh in the repo runs both scenarios back to back if you want the side-by-side output yourself; the whole thing takes under a minute on a laptop with Docker already running. Why this isn't just a Docker-volume toy problem The shared-sandbox failure mode is common enough that the Kubernetes agent ecosystem is actively standardizing the fix, because it doesn't live in the agent's prompt or code; it lives in whatever schedules the agent's compute: - kubernetes-sigs/agent-sandbox shipped v1.0.6 on 2026-10-08. It defines a Sandbox custom resource specifically so that "run this agent" and "give it its own isolated environment" are the same Kubernetes API call, not two things a platform operator has to remember to wire together separately. - kagent shipped gVisor-backed microVM sandboxes in v1.0.0-alpha4 on 2026-09-25, making per-agent compute isolation a platform default rather than something each deployment has to opt into. Neither of those is exercised in the repo; the repo is deliberately the smallest possible reproduction, runnable by anyone with Docker and no cluster. If you're already running agents on Kubernetes, that's where the equivalent boundary belongs, and I wrote about a related failure, an agent escaping its sandbox's egress controls entirely over DNS, in Your Agent Egress Proxy Never Saw the DNS Query. That post and this one are two different boundaries around the same box: what an agent can reach over the network, versus what it can see sitting still on its own filesystem. Worth being precise about what this repo does not claim: it doesn't model credential injection, token scoping, or an egress gateway deciding what an agent is allowed to call out to. That's a different, complementary boundary (what an agent can reach) from the one here (what an agent can see on its own filesystem). Conflating the two is exactly how a team ends up with a tightly scoped API token and a wide-open shared disk, which is closer to what actually happened in the reported incident than a token leak would have been. Honest limits - Both agents here are a dozen lines of Python each, not LLM-driven. The compute boundary holds the same way with a real agent framework in place, because Docker enforces it, not either agent's code, but I didn't wire up a live model to prove that specific claim in this repo. - mock-slack is a stand-in http.server, not real Slack. The receiving side doesn't matter to the demo; what matters is whether the read succeeds. - Only one failure mode is modeled: a shared named volume. A shared environment variable, a shared process namespace, or a shared cloud VM's local disk outside Docker entirely would leak the same way for the same reason. I didn't build separate repros for each, though the mechanism is identical in all of them. Try it git clone https://github.com/themsquared/shared-sandbox-agent-leak.git cd shared-sandbox-agent-leak ./run.sh Two agents, two jobs, one shared filesystem: one of them posts the other's secrets to Slack. Give them separate sandboxes instead, change nothing else, and the same agent that leaked finds nothing to leak. If you're scheduling agents for a living, that's the whole post: the isolation has to be an infrastructure decision, because it is never going to be a prompt. Frequently asked questions Why did a Grok Bot AI agent leak bank account data into Slack? Per Cybernews' 2026-10-10 report, XMTP Labs CEO Shane Mac gave a Grok Bot agent read-only access to his personal bank account. A separate Grok Bot agent, configured only to post updates to his company Slack, posted his balances there instead. Mac's own explanation: the two agents shared one cloud compute instance, so the Slack agent could reach data it was never scoped to see. Does giving two AI agents different tasks keep their data separate? No. A task description is a prompt-level instruction, not a compute boundary. If two agents share a filesystem, environment, or process namespace, either one can read whatever the other wrote there, regardless of what either agent was told to do. Isolation has to be enforced below the prompt, at the compute layer. How do you isolate AI agents running on the same host or cluster? Give each agent its own sandbox: a separate container, volume, or microVM, so there is no shared filesystem, environment variable set, or process namespace between agents. kubernetes-sigs/agent-sandbox defines a Sandbox CRD for this on Kubernetes, and kagent ships gVisor-backed microVM sandboxes as a platform default in its v1.0.0-alpha series. What's the difference between agent sandboxing and agent credential scoping? Sandboxing controls what an agent can see on its own machine: its filesystem, its environment, and whether another agent's data is reachable at all. Credential scoping and egress gateways control what an agent can reach over the network once it's running. They're complementary boundaries that solve different problems; this post is about the first one. --- *Canonical version, with machine-readable markdown at https://webofmike.com/shared-sandbox-agent-leak/index.md: webofmike.com
Adds a docker-sbx example that runs Docker Sandboxes (sbx) in a Sandbox on a KVM node. The pod hosts the sbx daemon and each agent runs in its own microVM. It follows the KVM-in-a-pod pattern of windows-sandbox. agent-sandbox#1831 t.co
I'm honestly getting lost between what primary usecase/workflow each of these is intended for: 1. kagent.dev 2. kubernetes-sigs/agent-sandbox 3. docker/docker-agent (this project) 4. docs.langchain.com 5. cloudflare.com