36 KiB
Compute Runtimes
Compute runtimes create, stop, start, delete, and watch sandbox workloads for the
gateway. A supported runtime provisions openshell-sandbox inside the workload,
openshell-supervisor outside it, a protected channel between them, and an
independent outer network fence. Drivers do not implement policy evaluation.
Podman provisions a paired workload and supervisor container using its native
libpod API. The workload uses network=none; the external supervisor alone joins
the configured network. A per-sandbox named volume carries their mutually
authenticated gRPC Unix socket, with supervisor credentials kept in its separate
filesystem. Both containers run as the resolved non-root identity with all
capabilities dropped. They share only a user namespace for volume ownership,
not PID, mount, or network namespaces. Podman owns paired lifecycle and health;
the common protocol owns process, identity, TCP, DNS, and forwarding semantics.
Driver Contract
External resource admission is an operator-owned boundary shared by drivers. The gateway gates caller driver JSON independently from attachment approval. Drivers resolve the complete effective attachment inventory against authoritative resource labels before launch and on reuse. Missing labels or an unsupported resolver deny access; GPU attachments are an explicit temporary exception. Fresh sandbox-private resources instead require verified provisioning ownership. Workload metadata must not grant approval or override admission evidence.
The shared evaluator lives in openshell-core; native resolution remains in
each driver. External drivers acknowledge the effective versioned policy through
capabilities, and policy mismatch prevents activation or new launch operations.
Trusted deployment configuration can explicitly disable label admission, but
that opt-out does not waive other ownership and isolation checks. This boundary
assumes operators control approval metadata and runtime resource replacement;
it does not provide atomic mount authorization or instantaneous revocation.
Each runtime receives a sandbox spec and canonical policy from the gateway and is responsible for:
- Selecting the sandbox image.
- Resolving an immutable non-root sandbox identity before workload creation.
- Supplying separate sandbox and supervisor bootstrap material.
- Delivering
openshell-sandboxto the workload andopenshell-supervisoronly to the external supervisor placement. - Provisioning protected control and boundary configs plus a private Unix socket, TLS-authenticated TCP, or vsock transport when the supervisor is separated. Runtime-specific code supplies immutable resource claims and transport coordinates; the shared boundary protocol supplies lifecycle, exec, signaling, forwarding, and binary identity semantics.
- Forwarding the exact canonical main-process argv and TTY mode without shell reconstruction. The sandbox-level environment and policy workspace apply to the main process.
- Reporting lifecycle and platform events back to the gateway.
- Cleaning up runtime-owned resources.
Drivers report runtime-observed state only and must not hold references to
gateway-internal types. For supervisor-controlled runtimes, Ready=True means
only that the compute resource is healthy; the gateway also requires a
supervisor session before publishing SandboxPhase::Ready. For
drivers that report runtime readiness, Ready=True is authoritative because the driver
launches and monitors the policy-constrained workload itself.
compute_driver.proto is the supported gateway/driver extension boundary.
At initialization the gateway snapshots the driver's identity, version,
default image, gateway-lifecycle preference, and
driver_reports_runtime_readiness from GetCapabilities. The gateway includes
the canonical SandboxPolicy in DriverSandboxSpec.policy for validation and
creation. Drivers that enforce policy outside the standard supervisor fetch
later revisions through GetSandboxConfig and acknowledge them through
ReportPolicyStatus.
Process-identity omissions are preserved across this boundary so every driver
can apply its native image or runtime defaults. Drivers connect supervisors to
the operator-configured gateway endpoint; they do not request additional
gateway listeners.
Canonical main-process support is part of the ComputeDriver contract. Every
in-tree and extension driver must forward the exact specification; it is not an
optional capability that drivers can omit or negotiate.
Drivers own runtime-specific platform event interpretation. When an event should
drive client provisioning UI, the driver attaches the shared
openshell.progress.* metadata defined in openshell-core instead of requiring
clients to parse Kubernetes reasons, VM cache states, or other driver-local
reason strings.
Sandbox Readiness Composition
The gateway composes driver state with the advertised readiness behavior to
produce the public SandboxPhase:
backend_phase = derive_phase(driver_status)
public_phase =
if backend_phase in {Error, Deleting}: → pass through (terminal precedence)
if driver_reports_runtime_readiness && backend_phase == Ready: → Ready
if backend_phase == Ready && session connected: → Ready
if backend_phase == Ready && no session: → Provisioning
if backend_phase in {Provisioning, Unknown} && session: → Ready
if backend_phase in {Provisioning, Unknown} && no session: → Provisioning
For a supervisor-controlled runtime, public_phase == Ready means both the
backend resource is healthy and a supervisor session is registered. A sandbox whose
backend reports ready but has no supervisor session yet holds Provisioning with a
Ready=False, SupervisorNotConnected condition and the message
Backend ready; waiting for supervisor session. This distinguishes it from a sandbox
whose compute resource is still provisioning without exposing contradictory public
readiness signals. When the driver reports runtime readiness, its ready condition
is published without waiting for a supervisor session.
Session precedence over lagging driver snapshots: A supervisor session can only be
established by a running workload. When set_supervisor_session_state promotes the
store record to Ready on session connect, a driver watch event may still arrive
shortly after carrying a stale Provisioning or Unknown backend phase. The
composition rule treats a connected session as the stronger signal and keeps Ready
in that case, preventing a lagging snapshot from undoing the session-driven promotion.
HA session composition: Live relay handles remain process-local, while the session-owning gateway publishes a short-lived owner record in shared PostgreSQL. Driver reconciliation treats a fresh local or remote owner as connected, so a non-owner replica cannot demote the shared sandbox phase merely because it lacks the in-memory stream. Session-bound requests are forwarded to the owning gateway; a supervisor reconnect publishes a higher connection epoch before stale-session cleanup can demote readiness. Multi-replica deployments therefore require shared PostgreSQL and the gateway peer Service configured by the Helm chart.
Extension point: Driver-reported readiness is a capability, not an
operator-configurable hook. A driver may enable it only when it owns workload
readiness. Policy delivery remains independent: create-time policy is embedded
in the sandbox specification, and later revisions use the existing sandbox
configuration API. RFC-0010 lifecycle hooks may observe readiness transitions via
post_commit; they do not override the composition rule.
The capability RPC reports driver identity, version, and the default sandbox image used by the gateway. GPU availability stays driver-local and is validated when a sandbox create request asks for GPU resources.
The gateway sends its common extension peer metadata with the startup capability request. The driver validates that metadata before responding, and the gateway rejects a driver whose protocol major or capability requirements are incompatible. It records the negotiated protocol, implementation identity and version, capability sets, and typed resource support once. Elevated gateway info reports that immutable snapshot instead of re-querying drivers on each request.
Compiled Driver Selection
The gateway binary explicitly installs the compute drivers compiled into that binary before entering server startup. The server selects a configured driver by normalized registry name. When no driver is configured, it evaluates only the installed drivers' probes and chooses the lowest registered priority. Drivers without a probe, including VM, remain opt-in.
Startup computes this selection once after merging configuration. The same selection drives authentication defaults and runtime construction, so a probe result cannot change which driver is constructed later in startup.
This follows the same composition model as SQLx's Any drivers: the binary
defines the available implementation set, while the runtime consumes a generic
registry. Adding or removing a compiled driver therefore changes registration
rather than the server's selection flow. Alternate gateway binaries can install
their own ComputeDriverFactory registrations and hand the completed registry
to run_cli_with_compute_drivers; factories receive merged driver config and
return either an in-process driver or a gateway-managed remote endpoint. The
server constructs the common runtime adapter and snapshots GetCapabilities
for either result. A configured UDS endpoint still takes precedence over a
compiled registration with the same name.
The openshell-gateway composition crate groups first-party registrations
behind the in-tree-compute-drivers feature. openshell-server has no compute
driver dependencies or backend-name dispatch. Protocol-only gateway builds
disable the composition feature and link no compute-driver crates. E2E lanes
compose that gateway with Docker, Podman, Kubernetes, and VM driver executables
over the public UDS gRPC contract so an in-tree driver cannot silently depend
on a server-only API.
Stop and Start Lifecycle
The gateway persists lifecycle intent before mutating compute:
Ready -> Stopping -> Stopped -> Starting -> Ready
A canonical main process that exits successfully follows Ready -> Completed.
A nonzero or signal-normalized result follows Ready -> Error with a
MainProcessFailed condition. Both retained results may be started explicitly,
which creates a fresh main-process instance. Drivers must not automatically
restart a completed or failed canonical process. Before an explicit restart,
the gateway disconnects the prior supervisor session and deletes its SSH
sessions so credentials cannot cross runtime generations.
StopSandbox and StartSandbox are idempotent driver operations. Stop
retains the driver resource and its persistent workspace boundary while making
exec, SSH, forwarding, and exposed services unavailable. Start reactivates the
same resource. The gateway requires a fresh supervisor session before a
starting sandbox returns to Ready; stale driver snapshots and supervisor
sessions cannot promote a Stopped row.
Runtime credentials are generation-scoped and memory-only after launch. A
supervisor or Sandbox Runtime process replacement does not resume a running
generation. Planned upgrades stop the sandbox first; the following start mints
a fresh session, TLS identity, and credential pair. An unexpected replacement
leaves the old workload on the normal fail-closed disconnect path.
The gateway commits the new authorization identity and the durable Starting
phase in one resource-version update. A concurrent start that loses that update
reuses the winning identity, so every idempotent driver retry receives credentials
that match the persisted sandbox.
A driver stop operation does not complete while its backend still reports an
in-progress stop. This prevents an immediate start from racing the previous
run's delayed exit event and regressing the new run to Error.
The Kubernetes driver records stop as a durable two-phase transition. The
releasing phase releases the sandbox's runtime-control relationship while the
workload boundary remains reachable. The suspending phase then suspends the
Agent Sandbox workload and cleans generation bootstrap material. The current
dedicated-supervisor implementation releases control by deleting the supervisor
Pod. Periodic reconciliation resumes either phase after a gateway restart. Pod
deletion waits include the configured termination grace period plus Kubernetes
API observation headroom.
Persisted Stopping and Starting rows are retried at startup. Stable
Stopped rows remain stopped. Docker and Podman retain the stopped container
and attached storage, Kubernetes retains the Sandbox CR and PVC while scaling
compute to zero, and VM retains its launch request and writable overlay beside
a stop marker. Delete remains a separate operation that removes these
resources.
On graceful gateway shutdown, persisted running intent for Docker, Podman, and
VM is stopped through the shared StopSandbox RPC before any gateway-managed
driver process exits. The gateway does not persist Stopped for this
infrastructure event. On startup, it reconciles the retained intent through the
shared idempotent StartSandbox RPC before watch processing begins. Explicitly
Stopped sandboxes are excluded from both sweeps. Kubernetes workloads are
cluster-owned and continue running without gateway shutdown or startup
lifecycle calls.
The driver reports this behavior through
GetCapabilities.gateway_manages_lifecycle. The same declaration works for
in-process and external drivers. Older drivers omit the field and retain the
conservative operator-managed behavior.
Drivers that can verify a platform-native sandbox credential advertise
GetCapabilities.supports_sandbox_authentication. On the path-scoped
IssueSandboxToken exchange, the gateway forwards the opaque bearer credential
to that selected driver through AuthenticateSandbox. The driver returns the
authenticated sandbox ID and opaque runtime identity. The gateway verifies
that both match its durable sandbox record and returns a generation-bound
session JWT whose lineage is checked on every subsequent sandbox RPC. Legacy
unbound sandbox JWTs are not admitted when session authentication is enabled.
The driver socket is therefore a sandbox-identity trust boundary, but it does
not grant user or administrator authority.
Deletion Lifecycle
Lifecycle requests use per-sandbox gates to serialize stop, start, and delete attempts. A delete request resolves the name once and remains bound to that stable ID. The only combined lock order is lifecycle gate, then the gateway-wide state guard; external driver calls run without the global guard.
Lifecycle gates are process-local and do not coordinate gateway replicas. They serialize attempts rather than share results: if one attempt fails and recovery restores a deletable state, a request waiting on the gate may retry the driver. Persisted resource-version checks remain the cross-replica safety boundary.
Watcher events do not acquire lifecycle gates. Exact resource-version checks allow
them to interleave safely: status snapshots are no-ops for Deleting rows,
deleted events are idempotent, and snapshots for absent rows are ignored.
An accepted delete (deleted = true) is finalized by the watcher. If the
backend is already absent (deleted = false), the request removes gateway state
synchronously. Sandbox row removal remains bound to the stable ID and resource
version. Settings retain their existing best-effort name-based cleanup; SSH
sessions, indexes, and watch/log buses are cleaned after confirmed removal.
Owned-record cleanup discovers records before mutating them and uses bounded
set-based deletes so teardown cannot amplify one sandbox into an unbounded
sequence of individual persistence writes.
When a sandbox is instead discovered gone out-of-band — a watcher deletion
event, or the periodic prune sweep finding no matching driver resource, with
no explicit DeleteSandbox request involved at all — the gateway also
releases driver-owned resources (for example Podman's per-sandbox secrets and
workspace volume) by calling the driver's idempotent DeleteSandbox, not just
gateway state. Both paths skip that call when a request-side lifecycle
operation already holds the sandbox's gate, since that operation already owns
driver-side cleanup. The watch path defers the call itself to a background
task after a non-blocking gate check, so a slow driver call cannot stall the
sequential watch loop; the prune sweep calls the driver inline, since it
already makes a blocking GetSandbox call per sandbox as part of its normal
operation.
The request acquires both locks before starting owned work, so cancellation
while queued does not leave a delete armed. After that commitment point, the
owned task prevents cancellation from stranding a mutation. A gateway restart
does not start a persisted Deleting operation. If the backend completed the
delete, reconciliation removes the row; otherwise it can remain Deleting.
Runtime Summary
| Runtime | Best fit | Sandbox boundary | Notes |
|---|---|---|---|
| Docker | Local development with Docker available. | Capability-free workload container. | Uses network_mode=none; a separate capability-free supervisor container mediates egress and access over a private daemon-local Unix socket volume. |
| Podman | Existing rootless driver. | Container. | Not converted by this isolation stack. |
| Kubernetes | Cluster deployment through Helm. | Capability-free sandbox Pod. | Always creates a namespace-wide empty-egress workload NetworkPolicy and a separate capability-free supervisor Pod over mutually authenticated TLS. It requires an enforcing CNI and trusted sandbox namespace; the Kubernetes API does not attest policy enforcement. |
| VM | Experimental microVM isolation. | Per-sandbox libkrun or QEMU VM. | The NIC-less guest runs openshell-sandbox as PID 1; host openshell-supervisor owns gateway networking and reaches the guest over vsock. |
| Extension | Out-of-tree drivers operated alongside the gateway. | Whatever boundary the driver implements. | Selected by a custom compute_drivers = ["<name>"] entry with [openshell.drivers.<name>].socket_path, or at launch time by pairing --drivers <name> with --compute-driver-socket=<path>. A launch-time endpoint may use a canonical built-in name to preserve its driver-config key while replacing in-process construction. The gateway connects to an operator-provisioned UDS, snapshots GetCapabilities, and dispatches all sandbox lifecycle calls through compute_driver.proto. The driver process and socket lifecycle are operator-owned; the gateway does not spawn, supervise, or remove unmanaged extension drivers. The trust boundary is the socket's filesystem permissions: the operator must ensure only the gateway uid can read/write it. |
Per-sandbox CPU and memory values currently enter the driver layer through template resource limits. Docker and Podman apply them as runtime limits. Kubernetes mirrors each limit into the matching request. VM accepts the fields but currently ignores them.
Reusable sandbox workload templates are resolved before the compute-driver
boundary. Drivers do not receive a separate template resource; the gateway
lowers the selected SandboxWorkloadTemplate into the existing sandbox spec
and validates that spec before calling ValidateSandboxCreate or
CreateSandbox. Template CPU and memory become the same typed resource limits
described above. Template GPU settings become ResourceRequirements, preserving
the driver's default GPU assignment when the count is omitted. Template
driver_config remains a driver-keyed envelope until the compute layer selects
the active driver block and forwards only that block to the driver.
Docker and Podman also accept per-sandbox driver-config mounts for existing
runtime-managed named volumes and tmpfs mounts. Podman additionally accepts
image mounts through its image-volume API. User-supplied bind and volume mounts
default to read-only. Direct host bind mounts, and Docker or Podman local-driver
bind-backed named volumes, are available only when explicitly enabled in the
active local driver table of gateway.toml. Host bind mounts are an unsafe
operator override because they place gateway-host filesystem state inside the
sandbox and can negate OpenShell workspace isolation and filesystem-policy
controls. Driver-owned supervisor, token, and TLS bind mounts stay reserved.
Network features follow the driver/substrate split. Drivers own only the outer fence and protected channel. The sandbox owns seccomp notification, local DNS, socket virtualization, process observation, and binary identity. The supervisor owns DNS eligibility, policy authorization, destination filtering, upstream dials, relay behavior, credential rewriting, and OCSF decisions. No supported path requires nftables, a workload network namespace, proxy environment variables, added capabilities, or an unconfined AppArmor profile.
The Kubernetes deployment packaging has two ownership boundaries. The gateway chart owns the gateway workload, configuration, Services, PKI, and cluster-scoped gateway resources. The workspace chart is installed into a pre-provisioned sandbox namespace and owns only the sandbox ServiceAccount, namespaced RBAC, and sandbox ingress NetworkPolicy. Its RoleBinding names the gateway ServiceAccount and namespace explicitly, so the two releases have disjoint lifecycle ownership. A shared-mode gateway can target one external namespace, while operator mode maps workspace names to multiple platform-provisioned namespaces.
Resource requirements enter the driver layer through SandboxSpec.resource_requirements. This includes a set of GPU requirements, where a user
can request a specific number of GPUs or the driver-specific default behaviour.
For all in-tree drivers, this is equivalent to selecting a single GPU.
VM runtime state paths are derived only from driver-validated sandbox IDs
matching [A-Za-z0-9._-]{1,128}. The gateway-owned VM driver socket uses a
private run/ directory plus Unix peer UID/PID checks. Standalone
unauthenticated TCP mode is disabled unless explicitly enabled for local
development. The VM image cache is owner-only. When the host assembles a
bootstrap rootfs from OCI layers, cross-layer symlinks may resolve only within
that rootfs; absolute or escaping targets reject the image before a later layer
can write through them. Rootfs traversal and mutation use opened directory
handles with no-follow file creation, so later copies, permission changes, and
whiteouts cannot be redirected by replacing a validated pathname component.
The bootstrap rootfs comes only from the operator-configured bootstrap_image
or default_image; a sandbox-requested image never becomes the VM bootstrap
image. The gateway rejects configurations without either trusted source, and
the standalone driver independently fails startup for the same condition.
Runtime-specific implementation notes belong in the driver crate README:
crates/openshell-driver-docker/README.mdcrates/openshell-driver-podman/README.mdcrates/openshell-driver-kubernetes/README.mdcrates/openshell-driver-vm/README.md
The VM guest bootstrap runs once as root to prepare mounts, loopback, and the
safe port-53 sysctl. It then drops to the resolved identity with empty
capability sets and executes openshell-sandbox as guest PID 1.
Supervisor Delivery
Drivers deliver the two binaries to separate trust domains:
| Runtime | Delivery model |
|---|---|
| Docker | A digest-pinned daemon-local volume supplies openshell-sandbox; the companion image runs openshell-supervisor. |
| Podman | Existing driver behavior; not converted by this stack. |
| Kubernetes | A non-root init container stages openshell-sandbox into a memory volume; a directly managed Pod runs openshell-supervisor. |
| VM | openshell-sandbox is embedded in the guest rootfs; a separately digest-checked native openshell-supervisor runs on the host. |
| Extension | Defined by the out-of-tree driver. |
Driver-controlled sandbox bootstrap must override image or template values for sandbox identity, command metadata, resolver configuration, and public trust paths. Gateway endpoints, callback credentials, policy, and private TLS material belong only to the supervisor placement.
Process Identity
The gateway preserves whether each policy process field was omitted and passes the admitted selectors to the driver. The driver resolves one exact UID, GID, and supplementary-group set before creating the immutable workload:
- Docker pins the image ID, resolves policy selectors against the image's
/etc/passwdand/etc/group, and validates its OCI working directory. - Kubernetes uses platform-resolved numeric values, including OpenShift namespace ranges.
- VM uses the configured numeric guest identity.
UID/GID zero and u32::MAX are invalid. The sandbox and every child start with
the resolved identity and zero capability masks; neither process performs an
in-workload UID transition. Identity-changing policy updates require sandbox
recreation, while other policy updates remain live.
Docker uses an absolute OCI working directory as the workspace. Empty, root,
and explicit /sandbox values select /sandbox; other paths must already
exist without symlink or reserved-mount collisions and must be usable by the
resolved identity. Kubernetes and VM use /sandbox.
Executable Identity Binding
Every mediated network open carries a connection-bound BinaryIdentity from
the isolation backend. The identity contains the socket-owning executable and
each executable ancestor, nearest first, as an absolute workload path plus a
SHA-256 digest. The backend resolves these values for the accepted connection
and hashes already-open live executable objects rather than reopening their
paths. Command-line paths remain diagnostic context and cannot authorize a
request.
Before policy evaluation, the supervisor validates and pins the complete leaf and ancestor chain in one runtime-scoped trust-on-first-use cache. It rejects missing digests, invalid paths, conflicting evidence within a chain, or a digest that differs from an existing path pin. Validation and insertion are atomic, so a rejected chain cannot leave partial pins.
The cache is shared across authorization paths for the lifetime of the supervisor network runtime. Policy reloads replace policy state without clearing executable pins; restarting the runtime creates a new cache. OPA receives executable and ancestor paths plus endpoint policy context. Digests remain supervisor-side integrity evidence and are not policy inputs. Any unavailable, incomplete, or conflicting executable evidence fails closed before OPA can authorize the connection.
The Kubernetes driver creates the namespace-wide empty-egress workload fence
before a suspended Sandbox CR, then provisions split immutable bootstrap
Secrets, the private runtime Service, and a gated supervisor Pod. A
non-root init container stages openshell-sandbox and one-use bootstrap files
into memory volumes. The workload Pod never mounts supervisor or gateway
credentials. The driver removes its scheduling gate only after the companions
exist; measured confirmation and supervisor-session registration gate public
readiness.
Images
The gateway image and Helm chart are built from this repository. Users supply workload images as standard OCI images.
Custom sandbox images must include the agent runtime and any system dependencies, but they should not need to include the gateway. GPU-capable images must include the user-space libraries required by the workload. The runtime still owns GPU device injection. GPU requests are explicit, and can be refined with a driver-native device identifier or requested count; the gateway validates the request shape and each runtime enforces the GPU allocation modes it supports.
Deployment Shape
Kubernetes deployments use the Helm chart under deploy/helm/openshell. The
chart deploys the gateway and sandbox runtime integration. The default gateway
workload is a StatefulSet for SQLite-backed single-replica installs. External
database-backed installs can render a Deployment with workload.kind=deployment;
HA deployments must point server.externalDbSecret at an operator-managed
PostgreSQL database. Agent Sandbox CRDs and controller lifecycle remain
operator-owned; the chart can optionally preflight for a served supported API
but does not install the cluster-scoped dependency. OpenShell's Kubernetes test
clusters install the upstream core manifest; Agent Sandbox extensions are not
required by the gateway.
Standalone local deployments start the gateway with a selected runtime such as
Docker, Podman, or VM. The CLI can register multiple gateways and switch between
them without changing the sandbox architecture.
Workspace Namespace Modes (Kubernetes)
The Kubernetes driver maps workspaces to namespaces through the workspace_mode
configuration field (WorkspaceMode in crates/openshell-driver-kubernetes/src/config.rs).
The mode controls namespace resolution, resource naming, sandbox CR watching, SA
token authentication, and RBAC requirements.
| Mode | Namespace resolution | Resource name | Namespace lifecycle |
|---|---|---|---|
| Shared (default) | Single static namespace from config | {workspace}--{name} |
None |
| Managed | openshell-{gateway_id}-{workspace} |
bare sandbox name | Driver creates and deletes |
| Operator | Workspace name maps 1:1 to a pre-provisioned namespace | bare sandbox name | External (platform team) |
Shared renders all sandboxes into one configured namespace. Resource names embed the workspace prefix for collision avoidance. No namespace lifecycle management. RBAC uses a namespace-scoped Role.
Managed auto-creates a K8s namespace per workspace on first sandbox create.
Each new namespace receives a ServiceAccount and the configured gateway-only
SSH ingress NetworkPolicy. Each sandbox runtime generation gets immutable copies
of the configured image-pull Secrets, read from the driver's source namespace and
named after the generation, so a sandbox picks up rotated registry credentials
on its next start. Their sources are operator-selected gateway configuration,
not caller attachments. An existing Secret with a generation name fails the
create and is never adopted. The namespace also copies
OpenShift SCC UID-range and supplemental-group annotations from the gateway
namespace when present. The driver deletes the namespace during workspace
deletion. The workspace remains durably Terminating until the Kubernetes API
accepts namespace cleanup, so a transient failure can be retried. Namespace
deletion uses the fetched UID as a
precondition to avoid deleting a replacement namespace. Requires a non-empty
gateway_id (validated as a
DNS-1123 label at startup) so the namespace prefix fits within the K8s 63-character
limit. RBAC promotes sandbox CRD permissions to a ClusterRole and adds namespace
create/delete and ServiceAccount create/get permissions.
Outside shared mode, the gateway client TLS material is staged into each
generation's supervisor bootstrap Secret rather than mounted from a Secret in
the workspace namespace. Every Secret the driver writes into a workspace
namespace is therefore generation-scoped, immutable, and created with create
only. RBAC cannot constrain create by resourceNames, so managed mode grants
cluster-wide Secret create and delete; source reads use a Role in the
driver's source namespace. Recovery deletes the Secrets of the recorded and
target runtime generations by exact name; Pod owner references let garbage
collection remove any other generation. The
driver exercises these broad permissions only in gateway-owned managed
namespaces. This depends on the managed-mode ownership invariant described below;
the gateway ServiceAccount must not be shared with unrelated workloads.
Operator mode does not create NetworkPolicies or copy image-pull Secrets.
Platform teams must apply the gateway ingress boundary and provision configured
image-pull Secrets in every operator-managed namespace.
The gateway ClusterRole grants no Secret permissions in operator mode. The
openshell-workspace chart Role installed in each operator-managed namespace
grants bootstrap Secret create and delete.
Operator uses pre-provisioned namespaces discovered through two optional
sources: a K8s label selector (operator_namespace_label) and a drop-in
allowlist file (operator_namespace_file). Exactly one must be configured.
The compute driver and the gateway's ServiceAccount authenticator independently
watch that public config source; no in-process driver state crosses into the
server. Sandbox creation and token bootstrap fail closed if the workspace is
not in the current allowlist. Platform teams manage namespace lifecycle
externally. RBAC uses the same ClusterRole as managed mode but without namespace
create/delete or ServiceAccount permissions.
Watching and Querying
Managed and operator modes set is_multi_namespace() == true, which switches
sandbox CR watchers from namespace-scoped Api::namespaced to cluster-wide
Api::all_with. In managed mode the driver scopes cluster-wide queries with a
LABEL_GATEWAY_ID label selector to support multiple gateways on the same
cluster. K8s Events are not watched in cluster-wide mode — the cluster-wide
watcher emits only sandbox CR changes, not platform events.
SA Token Authentication
The Kubernetes driver's AuthenticateSandbox implementation applies its named
[openshell.drivers.kubernetes] configuration per mode:
- Shared:
Exact— accepts only the single configured namespace. - Managed:
Prefix— accepts any namespace starting withopenshell-{gateway_id}-. - Operator:
Allowlist— accepts namespaces present in the dynamicBTreeSetpopulated by the label/file watchers. Starts empty (fail-closed) until the first watcher update.
It validates the projected token with Kubernetes TokenReview, checks the live
pod UID, and verifies the pod's controlling Sandbox CR UID and sandbox ID. The
driver returns both the sandbox ID and an opaque runtime identity derived from
the namespace, immutable Sandbox CR UID, and authenticated supervisor Pod UID.
Advertising sandbox authentication includes the runtime-binding contract. The
gateway requires non-empty runtime identities from successful create, start,
and authentication responses. It records the runtime identity when provisioning
succeeds and requires an exact match before issuing a sandbox JWT. If binding
validation or storage fails after a lifecycle call succeeds, the gateway
compensates that call before returning the error. This correlates credential
authentication with the durable runtime record rather than authorizing from the
sandbox ID alone.
StartSandbox carries the previously recorded opaque identity. Kubernetes
requires exactly one label-selected Sandbox CR and verifies that its namespace
and immutable UID match that identity before replacing the supervisor Pod. The
new Pod UID becomes the updated binding only after the continuity check passes.
Shared and managed modes still reserve the sandbox namespace, Sandbox CRs, sandbox pods, and configured sandbox ServiceAccount for the Kubernetes driver and trusted Agent Sandbox controller. In operator mode, the platform operator retains namespace lifecycle ownership and must preserve the same control of those resources. An allowlisted namespace is a trust grant, not a tenant isolation boundary.
Credential Driver Integration
The Kubernetes Secrets credential driver (openshell-driver-kubernetes-secrets)
stores every provider credential in its single configured namespace, in every
workspace mode, and rejects handles that reference another namespace. The
gateway reaches those Secrets through a namespaced Role; the gateway
ClusterRole grants no credential Secret permissions.
When runtime infrastructure changes, validate the relevant sandbox e2e path and update the matching driver README if a maintainer-facing constraint changes.