Repository navigation
feat(k8s): support ephemeral emptyDir-backed sandbox workspaces as an alternative to the default PVC #2787
Description
User Story
As a platform operator running OpenShell on a managed Kubernetes cluster (ROSA, EKS, AKS, GKE, Gardener) or on a short-lived CI cluster, I want to opt sandboxes into an ephemeral, emptyDir-backed workspace instead of the per-sandbox PVC, so that I can run OpenShell in environments where PVC provisioning is unavailable, unreliable, expensive, or disallowed by cluster policy — without giving up sandbox lifecycle correctness.
Problem Statement
The Kubernetes compute driver unconditionally injects a workspace PVC into every sandbox pod via volumeClaimTemplates on the agents.x-k8s.io/Sandbox CR (crates/openshell-driver-kubernetes/src/driver.rs:2041-2176, constants at :413-431). After PR #2088 removed SandboxTemplate.volume_claim_templates from the public API, there is no supported way to run a sandbox pod without a PVC. The only knobs available today are workspace_default_storage_size (#1436) and workspace_storage_class (#2442, PR #2463) — both change which PVC gets created, not whether one is created.
An emergent workaround exists (has_explicit_sandbox_data_mount disables default PVC injection when the operator supplies any driver_config subPath mount under /sandbox), but it is undocumented, requires supplying an unrelated PVC just to trigger the branch, and does not deliver emptyDir semantics. It is an implementation side-effect, not a contract.
The agents.x-k8s.io/Sandbox CRD does not require the workspace to be a PVC — volumeClaimTemplates is optional in api/v1beta1. This is entirely an OpenShell-side rendering assumption, so the change is scoped to this repo.
Impact / Why This Matters
Without an ephemeral option, users of the Kubernetes driver today must:
- Provide a StorageClass in every target cluster, even when the sandbox does not need pod-reschedule survival. On clusters without a default StorageClass and without
workspace_storage_classset, the workspace PVC staysPendingand the sandbox never starts. This blocks OpenShell adoption on short-lived CI clusters, edge clusters, restricted managed clusters, and Gardener-style shoots where dynamic provisioning is not always installed. - Pay the provisioning cost and lifecycle risk of a PVC per sandbox, even for short-lived agent tasks that never restart. The PVC + init-container path adds first-start latency (called out as a stopgap in the code comment at
driver.rs:399-411) and, as documented in Warm-pooled sandboxes for the Kubernetes compute driver #1879, leaves orphaned PVCs holding written user data on teardown paths that skip the finalizer (default agent-sandboxshutdownPolicy: Retain). - Give up the fail-safe property
emptyDirprovides — kubelet-guaranteed reclaim, no external object to orphan, no dependence on cleanup being correct. Warm-pooled sandboxes for the Kubernetes compute driver #1879 already identifies this as a valid workspace model, but scopes it to warm-pooled sandboxes only, leaving cold-path sandboxes without the option. - Rely on an undocumented driver-config side-effect (
has_explicit_sandbox_data_mount) to skip the PVC — not a stable contract and requires supplying a foreign PVC just to disable default injection.
The maintainer-authored design for the current PVC path (#743) explicitly anticipated this: "Opt-out mechanism: some users may want ephemeral sandboxes. Consider --no-persist flag or server env var. Can be a follow-up." No follow-up has been filed.
Proposed Design
Add a workspace backend selector to the Kubernetes driver configuration, mirroring the shape of workspace_storage_class (#2442) exactly.
Gateway-wide default (crates/openshell-driver-kubernetes/src/config.rs):
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WorkspaceBackend {
/// Per-sandbox PVC via `volumeClaimTemplates` on the Sandbox CR.
/// Survives pod rescheduling. Requires a StorageClass.
#[default]
Pvc,
/// `emptyDir` volume mounted at `/sandbox`. No PVC created.
/// Ephemeral — deleted with the pod.
Ephemeral,
}
pub struct KubernetesComputeConfig {
// ...existing fields...
pub workspace_backend: WorkspaceBackend, // new
pub workspace_default_storage_size: String, // ignored when Ephemeral
pub workspace_storage_class: String, // ignored when Ephemeral
// ...
}- Same driver-config → gateway TOML → Helm value fan-out that the existing storage-class field uses (
server.workspaceBackend). - Default is
Pvc, preserving current behavior byte-for-byte.
Per-sandbox override — no new proto or CLI surface required. The existing SandboxTemplate.driver_config envelope (proto/openshell.proto:862) is already the driver-keyed opaque config the gateway forwards to the compute driver, and the CLI already exposes it as --driver-config-json. The only change is: the Kubernetes driver reads workspace_backend from the per-request struct as it already does for other overridable fields.
# Ephemeral sandbox on a gateway defaulted to pvc:
openshell sandbox create --name ephemeral-task \
--driver-config-json '{"kubernetes":{"workspace_backend":"ephemeral"}}' \
-- claude
# Persistent sandbox on a gateway defaulted to ephemeral:
openshell sandbox create --name durable-task \
--driver-config-json '{"kubernetes":{"workspace_backend":"pvc","workspace_default_storage_size":"10Gi"}}' \
-- claudeRendering (crates/openshell-driver-kubernetes/src/driver.rs, apply_workspace_persistence):
Pvc→ existing path unchanged.Ephemeral→ skipvolumeClaimTemplatesentirely; add a pod-specemptyDirvolume namedworkspaceat/sandbox(WORKSPACE_MOUNT_PATH); theworkspace-initinit container still seeds/sandboxfrom the image on every pod start (sentinel unnecessary — volume is always empty at pod start).
Observable behavior
sandbox createinEphemeralmode succeeds on a cluster with no StorageClass installed.sandbox stop+sandbox starton an ephemeral sandbox starts with a fresh/sandbox— documented as the ephemeral contract, complementary to the PVC-backed lifecycle from feat(sandbox): add storage-preserving suspend and resume lifecycle #2652.- Pod eviction / rescheduling on an ephemeral sandbox loses
/sandboxstate (documented). PVC-backed sandboxes retain today's behavior unchanged. kubectl get pvc -n <ns>shows zero workspace PVCs for ephemeral sandboxes.
Acceptance Criteria
-
driver_config.kubernetes.workspace_backendaccepts"pvc"(default) and"ephemeral"; unknown values rejected at config parse. - With
workspace_backend = "ephemeral", the rendered pod spec contains anemptyDirvolume at/sandboxand theSandboxCR contains novolumeClaimTemplatesentry. - With
workspace_backend = "ephemeral", sandbox creation succeeds on a cluster that has no default StorageClass and no explicitworkspace_storage_class. - The
workspace-initinit container seeds/sandboxfrom the image on every pod start in ephemeral mode. - Default (unset) behavior is byte-for-byte identical to today's PVC path — asserted by a render-diff test.
- Per-sandbox override via
--driver-config-json '{"kubernetes":{"workspace_backend":"..."}}'works and takes precedence over the gateway default. - Combining
workspace_backend = "ephemeral"withworkspace_default_storage_sizeorworkspace_storage_classfails validation with a clear error. - Helm value
server.workspaceBackendand the gateway TOML field render correctly through the deployment path and are documented indocs/reference/gateway-config.mdxanddocs/reference/sandbox-compute-drivers.mdx. - Kubernetes e2e: create an ephemeral sandbox, write a marker to
/sandbox, delete the pod, verify (a) no PVC is left behind and (b) a re-created sandbox with the same identity starts with a fresh/sandbox.
Alternatives Considered
- Do nothing; document the
has_explicit_sandbox_data_mountside-effect. Keeps the emergent workaround as the answer. Requires operators to supply an unrelated PVC just to disable default injection, does not deliveremptyDirsemantics, and remains contract-less. - Generic Ephemeral Volumes (
ephemeral.volumeClaimTemplateon the pod spec). Still requires a StorageClass; does not solve the "cluster has no provisioner" case. Complementary; could be added later as a thirdworkspace_backendvariant. tmpfsfor/sandbox. Bounded by pod memory limits and interacts poorly with the image-seed path (image content can be arbitrarily large). Not a fit for the default case.- Wait for the container-snapshotting replacement referenced by
driver.rs:399-411. There is no tracking issue for that work; the ephemeral option is complementary and useful regardless of when snapshotting lands. - Reintroduce
SandboxTemplate.volume_claim_templates. Explicitly reverted by PR refactor(api): remove SandboxTemplate.volume_claim_templates #2088. This proposal instead follows the current direction (customize throughdriver_config, not by opting the whole PVC surface out via raw CRs).
Agent Investigation
- Kubernetes driver injects the workspace PVC unconditionally:
crates/openshell-driver-kubernetes/src/driver.rs:413-431(constants),:2041-2139(apply_workspace_persistence),:2150-2176(default_workspace_volume_claim_templates). - Emergent bypass via subPath mounts:
has_explicit_sandbox_data_mountdisables default injection for any driver-config mount at-or-under/sandbox(noted by @elezar in PR feat(kubernetes): support PVC subPath driver config #2034 review). - PR refactor(api): remove SandboxTemplate.volume_claim_templates #2088 removed
SandboxTemplate.volume_claim_templatesfrom the public API, closing the previous raw-CR escape hatch and consolidating storage configuration behinddriver_config.kubernetes. - Existing driver-keyed override pipe:
SandboxTemplate.driver_config(proto/openshell.proto:862) is already the opaque per-request envelope the gateway forwards to the compute driver — no proto change required for the per-sandbox override. - CLI already exposes the override generically:
--driver-config-jsononsandbox create(crates/openshell-cli/src/main.rs:1386-1392, parser incrates/openshell-cli/src/run.rs:262) — no CLI change required. - feat(sandbox): persist /sandbox directory across gateway restarts via init container + PVC #743 (design spike for the current PVC path) explicitly deferred the ephemeral opt-out as a follow-up that never materialized.
- Warm-pooled sandboxes for the Kubernetes compute driver #1879 (warm-pooled sandboxes) proposes
emptyDiras one of two workspace models — but only on the warm path, leaving cold-path sandboxes without the option. - Precedent for the config-knob shape and rollout: feat(k8s): support configuring storageClassName for the default workspace PVC #2442 → PR feat(k8s): support configuring workspace PVC storageClassName #2463 (
workspace_storage_class) — identical driver/TOML/Helm/docs fan-out. - Managed-cluster context: feat: Support restricted SecurityContextConstraints for managed Kubernetes platforms #899 (restricted SCC on OpenShift) confirms the managed-Kubernetes audience is present upstream; discussion Using openshell-sandbox standalone on edge devices (automotive/robotics) #2626 (edge devices) is an adjacent audience for the same "PVC undesirable" case.
- The
agents.x-k8s.io/SandboxCRD (kubernetes-sigs/agent-sandbox,api/v1beta1) does not requirevolumeClaimTemplates— the field is optional, so this proposal needs no operator or CRD change.
Checklist
- I've reviewed existing issues and the architecture docs
- This is a design proposal, not a "please build this" request
Activity
lunarwhite commentedon Oct 12, 2026
previous triage
It's been a while since the issue was filed. So I've tried to re-triage it against the latest codebase, assessment is as follows:
lunarwhite commentedon Oct 13, 2026
previous plan
Outcome
Kubernetes operators can opt a gateway into backing
/sandboxwith a size-cappedemptyDirinstead of a per-sandbox PVC, so its sandboxes need no StorageClass. One PR closes #2787.Design
[openshell.drivers.kubernetes]:workspace_volume = "persistent_volume_claim"(default, rendering exactly as today) or"empty_dir".empty_diromits the workspacevolumeClaimTemplatesentry and adds{name: workspace, emptyDir: {sizeLimit: <workspace_default_storage_size>}}. The agent's/sandboxmount and theseed-workspaceinit container, sentinel included, stay unchanged, so every new Pod is seeded from the image.workspace_default_storage_sizebecomes thesizeLimit: the sandbox user'sHOMEdefaults to/sandbox(crates/openshell-sandbox/src/process.rs:186-200), so today's PVC size bounds it; exceeding it evicts the Pod instead of returning ENOSPC.workspace_storage_classapplies to PVCs only./sandboxstill replaces the managed workspace in both modes./sandboxlives as long as the workload Pod (documented, not guarded): stop/start, restart-policy replacement, and starting aCompletedsandbox or one whose main process failed re-seed it, eviction loses it, and a gateway restart leaves it intact (crates/openshell-server/src/compute/mod.rs:3257-3259). Start re-applies the stored volumes (crates/openshell-driver-kubernetes/src/driver.rs:3275-3297), so the mode is fixed at creation.empty_dirgateway failssandbox-lifecycleby design (crates/openshell-conformance/src/scenarios/sandbox_lifecycle.rs:45-116). No capability flag or new lane.gatewayConfig.openshell.drivers.kubernetes.workspace_volume; the chart does not change. The standalone driver takes--workspace-volume/OPENSHELL_K8S_WORKSPACE_VOLUME.Tests
empty_dirparses; unknown values fail.empty_dirrender test: no claim templates, a size-cappedworkspaceemptyDir, and the/sandboxmount. A default-render test also checks that no Pod-levelworkspacevolume is added.empty_dirgateway with the default StorageClass removed after its own PVC binds, a sandbox reachesReadywith no workspace PVC, holds the image's/sandboxcontent, shows thesizeLimiton its volume, and loses a marker across stop/start.empty_dirgateway, which fails conformance by design and would need its own lane. The existing Kubernetes e2e lane runs locally for the default path.Documentation
docs/how-it-works/gateways/configuration.mdx: a commentedworkspace_volumeline in the Kubernetes TOML example.docs/how-it-works/sandboxes/runtimes.mdx: an option-table row, one sentence on lifetime and the size cap (eviction), and "default workspace volume" instead of "default workspace PVC" in the PVC-mounts note.crates/openshell-driver-kubernetes/README.md: a paragraph on the mode and its per-start seeding.skills/debug-openshell-cluster/SKILL.md: the PVC-pending row suggestsempty_dir.lunarwhite commentedon Oct 13, 2026
I will raise a PR based on above plan later, if no other comments.Check #4542 (comment)