server/v0.2.3

opensandbox-group/OpenSandboxserver/v0.2.3Aug 26, 2026by Pangjiping

AI Summary

The server introduces lifecycle hooks, QEMU VMState support, pool capacity exhaustion handling, and various security and performance fixes.

Key Highlights

  • Sandbox lifecycle hooks support
  • QEMU VMState pause/resume for runc-based sandboxes
  • Pool capacity exhaustion is now machine-identifiable
  • Docker runtime sandbox_env and sandbox_binds support

Breaking Changes

  • OPENSANDBOX_EGRESS_SANDBOX_ID is now server-injected and rejected in request env

New Features

  • Sandbox lifecycle hooks
  • QEMU VMState pause/resume
  • Pool capacity exhaustion identification
  • Pool allocation summary in lifecycle responses
  • Generic HTTP request metrics via OTLP
  • Secure-access signing keys from Kubernetes Secret
  • Docker runtime env/binds support
  • Egress sidecar readiness timeout configurable
  • Tenant namespaces validated at startup

Full Release Notes

## What's New

### ⚠️ Breaking Changes

- **`OPENSANDBOX_EGRESS_SANDBOX_ID` is now server-injected and rejected in request env** — the server unconditionally sets the egress sidecar's sandbox-id attribution variable in both Docker and Kubernetes runtimes, and removed it from the env allowlist. Requests that pass it in `env` now get a 400 instead of a user-spoofable value. #1381

### ✨ Features

- **Sandbox lifecycle hooks (`preStart` / `periodic`), OSEP-0020** — `CreateSandboxRequest.lifecycle` is accepted, validated (reserved transports, duplicate periodic names, pool-allocation combinations), and transported to Kubernetes workloads via the reserved `OPEN_SANDBOX_LIFECYCLE` env for execd to execute. Docker explicitly rejects lifecycle requests; PATCH and other hook types are out of scope. Ships with aligned Java/Kotlin, Python, and TypeScript SDK models. #1588 #1589

- **`preStart.timeoutSeconds` limit raised from 300s to 3 hours** — server and OpenAPI limits now allow up to 10800s for pre-start hooks; the periodic limit stays at 300s. The execd-side product policy bound is removed, keeping policy changes server-only. #1637

- **QEMU VMState pause/resume for runc-based sandboxes** — opt-in capture of guest memory/vCPU/device state via QMP migration; rootfs and compressed VMState are stored as separate OCI images keyed by immutable SHA-256 digests. Supports pooled BatchSandbox pause, detachment, and standalone restoration while preserving guest memory, writable disk, boot ID, and counter continuity. Rootfs-only snapshots remain backward compatible. #1584

- **Pool capacity exhaustion is now machine-identifiable** — the controller publishes a `PoolAllocationPending`/`PoolCapacityExhausted` condition; the server waits a bounded acquisition window (new `kubernetes.pool_acquisition_timeout_seconds`, default 30s) and then returns HTTP 429 with `KUBERNETES::POOL_CAPACITY_EXHAUSTED` and `Retry-After: 5` instead of the generic 504 readiness timeout. Older controllers safely fall back to the existing behavior. #1581

- **Pool allocation summary in lifecycle responses** — `getSandbox` now surfaces an optional `Sandbox.allocation` block (`mode: pool`, `poolRef`, `state: allocated`) for confirmed Kubernetes pool allocations, emitted only for concrete non-wildcard pool references with matching allocation evidence. No pod/namespace/IP data is exposed. #1481

- **Generic HTTP request metrics via OTLP** — a thin ASGI middleware exports `server.http.request.duration` (ms) with `http_method`, matched `http_route` (or `unknown` for unrouted/auth-failed requests), and `http_status_code` attributes, complementing the existing create-duration histogram. Raw paths, IDs, keys, and bodies are never attributes; no-op when `[otel]` is disabled. #1561

- **Stable diagnostics API with scopes** — diagnostics now honor the public inline descriptor contract: `logs: container|all` and `events: runtime|lifecycle|all`, with 400 `DIAGNOSTICS_SCOPE_UNSUPPORTED` for unknown scopes and explicit warnings for incomplete best-effort scopes. Deprecated no-`scope` plain-text responses still work with a `Deprecation` header. #1553

- **fast-sandbox fleets backend groundwork (OSEP-0007)** — a FastPath v2 gRPC client wraps all FastPathService RPCs with typed error mapping (`FastPathNotFound` → 404, etc.), and a create/status mapping layer converts the public contract to fast-sandbox semantics, rejecting unsupported fields (`volumes`, `platform`, `resourceRequests`, `credentialProxy`, `snapshotId`, etc.) with clear errors. Not yet wired as an active runtime. #1558 #1559

- **Secure-access signing keys can come from a Kubernetes Secret** — the server accepts `OPENSANDBOX_SECURE_ACCESS_KEYS` / `OPENSANDBOX_SECURE_ACCESS_ACTIVE_KEY` env overrides (mirroring the API-key override, gateway mode required), and the Helm chart adds `server.gateway.secureAccess.existingSecret` (mutually exclusive with plaintext `keys`, enforced at render time) so GitOps flows never commit key material. #1445

- **Docker runtime `sandbox_env` / `sandbox_binds`** — new `[docker]` fields apply fleet-wide environment variables and host bind mounts to every sandbox container (request `env` wins over `sandbox_env`; config binds precede request-derived binds), giving the Docker runtime the operator-side hook the K8s pod template has. #1303

- **Egress sidecar readiness timeout configurable** — new `[egress] timeout_seconds` (default 30s, non-positive values rejected) lets Docker deployments tune how long the server waits for the sidecar health endpoint. #1522

- **Tenant namespaces validated at startup (OSEP-0014)** — the server fail-fast verifies every tenant namespace exists and is accessible before serving traffic, aggregating all missing/inaccessible namespaces into a single fatal error instead of failing only at request time. OSEP-0004/0005/0014 marked implemented. #1389

### 🔒 Security

- **CVE-remediating dependency bumps** — `google.golang.org/grpc` → v1.82.1 across kubernetes/ingress/execd/egress/internal (xDS RBAC and HTTP/2 fixes), `postcss` → 8.5.11 (arbitrary file read via sourceMappingURL). #1385

### 🐛 Bug Fixes

- **Docker endpoint lookups ~108x faster** — container lookup now goes through the deterministic `sandbox-<id>` name with `opensandbox.io/id` label validation, falling back to the label scan only when needed. Local benchmark: 6.07s → 56ms for execd+egress endpoint resolution. #1400

- **Informer cache expires when the watch stalls** — `has_synced` now means "cache is currently live", not "an initial list finished once"; a stalled or half-open watch (e.g. after a control-plane upgrade) degrades to direct API reads instead of serving frozen status forever, which previously caused every create to 504 with `POD_READY_TIMEOUT`. Bound derived from existing knobs (`resync_period + watch_timeout`); no new config. #1534

- **Proxy connection leak on aborted responses** — when a downstream client disconnected before response-body streaming started, the backend httpx response was never released, gradually exhausting the shared pool until `httpcore.PoolTimeout`. The proxy now owns the backend response for the full ASGI lifecycle, shields `aclose()` from cancellation, and releases on setup failure. Stress test: 1000/1000 disconnects cleaned up. #1488

- **Unique, stable proxy operation IDs** — the 20 proxied HTTP operations previously emitted 4 process-dependent duplicate operation IDs (16 OpenAPI warnings); each method now gets its own deterministic ID with identical runtime dispatch, auth, and 405 behavior. Generated clients should be regenerated. #1634

- **PVC auto-create no longer crashes with a malformed 500** — the PVC owner-reference patch went through an unsupported `_content_type` path that raised `ApiTypeError` (leaving PVCs without owner refs) and `SandboxErrorCodes.INTERNAL_ERROR` was undefined, masking the real error. Both fixed: patch uses the supported strategic-merge `call_api` path; error code defined. #1538 #1447

- **Template container `securityContext` now reaches pods** — the deep-merge previously replaced the template's containers wholesale, silently dropping container-level securityContext. Template keys now supplement (never override) runtime values, matching the existing volumeMounts philosophy. #1564

- **Root-relative backend redirects stay under the proxy route** — `Location: /login` from a sandbox backend is rebased to `/v1/sandboxes/{id}/proxy/{port}/login`; absolute, network-path, and query-only references are left untouched. #1516

- **Server-proxy endpoints drop the ingress routing header** — `OpenSandbox-Ingress-To` (case-insensitive) is removed from `use_server_proxy=true` responses while secure-access and other endpoint headers are preserved. #1503

- **No duplicate `Date`/`Server` headers on proxied responses** — the proxy preserves an origin `Date` while suppressing Uvicorn's default (and the backend `Server` header), emitting exactly one `Date`. #1471

- **Invalid-resume error no longer leaks controller internals** — the 409 message for a Running (internally Succeed) sandbox now says "state Running" instead of "phase Succeed"; HTTP status and error code unchanged. #1586

- **Non-finite CPU limits rejected** — `nan`, infinite, and overflowed values are rejected before Docker NanoCpus conversion instead of being silently accepted. #1568

- **Mixed read-only PVC subPath mounts allowed** — the PVC source is set read-only only when every mount of the claim is read-only; the service-level rejection that blocked legitimate mixed-policy PVC creation is removed. #1440

- **Agent-sandbox server-proxy endpoints resolve** — internal endpoint resolution now delegates to the workload provider and reads agent-sandbox pod addresses from `status.podIPs` instead of the BatchSandbox-only annotation, and brackets IPv6 literals before appending the port. #1424

- **Version reporting fixed everywhere** — `/openapi.json`, installed package metadata, and the chart `appVersion` all reported stale `0.1.0`; the server now reads its version from package metadata at runtime, builds inject `SETUPTOOLS_SCM_PRETEND_VERSION`, and the release script bumps `Chart.yaml` `appVersion`. #1439

### 📦 Misc

- **execd v1.0.22 ships OSEP-0018 init-mode hardening** — execd as sandbox init (PID 1) with pre-exec hardening floor (env strip, bounding set, no_new_privs, identity drop, ambient caps, seccomp) and Kubernetes hardening e2e + PTY/bwrap coverage. #1474 #1555

- **Isolated-session fail-closed workload lifecycle gate** — the server distributes a native Bubblewrap lifecycle gate to `/opt/opensandbox` for both runtimes; workloads stay blocked until execd marks them ready. First phase only — capability admission and session networking remain disabled. #1388

- **Egress transparent MITM can intercept extra TCP ports** — experimental `OPENSANDBOX_EGRESS_MITMPROXY_EXTRA_PORTS` appends ports to the always-on 80/443 with fail-closed validation (multiport cap 15); credential binding still applies to canonical ports only. #1379

- **Auto-bumps** — `egress` v1.1.5 → v1.1.7 (#1391 #1498 #1600); `execd` v1.0.22 (#1501).

## 👥 Contributors

Thanks to these contributors ❤️

- @aGallea
- @cwj2001
- @Federaffo
- @fengcone
- @gegemeimingzi
- @GodBlf
- @Gujiassh
- @hittyt
- @hpliStartAgain
- @jianpingpei
- @Joey777210
- @luffy0x
- @mameikagou
- @mpu
- @Pangjiping
- @ruirui6946
- @tea-artist
- @y4ney

---

- PyPI: opensandbox-server==0.2.3
- Docker Hub: opensandbox/server:v0.2.3
- Aliyun Registry: sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/server:v0.2.3
- GHCR: ghcr.io/opensandbox-group/server:v0.2.3