Skip to content

feat(k8s): support ephemeral emptyDir-backed sandbox workspaces as an alternative to the default PVC #2787

Description

@kon-angelo

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_class set, the workspace PVC stays Pending and 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-sandbox shutdownPolicy: Retain).
  • Give up the fail-safe property emptyDir provides — 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"}}' \
  -- claude

Rendering (crates/openshell-driver-kubernetes/src/driver.rs, apply_workspace_persistence):

  • Pvc → existing path unchanged.
  • Ephemeral → skip volumeClaimTemplates entirely; add a pod-spec emptyDir volume named workspace at /sandbox (WORKSPACE_MOUNT_PATH); the workspace-init init container still seeds /sandbox from the image on every pod start (sentinel unnecessary — volume is always empty at pod start).

Observable behavior

  • sandbox create in Ephemeral mode succeeds on a cluster with no StorageClass installed.
  • sandbox stop + sandbox start on 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 /sandbox state (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_backend accepts "pvc" (default) and "ephemeral"; unknown values rejected at config parse.
  • With workspace_backend = "ephemeral", the rendered pod spec contains an emptyDir volume at /sandbox and the Sandbox CR contains no volumeClaimTemplates entry.
  • With workspace_backend = "ephemeral", sandbox creation succeeds on a cluster that has no default StorageClass and no explicit workspace_storage_class.
  • The workspace-init init container seeds /sandbox from 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" with workspace_default_storage_size or workspace_storage_class fails validation with a clear error.
  • Helm value server.workspaceBackend and the gateway TOML field render correctly through the deployment path and are documented in docs/reference/gateway-config.mdx and docs/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_mount side-effect. Keeps the emergent workaround as the answer. Requires operators to supply an unrelated PVC just to disable default injection, does not deliver emptyDir semantics, and remains contract-less.
  • Generic Ephemeral Volumes (ephemeral.volumeClaimTemplate on the pod spec). Still requires a StorageClass; does not solve the "cluster has no provisioner" case. Complementary; could be added later as a third workspace_backend variant.
  • tmpfs for /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 through driver_config, not by opting the whole PVC surface out via raw CRs).

Agent Investigation

Checklist

  • I've reviewed existing issues and the architecture docs
  • This is a design proposal, not a "please build this" request

Activity

  1. removed
    state:triage-neededOpened without agent diagnostics and needs triage
    on Oct 1, 2026
  2. reopened this on Oct 7, 2026
  3. added
    state:acceptedA maintainer decided OpenShell should pursue this issue
    on Oct 7, 2026
  4. lunarwhite commented on Oct 12, 2026

    @lunarwhite
    Contributor
    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:

    📋 triage-agent

    Triage Assessment

    Classification: validated-feature

    Summary

    The gap is real at main (eeba0e795), and an emptyDir workspace needs no CRD change. Parts of the design and criteria drifted since filing. Confidence is high.

    Investigation

    • OpenShell adds a workspace claim template unless a driver-config mount sits below /sandbox (crates/openshell-driver-kubernetes/src/driver.rs:6244-6283).
    • Both pinned Agent Sandbox APIs allow omitting it; neither admission nor the Pod validator needs the PVC.
    • No duplicate or open PR; feat: add warm-pool provisioning for Kubernetes sandboxes #2157 (warm pools) is complementary.

    Superseded parts of the issue:

    • Per-sandbox override (AC1 key path, AC6, examples): per-sandbox config is only pod/containers/volumes, off by default, and rejects the size example (driver.rs:367-373); dropped for one gateway setting.
    • workspace_backend/Ephemeral: "ephemeral" means --no-keep, and "workspace" also names tenancy; renamed workspace_volume (persistent_volume_claim/empty_dir).
    • AC7: contradicts the design section, and the size always defaults to "2Gi"; it becomes the sizeLimit, class stays PVC-only, no validation.
    • AC4 "sentinel unnecessary": the create-only seeder needs it and runs once per new Pod anyway; unchanged.
    • AC5 render diff: existing tests pin the default render; dropped.
    • AC8 (server.workspaceBackend, docs/reference/*): feat(helm): migrate gateway configuration to gatewayConfig #3384 made server.* deprecated aliases of gatewayConfig, and those pages are gone; gatewayConfig only, docs in configuration.mdx/runtimes.mdx.
    • AC9 "delete the pod": stop/start is the supported path (restartPolicy: Never), and CI's default Kubernetes lane runs only conformance; unit tests and a recorded kind run instead.
    • Reset cases (issue: stop/start, eviction): restart-policy replacement and starting a Completed or failed sandbox also reset /sandbox, gateway restarts do not; documented, unguarded.
    • AC3: true for sandboxes; the gateway StatefulSet claims a PVC (deploy/helm/openshell/templates/statefulset.yaml:21-28), needing a pre-bound PV or workload.kind=deployment with server.externalDbSecret; out of scope.
    • Stale references: line numbers moved, Warm-pooled sandboxes for the Kubernetes compute driver #1879 closed as a duplicate of feat: add warm-pool provisioning for Kubernetes sandboxes #2157, and the workaround is documented (docs/how-it-works/sandboxes/runtimes.mdx:271-273).

    Impact Signals

    • Affected users/scope: Kubernetes operators without dynamic provisioning or a default StorageClass.
    • Regression: No.
    • Workaround: Partial: a PVC mount under /sandbox skips the claim but needs a labeled PVC and allow_driver_config.
    • Evidence quality: High: traced in code and pinned upstream sources; no cluster run.

    Human Decision Required

    None: It was accepted on 2026-10-07.

  5. lunarwhite commented on Oct 13, 2026

    @lunarwhite
    Contributor
    previous plan

    🏗️ build-plan

    Outcome

    Kubernetes operators can opt a gateway into backing /sandbox with a size-capped emptyDir instead of a per-sandbox PVC, so its sandboxes need no StorageClass. One PR closes #2787.

    Design

    • One operator-owned setting in [openshell.drivers.kubernetes]: workspace_volume = "persistent_volume_claim" (default, rendering exactly as today) or "empty_dir".
    • empty_dir omits the workspace volumeClaimTemplates entry and adds {name: workspace, emptyDir: {sizeLimit: <workspace_default_storage_size>}}. The agent's /sandbox mount and the seed-workspace init container, sentinel included, stay unchanged, so every new Pod is seeded from the image.
    • workspace_default_storage_size becomes the sizeLimit: the sandbox user's HOME defaults 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_class applies to PVCs only.
    • A driver-config mount under /sandbox still replaces the managed workspace in both modes.
    • /sandbox lives as long as the workload Pod (documented, not guarded): stop/start, restart-policy replacement, and starting a Completed sandbox 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.
    • Conformance stays unchanged and certifies the default PVC mode; an empty_dir gateway fails sandbox-lifecycle by design (crates/openshell-conformance/src/scenarios/sandbox_lifecycle.rs:45-116). No capability flag or new lane.
    • Helm users set gatewayConfig.openshell.drivers.kubernetes.workspace_volume; the chart does not change. The standalone driver takes --workspace-volume / OPENSHELL_K8S_WORKSPACE_VOLUME.
    # Gateway TOML: workspace_volume = "empty_dir" under [openshell.drivers.kubernetes]
    gatewayConfig:
      openshell.drivers.kubernetes:
        workspace_volume: empty_dir

    Tests

    • Serde tests: empty_dir parses; unknown values fail.
    • One empty_dir render test: no claim templates, a size-capped workspace emptyDir, and the /sandbox mount. A default-render test also checks that no Pod-level workspace volume is added.
    • One manual kind run, recorded in the PR: on an empty_dir gateway with the default StorageClass removed after its own PVC binds, a sandbox reaches Ready with no workspace PVC, holds the image's /sandbox content, shows the sizeLimit on its volume, and loses a marker across stop/start.
    • No Rust e2e test: it needs an empty_dir gateway, 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 commented workspace_volume line 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 suggests empty_dir.
  6. lunarwhite commented on Oct 13, 2026

    @lunarwhite
    Contributor

    I will raise a PR based on above plan later, if no other comments.

    Check #4542 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:acceptedA maintainer decided OpenShell should pursue this issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions