Warm-pooled sandboxes for the Kubernetes compute driver
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 8.7k
- Forks
- 1.3k
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 253
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 #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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the proposed phased plan and the existing validation logic in crates/openshell-server/src/auth/k8s_sa.rs, then review the agent-sandbox v0.4.6 warm-pool and extension CRDs. Resolve the open choice between an ephemeral workspace and a per-Sandbox PVC with teardown destruction, while preserving the fail-closed ownership chain and read-only shared-volume requirements. Done means the design is agreed well enough to implement the flagged driver, auth, lifecycle, and test phases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- kubernetes, rust
- Domain
- backend, infrastructure, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100