Repository navigation
Warm-pooled sandboxes for the Kubernetes compute driver #1879
Description
Problem Statement
Creating a Kubernetes sandbox is a cold start: the gateway creates a Sandbox CR, the agent-sandbox controller schedules a Pod, the image is pulled (or read from cache), the supervisor boots, and only then is the sandbox Ready. Measured locally this is ~4s+ even with the image preloaded. For interactive agent workloads and high-churn "fresh sandbox per task" usage, that latency dominates time-to-first-action.
We want near-instant Kubernetes sandbox provisioning via a warm pool of pre-provisioned, ready Pods.
This issue scopes the design (moved here from PR #1813 per the issue-first RFC process). It supersedes the draft RFC and incorporates the security review on that PR.
Proposed Design
Adopt the upstream agent-sandbox warm-pool extension CRDs — SandboxTemplate, SandboxWarmPool, SandboxClaim (extensions.agents.x-k8s.io/v1alpha1) — already shipped in the v0.4.6 release OpenShell pins for the core Sandbox CRD. The gateway pre-declares operator-owned warm pools; on CreateSandbox, when the requested shape matches a pool, the Kubernetes driver creates a SandboxClaim that binds a pre-warmed Pod in ~0.1s. Non-matching requests fall back to the existing cold path.
Identity re-anchor (security-critical)
Today validate_sandbox_owner_reference() (crates/openshell-server/src/auth/k8s_sa.rs) cross-checks the owning Sandbox CR label openshell.ai/sandbox-id against the Pod annotation openshell.io/sandbox-id. Warm Sandboxes are created generically by the pool controller and carry agents.x-k8s.io/claim-uid + a controlling SandboxClaim ownerReference instead, so identity must re-anchor to the gateway-created SandboxClaim. The IssueSandboxToken warm path must enforce a strict fail-closed chain, with any mismatch rejecting:
- TokenReview audience/SA/pod-name/pod-UID match the live Pod.
- Pod has exactly one controlling
SandboxownerReference (matching UID). - That
Sandboxhas exactly one controllingSandboxClaimownerReference (matching name + UID);agents.x-k8s.io/claim-uidagrees. - The live
SandboxClaimexists, has the expected UID, is bound, andstatus.sandbox.nameequals the owningSandbox. - The gateway Store has a durable, gateway-created record for
(namespace, claim name, claim UID)containing the expected sandbox ID, and it equals the Pod'sopenshell.io/sandbox-idannotation.
The mapping must live in the shared gateway Store (HA-safe; any replica may handle bootstrap), and claim creation must be ordered before a Pod can bootstrap. Users must not be able to set reserved metadata (openshell.io/*, agents.x-k8s.io/*, identity/SPIFFE keys, SandboxClaim.spec.additionalPodMetadata, spec.env).
Workspace isolation: separate the private workspace from shared data
The data-isolation concern is about the writable, per-agent workspace — not the large files sandboxes legitimately read. Conflating the two is the trap. The design uses two volumes with opposite requirements:
1. Writable per-agent workspace (/sandbox) — agent-private state (code, credentials, gateway JWTs, intermediate files). Must never be visible to another agent. Isolation options:
- Ephemeral (e.g.
emptyDir) when reschedule-survival is not required — fail-safe: the kubelet deletes it when the Pod is gone, so there is no object to orphan and no dependence on cleanup being correct. - Per-Sandbox PVC, single-use, destroyed on teardown when the workspace must survive pod rescheduling for that agent — this requires reliable destruction on every teardown path and a
reclaimPolicythat actually wipes backing storage (Delete, notRetain). - Either way: a warm Pod is seeded pristine from the image and never runs user code while pooled; a claimed
Sandbox/Pod/(PVC) is single-use and never returned to the pool.
2. Shared large-file volume (datasets, models, caches) — mounted read-only into every warm + claimed sandbox via the SandboxTemplate, so big files are available on the filesystem without streaming them through the relay. Sharing here is intended and safe because it is read-only and holds no per-agent secrets — there is nothing private to leak. It is long-lived and untouched by pooling/claiming (not per-agent state), and being pre-attached, a claimed sandbox reads it instantly.
- Read-sharing across Pods on different nodes requires an access mode / provisioner that supports it (
ReadOnlyMany/ReadWriteMany: NFS, CephFS, EFS, Filestore, or a FUSE/object-store CSI). The k3dlocal-pathprovisioner (RWO, single-node) does not support multi-reader sharing.
Empirical basis (see Agent Investigation): each warm Sandbox already gets its own PVC and the pool replenishes with fresh Sandboxes (claimed ones are not recycled). The remaining risk is purely lifecycle — under the default shutdownPolicy: Retain, deleting a claim deletes the Sandbox/Pod but leaves the workspace PVC orphaned with user data (a written marker survived). That is closed by the single-use + explicit-destruction rule above, or avoided entirely by an ephemeral workspace.
Cross-agent writable sharing (e.g. agent A's output reused by agent B as a build/model cache) is a separate, deliberate decision — not the private workspace — and must be scoped so credentials/secrets never land in it.
Single-use still preserves the latency win: a warm Pod is pre-scheduled, image-pulled, supervisor-booted, and workspace-prepared before claim (~4s → ~0.1s measured).
Open decision for this issue: which workspace model is the default for pooled sandboxes — ephemeral, or per-Sandbox PVC destroyed-on-teardown — noting that large shared files live on the separate read-only volume either way.
Scope guardrails
Initially, only operator-declared pools using trusted templates/images are warm-pooled; user-supplied images or arbitrary per-request templates fall back to the cold path until per-tenant pool isolation and cleanup guarantees are designed.
What bakes vs. late-binds
- Baked into the shared
SandboxTemplate: image, mTLS mount, projected SA-token volume, supervisor sideload, capabilities, host aliases, runtimeClass, resources, and any read-only shared data volume (large files / datasets). - Per-claim, isolated: the writable per-agent workspace — ephemeral, or a per-Sandbox PVC destroyed on teardown — never the shared data volume.
- Injected per-claim (annotation only):
openshell.io/sandbox-id(per-claimenvis rejected on the warm path). - Late-bound over the supervisor relay (already works): policy, providers. Identity is established by the existing token exchange, not Pod env.
Phased plan
- Settle this design (this issue).
- Driver warm path (flagged): create
SandboxClaiminstead ofSandboxfor pooled shapes; gateway RBAC forextensions.agents.x-k8s.io; durable Store claim mapping. Installextensions.yamlin dev/e2e alongside this consumer (not before). - Auth re-anchor: implement the fail-closed chain in
k8s_sa.rs+ adversarial tests. - Single-use lifecycle + volume model: isolate the writable workspace (ephemeral or per-Sandbox PVC destroyed on teardown); mount shared large-file data read-only; workspace-isolation e2e (claim → write secret to the writable workspace → delete → re-claim → assert clean + workspace not reused, and assert the shared volume is read-only and carries no per-agent data).
- Pool management + surface/docs.
Alternatives Considered
- Patch identity onto the claimed Pod after bind (keep the label cross-check): requires granting the gateway
patch pods(deliberately denied for immutability) and is racy. - Bare-Pod warm pools (if upstream pools created Pods, not
SandboxCRs — see upstream issue refactor(build): unify image build graph for cache reuse #390): would break the ownerReference auth chain. v0.4.6 createsSandboxCRs. - Do nothing: accept cold-start latency. Viable for low-churn usage, poor for interactive agents.
Agent Investigation
Validated on a local k3s (k3d) cluster with agent-sandbox v0.4.6 (core + extensions):
- Identity: claim binds in ~0.13s; the warm Pod is owned by a controlling
SandboxCR (ownerRef chain intact); the claim-injectedopenshell.io/sandbox-idannotation lands on the Pod; per-claimenvis rejected on the warm path. The boundSandboxcarriesagents.x-k8s.io/claim-uid+ a controllingSandboxClaimownerRef. The currentvalidate_sandbox_owner_reference()fails closed against warm Sandboxes (they lackopenshell.ai/sandbox-id), so there is no exploit in the install-only PR. - Workspace:
SandboxTemplate.volumeClaimTemplates→ each warmSandboxgets its ownBoundPVC (2 warm pods → 2 distinct PVCs, each owned by itsSandbox). Claiming replenished the pool with a newSandbox+ new PVC (claimed one not recycled). Deleting the claim deleted theSandboxbut left the workspace PVCBoundholding a writtenTENANT-A-SECRETmarker —shutdownPolicydefault isRetain, confirming the orphaned-workspace data risk. - Baseline: the cold path is unaffected —
sandbox create→Ready,IssueSandboxToken→ minted JWT,echoexecuted inside the sandbox over the supervisor relay.
Checklist
- I've reviewed existing issues and the architecture docs
- This is a design proposal, not a "please build this" request
Activity
rmalani-nv commentedon Jun 12, 2026
Updated the design to address the "avoid sharing workspace data between agents" concern by splitting workspace storage into two volumes by role, rather than going fully ephemeral:
/sandbox) — the only volume with the cross-agent leak concern. Isolated per claim and single-use: either ephemeral (emptyDir, fail-safe — kubelet reclaims it, nothing to orphan) or a per-Sandbox PVC destroyed on teardown (when reschedule-survival is needed; requires reliable destruction on every path +reclaimPolicy: Delete). Never returned to the pool.SandboxTemplate, so big files are on the filesystem without streaming through the relay. Safe to share precisely because it's read-only and holds no per-agent secrets.This preserves persistent PVCs for the large-file use case while guaranteeing the writable workspace is never shared. Note: read-sharing across nodes needs an
RWX/ROXprovisioner or a FUSE/object-store CSI —local-pathwon't do it. Cross-agent writable caches remain a separate, deliberate decision.The ephemeral-vs-per-Sandbox-PVC choice for the writable workspace is left as an explicit open decision in the issue body.
krishicks commentedon Aug 14, 2026
github-actions commentedon Aug 29, 2026
This issue has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. Comment or remove the state:stale label to keep it open.
grs commentedon Aug 30, 2026
I believe this is a valuable feature. As I understand it we are building consensus on the exact path forward.
AlanCoding commentedon Sep 8, 2026
It looks like #2157 is looking for the same thing but has developed further than this. Would the solutions described there address what is asked for here? I just didn't see any link yet.
drew commentedon Sep 21, 2026
Closing as a duplicate of #2157. Please continue warm-pool design and implementation tracking on the canonical issue.