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.idlePolicycreate 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(idleTimeoutSeconds30–86400,onIdle: pause,wakeOnAccess, optionalactiveProbe) validates with pydantic bounds and persists as the reservedsandbox.opensandbox.io/idle-policySandbox 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 FastPathPauseSandbox; a declaredactiveProbeexecutes through the ingress gateway and a busy answer records ordinary activity; anOpenSandbox-Skip-Activity: skipsentinel 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 redundantredis.enabledgate is removed —redis.dsnis required (validated at load) whenenabled = true, failing loudly instead of silently not starting the sweeper;configuration.mdis rewritten for conciseness and now documentsingress.secure_accessand 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 reportingSucceed; 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 explicitrestartPolicyin the template still wins, the Windows profile keeps forcingAlways(multi-phase installation relies on it), and pool mode is unchanged (#2128) - Treat
pasta/slirp4netns/noneas 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,pastawas the only working network and previously failed create with a 400 asking for a user-defined Docker network;networkPolicyis now rejected for these modes since there is no bridge for the egress sidecar to attach to (previously silently accepted fornone) (#2203) - Parse Docker timestamps whose fractional second is not 3 or 6 digits: Docker writes RFC3339Nano (trailing zeros dropped), Python 3.10's
fromisoformatrejects 5-digit fractions, and the helper fell back todatetime.now()— socreatedAtandstatus.lastTransitionAtsilently 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.idlePolicycreate field (IdlePolicy/ActivityProbemodels 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 a503carrying the gateway-ownedOpenSandbox-Wake: expiredmarker (its presence proves the request was parked and never forwarded, making any-method retry safe), the SDK resends afterRetry-Afterunder 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, bare503handling 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, OkHttpcallTimeout,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.createclient metrics for client-pool warmup creates in Go, Python, and JavaScript, aligning with Kotlin: warmup traffic (hardcodedskipHealthCheck, 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 inSTARTING, where a subsequentstart()silently returned and startup-owned resources skipped cleanup; cancellation now reuses the reconciliation cleanup path, moves toSTOPPED, 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 fromreplace_contents()returned success and an agent could keep building on an unapplied edit; the catch is removed and the documentedSandboxExceptionfailure 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 (includingX-EXECD-ACCESS-TOKEN,X-API-Key, and customWithAuthHeadernames) 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 customCheckRedirectpolicies (#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 —
DELETEstill returns 204 when accepted, queries reportStoppingduring managed cleanup, and 404 arrives only after the runtime is gone; all five SDKs'killdocs 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.eventson v2, best-effort on v1) — the hierarchical counters cover nested cgroups like bwrap isolated sessions — exportingexecd.system.oom.killswith alocal/descendantlevel 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 /metricsand theexecd.system.cpu.usage/execd.system.memory.usage_bytesgauges read/procthrough 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.ParseMediaTyperejected it and clients got no filename at all); such names now route through the samefilename*=UTF-8''encoding as non-ASCII names (#2161) - Parse the
Rangeheader before setting download headers: a malformed range answered 416 with the file'sContent-DispositionandContent-Lengthstill attached, truncating the JSON body or dropping it entirely — surfacing in the Python SDK as a connection error instead of a 416SandboxApiException; 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.alwaysreloads 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_STREAMbefore 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
QuarantineConfirmedreported 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: oneResumeSandboxper replica per sandbox under a checkpoint-fenced singleflight, a bounded parking lot and budget (overflow/timeouts answer 503 +Retry-Afterwhile 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
POSTis never silently re-executed); the pre-forward wake-timeout answer carries the OSEP-defined marker (503+Retry-After: 1+OpenSandbox-Wake: expired+ asandbox_wake_timeoutbody 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-sandboxlifecycle.idlePolicyopt-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 --noflushbatch, so reapplying the same guest address no longer accumulates rules, with a newfast_sandbox_network_guest_apply_stage_latency_secondsmetric 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
/sandboxesAPI (with aruntimeSummaryon 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_keyno longer silently resolves to null (breaking every command with aValidationError) and a numericdefaults.timeoutno longer crashessandbox create(#2169); keep output writable on non-UTF-8 streams — GBK/cp1252/C-locale users could not run anyosbcommand because emoji rendering raisedUnicodeEncodeError, 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 — theclaudetarget now writes.claude/skills/<name>/SKILL.md(the previous flat file was silently never loaded), and globalcline/copilottargets write~/.cline/rules/opensandbox-<name>.mdand~/.copilot/copilot-instructions.md; previously installed inert files are not migrated (#2194 #2208) - Fast-sandbox integration environment: drive the verify stages through the published
osbCLI instead of rawcurl(template build, sandbox create/kill, endpoint fetch, pause/resume, snapshot create/get/list/delete/restore, renew), withserver_apikept 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-inIDLE_PAUSE=1profile 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 onup, 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
idlePolicyfield 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 atdocs/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), dedupeAGENTS.mdrules 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.1across charts and image references
Upgrade & Compatibility
- Behavior change: BatchSandbox pods now default to
restartPolicy: Never(implicitAlways→Never) — deployments that relied on the implicit default and want restarts must setrestartPolicy: Alwaysexplicitly in theirbatchsandbox_template_file(a startup warning will note the trade-off) (#2128) - Behavior change: deleted sandboxes stay queryable in
Stoppinguntil 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 /metricsfield semantics and OTel metric names are unchanged, but dashboards will shift (#2200) - Behavior change:
networkPolicyis rejected for Docker network modesnone/pasta/slirp4netns— previously accepted silently fornone(#2203) - Images:
opensandbox/<component>:release-1.2.0-rc.1(all three registries) - Packages: unchanged — SDK/CLI artifacts publish at the
1.2.0stable 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 ❤️
- @Dev-next-gen
- @GodBlf
- @JasonBuildAI
- @Jinzhengxu
- @Jun516
- @Pangjiping
- @acodercat
- @comqx
- @hpliStartAgain
- @kittimzhe
- @mengdehong
- @ranjianghong526-hash
- @ratishjain12
- @zw11591-sketch
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 + tagVerify what you installed against the BOM: see
Release Verification.