github opensandbox-group/OpenSandbox release-1.2.0-rc.1
OpenSandbox 1.2.0-rc.1

pre-release2 hours ago

First release candidate on the 1.2.0 line, carrying everything merged since release-1.1.1 — the headline is OSEP-0024: auto-pause and wake-on-access for fast sandboxes, landing end to end. Images publish now; SDK/CLI packages ship at the 1.2.0 stable bump. Please test and report issues.

Highlights

  • Auto-pause and wake-on-access for fast sandboxes (OSEP-0024) lands end to end: ingress activity tracking and the wake path, the server-side idle sweeper, the opt-in lifecycle.idlePolicy create field, and the wake-retry contract in all five SDKs — off by default, additive only (#2156 #2180 #2192 #2198 #2218)
  • Egress sidecar (experimental, opt-in): live credential-bound Vault admission, HTTP/2 support with bounded drain, atomic policy-file persistence, and runtime fault containment (#2176 #2201 #2182 #2162 #2172)
  • SDK reliability: file uploads and downloads are no longer aborted at the 30-second request timeout in any of the five SDKs; execd now reports sandbox-scoped CPU/memory and OOM kills (#2165 #2186 #2196 #2206 #2191 #2200)

Server

  • Implement the server side of OSEP-0024 auto-pause for fast sandboxes: the lifecycle create field lifecycle.idlePolicy (idleTimeoutSeconds 30–86400, onIdle: pause, wakeOnAccess, optional activeProbe) validates with pydantic bounds and persists as the reserved sandbox.opensandbox.io/idle-policy Sandbox CR annotation (annotation-write failure stays fail-safe — the sandbox is created unpolicied); an idle sweeper enumerates opted-in fsb sandboxes from the informer cache, checks activity in Redis (monotonic-max writes, missing-key tombstones can only delay a pause), takes a per-sandbox pause lock with re-verify under lock, and issues an idempotent FastPath PauseSandbox; a declared activeProbe executes through the ingress gateway and a busy answer records ordinary activity; an OpenSandbox-Skip-Activity: skip sentinel suppresses the observation for one request and is stripped on both gateway and server-proxy hops (#2192 #2181)
  • Configure idle-pause under the new lifecycle-automation umbrella: [idle_pause] becomes [automation.idle_pause] and the redundant redis.enabled gate is removed — redis.dsn is required (validated at load) when enabled = true, failing loudly instead of silently not starting the sweeper; configuration.md is rewritten for conciseness and now documents ingress.secure_access and the missing env-var overrides (#2215)
  • Default BatchSandbox pods to restartPolicy: Never: a kubelet in-place container restart recreated the main container from its image — wiping the writable layer and in-memory state — while the sandbox kept reporting Succeed; restarting is never a meaningful recovery for a sandbox, so the honest default is no restart, which also makes the controller's terminal-failure path reachable. An explicit restartPolicy in the template still wins, the Windows profile keeps forcing Always (multi-phase installation relies on it), and pool mode is unchanged (#2128)
  • Treat pasta/slirp4netns/none as container-runtime network modes and pass them straight through, making podman's kernel-level networking selectable — on Windows 11 + WSL2 where netavark breaks bridge mode, pasta was the only working network and previously failed create with a 400 asking for a user-defined Docker network; networkPolicy is now rejected for these modes since there is no bridge for the egress sidecar to attach to (previously silently accepted for none) (#2203)
  • Parse Docker timestamps whose fractional second is not 3 or 6 digits: Docker writes RFC3339Nano (trailing zeros dropped), Python 3.10's fromisoformat rejects 5-digit fractions, and the helper fell back to datetime.now() — so createdAt and status.lastTransitionAt silently reported the current time; the fraction is now zero-padded to 6 digits (#2220)

SDKs

Code changes below ride the 1.2.0 stable release — rc ships no SDK packages.

  • Add the OSEP-0024 SDK contract to all five languages: a typed lifecycle.idlePolicy create field (IdlePolicy/ActivityProbe models with client-side fail-fast validation mirroring the server: timeout 30–86400 s, onIdle: "pause" pairing, probe path/timeout bounds; wired through both standard and template create) and the wake-retry contract — on a 503 carrying the gateway-owned OpenSandbox-Wake: expired marker (its presence proves the request was parked and never forwarded, making any-method retry safe), the SDK resends after Retry-After under a bounded budget (default 5 attempts / 60 s, per-client knobs), exhaustion raises a typed wake-timeout exception carrying attempts and waited time, the server-side resume is never canceled, bare 503 handling is unchanged, and SSE/streaming transports bypass wake retry (#2198 #2218)
  • Stop capping file uploads and downloads at the request timeout (30 s default) in all four remaining SDKs — the whole-call timeout (http.Client.Timeout, OkHttp callTimeout, AbortController, HttpClient.Timeout) covered sending the request body and reading the response, so any large or slow transfer failed mid-flight even while healthy; transfers now run on a dedicated client with no whole-call deadline that still bounds the wait for response headers, so a stalled transfer still fails instead of hanging (#2165 #2186 #2196 #2206)
  • Suppress sandbox.create client metrics for client-pool warmup creates in Go, Python, and JavaScript, aligning with Kotlin: warmup traffic (hardcoded skipHealthCheck, hundreds of ms) previously mixed into user-facing create-latency metrics and dragged p50/p99 down; direct-create from pool acquire and user-facing creates keep reporting (#2199)

Python

  • Clean up cancelled async pool startup: cancelling SandboxPoolAsync.start() mid-await left the instance stuck in STARTING, where a subsequent start() silently returned and startup-owned resources skipped cleanup; cancellation now reuses the reconciliation cleanup path, moves to STOPPED, and re-raises so the caller can retry (#2209)
  • Stop reporting failed file replacements as successful: the ordinary filesystem adapters caught and discarded the generated client's JSONDecodeError, so an empty or non-JSON HTTP 400/500 from replace_contents() returned success and an agent could keep building on an unapplied edit; the catch is removed and the documented SandboxException failure contract is restored (isolated-session adapters keep their empty-body compatibility catch) (#2222)

Go

  • Strip all OPEN-SANDBOX-*/OPENSANDBOX-* headers and every configured authentication header (including X-EXECD-ACCESS-TOKEN, X-API-Key, and custom WithAuthHeader names) on cross-origin redirects, comparing each destination against the initial request's scheme, host, and effective port so credentials cannot reappear on later foreign-origin hops; applied across lifecycle, execd, egress, file transfers, SSE, and creation metrics while preserving custom CheckRedirect policies (#2178)

Controller

  • Retain a BatchSandbox until cleanup completes: a deleted sandbox could disappear from the lifecycle API (GET 404) while its owned pod still ran, letting a replacement sandbox write the same persistent directory before the old writer exited; deletion now uses foreground cascading deletion alongside the existing task-cleanup and pool-release finalizers, and TTL reconciliation stops once deletion is accepted — DELETE still returns 204 when accepted, queries report Stopping during managed cleanup, and 404 arrives only after the runtime is gone; all five SDKs' kill docs and logs describe the accepted-deletion semantics (#2154)

Execd

  • Report OOM kills via cgroupfs polling: kernel-side evidence was out of reach from inside a sandbox (no CAP_SYSLOG, no /dev/kmsg), so OOM events were unobservable; execd now polls the sandbox cgroup every 5 s and diffs the kernel OOM kill counter (memory.events on v2, best-effort on v1) — the hierarchical counters cover nested cgroups like bwrap isolated sessions — exporting execd.system.oom.kills with a local/descendant level attribute plus structured warn logs with best-effort victim attribution; the watcher runs independently of OTLP export and needs no capabilities (#2191)
  • Report sandbox-scoped CPU/memory metrics: GET /metrics and the execd.system.cpu.usage/execd.system.memory.usage_bytes gauges read /proc through gopsutil, which inside a container describes the host — dashboards showed host-wide memory and CPU; when the sandbox cgroup carries configured limits, usage now comes from the sandbox's own cgroup (cpu.stat/cpu.max, memory.current/memory.max, v1 fallback, hybrid hosts prefer v1), with the previous host view kept for unlimitied guests such as fast-sandbox microVMs; the wire format is unchanged (#2200)
  • Percent-encode quotes and backslashes in Content-Disposition: a filename containing " or \ produced a quoted-string no standard parser accepts (mime.ParseMediaType rejected it and clients got no filename at all); such names now route through the same filename*=UTF-8'' encoding as non-ASCII names (#2161)
  • Parse the Range header before setting download headers: a malformed range answered 416 with the file's Content-Disposition and Content-Length still attached, truncating the JSON body or dropping it entirely — surfacing in the Python SDK as a connection error instead of a 416 SandboxApiException; the error response is now a clean JSON 416, and 200/206 responses are unchanged (#2216)

Networking

egress

All items below concern the default-off OSEP-0023 experimental revision runtime; legacy and DNS-only deployments are unchanged.

  • Add experimental live credential-bound Vault admission: Vault mutations flow through the revision transaction and install the private authenticated snapshot before publishing the public result; bound HTTPS connections are admitted and header credentials injected only after exact request matching with SNI/HTTP/1 authority agreement (ECH-extension ClientHellos stay opaque), with per-request credential rotation, fencing of removed bindings on existing connections, and transport retirement at the drain deadline; scope is exact-host HTTPS on 443 with HTTP/1.1 and inline header credentials (#2176)
  • Acknowledge live Policy revisions before publication: the Policy API and automatic deny.always/allow.always reloads now commit in order — optional file persistence, nft application, exact receiver acknowledgement or reconciled readback, then proxy publication and API success — with fail-closed recovery when the receiver fails after nft has taken effect, and 30-second drain deadlines on policy-only updates that never extend across later revisions (#2182)
  • Add experimental HTTP/2 admission and bounded drain: snapshots bind atomically to actual client stream IDs at requestheaders, post-fence requests get REFUSED_STREAM before reaching origin, GOAWAY waits for outstanding output obligations while expiry uses abnormal closure, and a once-only mitmdump exit fallback covers unverifiable cleanup ownership; the adapter is pinned to CPython 3.10/3.11 and rejects configured chained upstream proxies (#2201)
  • Contain runtime recovery faults in the sidecar network namespace: an unexpected MITM exit now triggers an independent, read-back-verified IPv4/IPv6 fence pinned to the original namespace for the process lifetime — blocking all input/output/forward traffic including existing connections and loopback — with QuarantineConfirmed reported only after real rule readback; normal successful mutations never introduce a fence (#2172)
  • Persist experimental revision policy files atomically: writes go through a same-directory temporary inode with fsync + rename + directory fsync, restores put back the exact previous bytes (or synchronized absence), and unsupported storage — file bind mounts, read-only or symlinked paths, hardlinks, unsafe ownership, unsupported SELinux/IMA metadata — is rejected before the original is touched; operators must provide an existing egress-owned, exclusively written directory mount (#2162)
  • Classify nft apply effects before policy recovery: local construction, pre-start cancellation, and start failures are Unchanged (state preserved, retry allowed after restore), successful execution is Committed, and every post-start or unclassified error is Unknown — closing writer admission and latching recovery before best-effort restoration; the missing-table fallback retains both errors and never downgrades an Unknown result (#2167)

ingress

  • Implement OSEP-0024 activity tracking and wake-on-access: with --activity-enabled, every routed request records a monotonic activity timestamp to Redis — fire-and-forget, never on the hot path — with per-replica write coalescing, Lua monotonic-max updates so out-of-order writes never regress the newest observation, and pipeline batching; with --wake-enabled, a request routed to a paused fast sandbox is parked instead of answered 503: one ResumeSandbox per replica per sandbox under a checkpoint-fenced singleflight, a bounded parking lot and budget (overflow/timeouts answer 503 + Retry-After while the restore continues, never canceled), and stale-route re-entry replayed on a fresh route only when safely replayable; with both flags unset, behavior is byte-identical to today (#2156)
  • Complete OSEP-0024 wake hardening: post-forward stale-route replay is restricted to idempotent methods with reproducible bodies (a forwarded POST is never silently re-executed); the pre-forward wake-timeout answer carries the OSEP-defined marker (503 + Retry-After: 1 + OpenSandbox-Wake: expired + a sandbox_wake_timeout body naming the sandbox), stripped from every upstream response so a sandbox cannot forge provenance; a separate restore permit (--wake-restore-max, held to flight terminal) bounds concurrent restores independently of the parking lot; and wake now requires the per-sandbox lifecycle.idlePolicy opt-in read strictly from the Sandbox CR annotation — missing or malformed means no policy and today's permanent 503, never an implicit opt-in; gateway.wake.* chart values wire the flags and add read-only RBAC only when enabled (#2180)

Fast Sandbox

  • Make sandbox deletion reliable: deletion now sends a fenced Fastlet delete before any runtime-absence check, so a missing registry entry, transport failure, or empty inspect response is no longer treated as proof the sandbox is gone (which could orphan the microVM while the CR disappeared); the Fastlet records a per-identity tombstone so a delayed create cannot revive a deleted sandbox, and the cleanup finalizer is persisted at CR creation (#2214)
  • Make create placement recover faster: after proven rejections FastPath refreshes its candidate list within a bounded budget (excluding Fastlets already tried) and waits up to 2 s for assignment status to catch up before switching, and a process-local create tracker stops a deleted or replaced Fastlet pod from blocking the new assignment's projection or racing pod-loss recovery — the first-create happy path is unchanged (#2214)
  • Batch guest NAT updates: new guest-VM slots prepare dedicated DNAT/SNAT chains and restore replaces only those chains in one iptables-restore --noflush batch, so reapplying the same guest address no longer accumulates rules, with a new fast_sandbox_network_guest_apply_stage_latency_seconds metric separating the apply stages (#2214)

Misc

  • Console: land the first slice of a new optional web console — a FastAPI BFF with httpOnly sessions and tenant/admin login proxying the lifecycle /sandboxes API (with a runtimeSummary on list/get), plus a React SPA covering tenant overview, sandbox list/create/detail, renew/delete, and the endpoint; first of five planned console PRs, with the admin UI to follow (#2129)
  • CLI: coerce config-file values to their declared types so a numeric api_key no longer silently resolves to null (breaking every command with a ValidationError) and a numeric defaults.timeout no longer crashes sandbox create (#2169); keep output writable on non-UTF-8 streams — GBK/cp1252/C-locale users could not run any osb command because emoji rendering raised UnicodeEncodeError, often after the work was already done; streams now escape unencodable characters and YAML output is re-dumped escaped to stay valid (#2187); write installed skills where each tool actually reads them — the claude target now writes .claude/skills/<name>/SKILL.md (the previous flat file was silently never loaded), and global cline/copilot targets write ~/.cline/rules/opensandbox-<name>.md and ~/.copilot/copilot-instructions.md; previously installed inert files are not migrated (#2194 #2208)
  • Fast-sandbox integration environment: drive the verify stages through the published osb CLI instead of raw curl (template build, sandbox create/kill, endpoint fetch, pause/resume, snapshot create/get/list/delete/restore, renew), with server_api kept only for calls the CLI does not expose (networkpolicy replace PUT, RFC 7396 merge-patch, the snapshot re-entry code fence, gateway probes) (#2171); add an opt-in IDLE_PAUSE=1 profile that deploys ephemeral Redis, enables the [automation.idle_pause] sweeper and gateway activity/wake values, and verifies the full auto-pause → wake-on-access loop on up, plus an SDK e2e suite and a nightly KVM workflow job (#2202)
  • Docs: accept and document OSEP-0024 — the proposal itself (#2152) plus an architecture page and user guide covering enablement, the idlePolicy field reference, the activity-probe contract, SDK wake-retry behavior, and an interactive diagram (#2217); unify component telemetry metrics from seven scattered locations into one reference at docs/guides/telemetry.md (#2205); refresh the network architecture pages with restyled SVG diagrams, a condensed egress page, and the network-isolation guide relocation (#2210); align CONTRIBUTING with the code (Go 1.25, Gin — not Beego) (#2175), dedupe AGENTS.md rules and state the architecture-vs-guides placement rule once (#2211), and fix the "shared Fastlet egress profile" misnomer to "shared egress process per Fastlet pod" with a note that gRPC ends at FastPath (#2225)
  • Tests/CI: align the node-agent log-collection e2e assertion with execd's preserved trailing newline (#2164); make the umbrella release workflow dispatch-only by dropping the weekly scheduled dry-run (#2223)
  • release: bump platform version to 1.2.0-rc.1 across charts and image references

Upgrade & Compatibility

  • Behavior change: BatchSandbox pods now default to restartPolicy: Never (implicit Always → Never) — deployments that relied on the implicit default and want restarts must set restartPolicy: Always explicitly in their batchsandbox_template_file (a startup warning will note the trade-off) (#2128)
  • Behavior change: deleted sandboxes stay queryable in Stopping until runtime cleanup completes, and only then return 404 — clients sharing a persistent directory should wait for the 404 before starting a replacement (#2154)
  • Behavior change: execd CPU/memory metrics report sandbox-scoped values when the sandbox cgroup has limits configured (previously host-wide); GET /metrics field semantics and OTel metric names are unchanged, but dashboards will shift (#2200)
  • Behavior change: networkPolicy is rejected for Docker network modes none/pasta/slirp4netns — previously accepted silently for none (#2203)
  • Images: opensandbox/<component>:release-1.2.0-rc.1 (all three registries)
  • Packages: unchanged — SDK/CLI artifacts publish at the 1.2.0 stable release
  • Charts: render from this tag (helm template ./manifests/charts/opensandbox)
  • Kubernetes: supported range v1.21 – v1.34 (charts declare kubeVersion: ">=1.21.1-0")
  • Skew: server ↔ CLI/SDK same line supported; ±1 minor warns

👥 Contributors

Thanks to these contributors ❤️

Artifacts

  • Images: opensandbox/<component>:release-1.2.0-rc.1 (Docker Hub / GHCR / ACR)

Installation

# Platform (Kubernetes) — render the chart at this tag and apply
git clone https://github.com/opensandbox-group/OpenSandbox
cd OpenSandbox
git checkout release-1.2.0-rc.1
helm dependency build manifests/charts/opensandbox  # package file:// sub-charts (not committed)
# the default render needs required values (server env / api key) and the
# ingress gateway — see docs/deployment before applying
helm template ./manifests/charts/opensandbox | kubectl apply -f -
# or point your GitOps platform (Argo / Flux) at the repo path + tag

Verify what you installed against the BOM: see
Release Verification.

Don't miss a new OpenSandbox release

NewReleases is sending notifications on new releases.