# Technical Documentation: alibaba/OpenSandbox

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/alibaba/OpenSandbox)

### Overview (README.md)

<div align="center">
  <img src="docs/public/images/logo.svg" alt="OpenSandbox logo" width="150" />

  <h1>OpenSandbox</h1>

<p align="center">
  <a href="https://github.com/opensandbox-group/OpenSandbox"><img src="https://img.shields.io/github/stars/opensandbox-group/OpenSandbox?style=flat-square&logo=github&logoColor=white&label=Stars&color=181717" alt="Stars" /></a>
  <a href="https://www.bestpractices.dev/projects/12588"><img src="https://img.shields.io/badge/OpenSSF-Best-4C566A?style=flat-square" alt="OpenSSF Best Practices" /></a>
  <a href="https://landscape.cncf.io/?item=orchestration-management--scheduling-orchestration--opensandbox"><img src="https://img.shields.io/badge/CNCF-Landscape-0C66E4?style=flat-square" alt="CNCF Landscape" /></a>
  <a href="https://discord.gg/g7FuPs8YeD"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat-square&logo=discord&logoColor=white" alt="Discord" /></a>
  <a href="https://qr.dingtalk.com/action/joingroup?code=v1,k1,A4Bgl5q1I1eNU/r33D18YFNrMY108aFF38V+r19RJOM=&_dt_no_comment=1&origin=11"><img src="https://img.shields.io/badge/DingTalk-Join-0089FF?style=flat-square" alt="DingTalk" /></a>
  <a href="https://github.com/opensandbox-group/OpenSandbox/actions"><img src="https://img.shields.io/github/actions/workflow/status/opensandbox-group/OpenSandbox/real-e2e.yml?branch=main&label=TEST&style=flat-square&logo=github&logoColor=white" alt="E2E Status" /></a>
  <a href="https://github.com/opensandbox-group/OpenSandbox/actions"><img src="https://img.shields.io/github/actions/workflow/status/opensandbox-group/OpenSandbox/kubernetes-nightly-build.yml?branch=main&label=K8S&style=flat-square&logo=kubernetes&logoColor=white" alt="Kubernetes nightly build status" /></a>
</p>

  <p align="center">
    <a href="https://trendshift.io/repositories/21828" target="_blank"><img src="https://trendshift.io/api/badge/repositories/21828" alt="opensandbox-group%2FOpenSandbox | Trendshift" style="width: 320px; height: 70px;" width="320" height="70" /></a>
  </p>

  <hr />
</div>

**Run AI agents in sandboxes on your own infrastructure.**

OpenSandbox gives AI applications isolated environments to execute code, run commands, manage files, and operate browsers or desktops. Start locally with Docker and deploy on Kubernetes through a unified sandbox API.

[Quick Start](#getting-started) · [Examples](#examples) · [Documentation](#documentation) · [Fast Sandbox](docs/architecture/fast-sandbox/index.md)

## Features

| Feature | What it enables | Learn more |
|---------|-----------------|------------|
| **Fast Sandbox runtime** | Fast, high-density sandboxes on Kubernetes. Firecracker creation: **64 ms P50 (serial)** / **125.4 ms P99 (10 concurrent)**. Firecracker sandboxes support pause/resume with memory and disk state preserved. | [Integration](docs/architecture/fast-sandbox/index.md) · [Performance](docs/architecture/fast-sandbox/performance.md) · [Pause/resume](docs/architecture/fast-sandbox/checkpoints.md) |
| **Agent working environments** | Execute commands, manage files, and run code with built-in APIs. Integration examples show how to run coding agents, browsers, and desktops inside sandboxes. | [Examples](docs/examples/index.md) |
| **Network access control** | Route inbound traffic through a unified ingress gateway and control outbound access with per-sandbox egress policies. | [Ingress](docs/architecture/network/ingress.md) · [Egress](docs/architecture/network/egress.md) |
| **Credential Vault** | Let agents call external services without exposing real credentials to sandbox workloads. | [Credential Vault](docs/guides/credential-vault.md) |
| **Local to cluster** | Start with Docker and deploy on Kubernetes through a unified lifecycle API. Resource pools and batch creation support agent evaluation and RL training workloads. | [Kubernetes runtime](docs/architecture/control-plane/operator.md) |
| **SDKs, CLI, and MCP** | Integrate with Python, Java/Kotlin, TypeScript/JavaScript, C#/.NET, or Go SDKs. Use `osb` from the terminal or connect agents through MCP. | [SDKs](#sdks) · [CLI](#cli) · [MCP](#mcp) |
| **Extensible sandbox protocol** | Build custom runtime integrations against defined sandbox lifecycle and execution APIs. | [API specs](specs/README.md) |

## Getting Started

Requirements:

- Docker (required for local execution)
- Python 3.10+ (required for examples and local runtime)

### Install and Configure the Sandbox Server

```bash
uvx opensandbox-server init-config ~/.sandbox.toml --example docker

uvx opensandbox-server

# Show help
# uvx opensandbox-server -h
```

### Create a Sandbox and Execute Commands/Scripts

Install the Sandbox SDK

```bash
uv pip install opensandbox
```

Create a sandbox from an `alpine` image and execute commands and scripts.

```python
import asyncio

from opensandbox import Sandbox
from opensandbox.models import WriteEntry

async def main() -> None:
    # 1. Create a sandbox from the alpine image
    sandbox = await Sandbox.create("alpine")

    try:
        # 2. Execute a shell command
        execution = await sandbox.commands.run("echo 'Hello OpenSandbox!'")
        print(execution.logs.stdout[0].text)

        # 3. Write a script file
        await sandbox.files.write_files([
            WriteEntry(
                path="/tmp/hello.sh",
                data="echo \"Hello $1\"\necho '2 + 2 =' $((2 + 2))",
                mode=755,
            )
        ])

        # 4. Read the file back
        content = await sandbox.files.read_file("/tmp/hello.sh")
        print(f"Content: {content}")

        # 5. Execute the script
        execution = await sandbox.commands.run("sh /tmp/hello.sh OpenSandbox")
        for log in execution.logs.stdout:
            print(log.text)

    finally:
        # 6. Cleanup the sandbox
        await sandbox.destroy()

if __name__ == "__main__":
    asyncio.run(main())
```

## Examples

Explore examples by what you want your agent to do. Runnable source code lives in [`examples/`](examples/).

| Use case | What you can build | Examples |
|----------|--------------------|----------|
| **Coding agents** | Run coding agents in isolated environments to edit files, execute commands, and complete development tasks. | [Claude Code](docs/examples/claude-code.md) · [Codex CLI](docs/examples/codex-cli.md) · [DeerFlow](docs/examples/deer-flow.md) |
| **Code execution and data analysis** | Execute model-generated code and work with results through the Code Interpreter SDK. | [Code Interpreter](docs/examples/code-interpreter.md) |
| **Browser and desktop automation** | Automate web interactions and testing, or give agents access to a desktop environment. | [Playwright](docs/examples/playwright.md) · [Chrome](docs/examples/chrome.md) · [Desktop](docs/examples/desktop.md) |
| **Agent evaluation** | Run evaluations with a separate sandbox for each trial. | [Harbor Evaluation](docs/examples/harbor-evaluation.md) |

See the [full example catalog](docs/examples/index.md) for more coding agents, framework integrations, remote development environments, Kubernetes deployment, and storage patterns.

## SDKs

Pick your language:

<details>
<summary><b>Python</b></summary>

```bash
pip install opensandbox
```

</details>

<details>
<summary><b>Java/Kotlin (Gradle Kotlin DSL)</b></summary>

```kotlin
dependencies {
    implementation("com.alibaba.opensandbox:sandbox:{latest_version}")
}
```

</details>

<details>
<summary><b>Java/Kotlin (Maven)</b></summary>

```xml
<dependency>
    <groupId>com.alibaba.opensandbox</groupId>
    <artifactId>sandbox</artifactId>
    <version>{latest_version}</version>
</dependency>
```

</details>

<details>
<summary><b>JavaScript/TypeScript</b></summary>

```bash
npm install @alibaba-group/opensandbox
```

</details>

<details>
<summary><b>C#/.NET</b></summary>

```bash
dotnet add package Alibaba.OpenSandbox
```

</details>

<details>
<summary><b>Go</b></summary>

```bash
go get github.com/alibaba/OpenSandbox/sdks/sandbox/go
```

</details>

## CLI

OpenSandbox also provides `osb`, a terminal CLI for the common sandbox workflow: create sandboxes, run commands, move files, inspect diagnostics, and manage runtime egress policy.

Install:

```bash
pip install opensandbox-cli
# or
uv tool install opensandbox-cli
```

Quick start:

```bash
osb config init
osb config set connection.domain localhost:8080
osb config set connection.protocol http
osb config set connection.api_key <your-api-key>
osb sandbox create --image python:3.12 --timeout 30m -o json
osb command run <sandbox-id> -o raw -- python -c "print(1 + 1)"
```

See the [CLI README](cli/README.md) for the full command reference.

## MCP

The OpenSandbox MCP server exposes sandbox creation, command execution, and text file operations to MCP-capable clients such as Claude Code and Cursor.

Install and run:

```bash
pip install opensandbox-mcp
opensandbox-mcp --domain localhost:8080 --protocol http
```

Minimal stdio config:

```json
{
  "mcpServers": {
    "opensandbox": {
      "command": "opensandbox-mcp",
      "args": ["--domain", "localhost:8080", "--protocol", "http"]
    }
  }
}
```

See the [MCP README](sdks/mcp/sandbox/python/README.md) for client-specific setup.

## Official Container Images

OpenSandbox release images are published under the same component name in three official registries:

- Docker Hub: `docker.io/opensandbox/<component>`
- GitHub Container Registry: `ghcr.io/opensandbox-group/opensandbox/<component>`
- Alibaba Cloud Container Registry: `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/<component>`

Tagged release images are signed keylessly with Cosign and include provenance attestations. Pin production images by digest and follow the [release verification guide](docs/community/release-verification.md) to verify the image against the OpenSandbox GitHub Actions identity before deployment.

## Documentation

- [Website](https://open-sandbox.ai) — Project homepage and documentation site
- [Architecture](docs/architecture/index.md) — System design and component responsibilities
- [Deployment Guide](docs/deployment/index.md) — Kubernetes installation and operations
- [Server Configuration](docs/getting-started/configuration.md) — Runtime and server settings
- [SDK Reference](docs/sdks/index.md) — Language guides and capability coverage
- [CLI Guide](cli/README.md) — Installation and command reference
- [MCP Integration](sdks/mcp/sandbox/python/README.md) — Setup for MCP-capable clients
- [API Reference](docs/api/index.md) — Sandbox lifecycle and execution contracts
- [Credential Vault](docs/guides/credential-vault.md) — Outbound credential injection
- [Release Verification](docs/community/release-verification.md) — Image signing and artifact verification
- [Enhancement Proposals](oseps/README.md) — Design proposals and technical direction
- [Roadmap](ROADMAP.md) — Project priorities and planning

## Contact and Discussion

- Issues: Submit bugs, feature requests, or design discussions through GitHub Issues
- Discord: Join the [OpenSandbox Discord community](https://discord.gg/g7FuPs8YeD)
- DingTalk: Join the [OpenSandbox technical discussion group](https://qr.dingtalk.com/action/joingroup?code=v1,k1,A4Bgl5q1I1eNU/r33D18YFNrMY108aFF38V+r19RJOM=&_dt_no_comment=1&origin=11)

## License

This project is open source under the [Apache 2.0 License](LICENSE).

---

### README (`docs/README.md`)

# OpenSandbox Docs Site

This directory hosts the VitePress site for OpenSandbox.

## Local development

```bash
nvm use 22
cd docs
pnpm install
pnpm docs:dev
```

## Build

```bash
nvm use 22
cd docs
pnpm install
pnpm docs:build
```

## Notes

- Site content is maintained directly under `docs/` — not auto-generated from monorepo READMEs.
- See `AGENTS.md` → "Documentation Rules" for content ownership and conventions.

---

### Index (`docs/index.md`)

---
layout: home

hero:
  name: OpenSandbox
  text: Universal Sandbox Infrastructure for AI Applications
  tagline: Securely run commands, code interpreters, browsers, and developer tools in isolated environments with multi-language SDKs.
  actions:
    - theme: brand
      text: Quick Start
      link: /getting-started/
    - theme: alt
      text: Architecture
      link: /architecture/
    - theme: alt
      text: SDKs
      link: /sdks/

features:
  - title: Sandbox Lifecycle Management
    details: Provision, monitor, renew, pause/resume, and terminate sandbox instances with Docker and Kubernetes runtimes.
  - title: Multi-Language SDKs
    details: Build with Python, Java/Kotlin, JavaScript/TypeScript, C#/.NET, and Go SDKs on top of standardized lifecycle and execution protocols.
  - title: In-Sandbox Execution
    details: Execute shell commands, manage files, run multi-language code interpreters, expose ports, and stream logs and metrics.
  - title: Built for AI Workloads
    details: Supports coding agents, browser automation, remote development, code execution, and reinforcement learning scenarios.
---

## Typical Scenarios

OpenSandbox is listed in the [CNCF Landscape](https://landscape.cncf.io/?item=orchestration-management--scheduling-orchestration--opensandbox).

<div class="scenario-grid">
  <a class="scenario-card" href="/examples/claude-code">
    <h3>Coding Agents</h3>
    <p>Run Claude Code, Gemini CLI, Codex, and other coding agents in isolated sandboxes.</p>
  </a>
  <a class="scenario-card" href="/examples/playwright">
    <h3>Browser Automation</h3>
    <p>Execute Chrome and Playwright workloads with controlled runtime, filesystem, and networking.</p>
  </a>
  <a class="scenario-card" href="/examples/vscode">
    <h3>Remote Development</h3>
    <p>Host VS Code Web and desktop-like environments for secure cloud development workflows.</p>
  </a>
  <a class="scenario-card" href="/examples/code-interpreter">
    <h3>AI Code Execution</h3>
    <p>Run model-generated code safely, stream outputs, and iterate quickly with reproducible environments.</p>
  </a>
</div>

Explore all examples in [Examples](/examples/).

## Quick Install

::: code-group

```bash [Python SDK]
pip install opensandbox
```

```bash [JavaScript SDK]
npm install @alibaba-group/opensandbox
```

```kotlin [Kotlin (Gradle)]
dependencies {
    implementation("com.alibaba.opensandbox:sandbox:{latest_version}")
}
```

```bash [Go SDK]
go get github.com/alibaba/OpenSandbox/sdks/sandbox/go
```

```bash [C# SDK]
dotnet add package Alibaba.OpenSandbox
```

:::

See [Getting Started](/getting-started/) for the full setup guide.

---

### Api Index (`docs/api/index.md`)

---
title: API Specifications
description: OpenAPI specification documents defining the complete API interfaces and data models for OpenSandbox.
---

# OpenSandbox API Specifications

This section contains the OpenAPI specification documents for the OpenSandbox project, defining the complete API interfaces and data models. Use the server base URLs defined in each spec (for example, `http://localhost:8080/v1` for the lifecycle API, `http://localhost:44772` for execd, and `http://localhost:18080` for egress) when constructing requests.

## Specification Files

Implementing an alternative backend or integrating a workload's own API? Read [Implementation compatibility](/api/implementation-compatibility) for the boundary between these contracts, the reference daemons, and current SDK expectations.

### 1. sandbox-lifecycle.yml

[OpenAPI source](https://github.com/opensandbox-group/OpenSandbox/blob/main/specs/sandbox-lifecycle.yml)

**Sandbox Lifecycle Management API**

Defines the complete lifecycle interfaces for creating, managing, and destroying sandbox environments from container images or snapshots.

**Core Features:**
- **Sandbox Management**: Create, list, query, and delete sandbox instances with metadata filters and pagination
- **State Control**: Pause and resume sandbox execution
- **Lifecycle States**: Supports transitions across Pending -> Running -> Pausing -> Paused -> Stopping -> Terminated, and error handling with `Failed`
- **Resource & Runtime Configuration**: Specify resource limits and optional Kubernetes `resourceRequests`, image startup `entrypoint`, `platform`, lifecycle hooks, optional `secureAccess`, volumes, environment variables, and opaque `extensions`
- **Image Support**: Create sandboxes from public or private registries, including registry auth
- **Timeout Management**: Optional `timeout` on creation (omit or set to `null` to disable automatic expiration) with explicit renewal via API
- **Endpoint Access**: Retrieve public access endpoints for services running inside sandboxes, including required headers when secured access is enabled; endpoint lookups report the sandbox origin via the `OPEN-SANDBOX-ORIGIN` response header (`template` for fsb golden-image sandboxes)
- **Template Management**: Create, list, inspect, and delete fsb golden-image templates; template builds are asynchronous (poll until `Succeeded`)
- **Snapshot Management**: Create snapshots from sandboxes, list snapshots with source/name filters, and delete snapshots

**Main Endpoints (base path `/v1`):**
- `POST /sandboxes` - Create a sandbox from an image, snapshot, or template with timeout and resource limits
- `GET /sandboxes` - List sandboxes with state/metadata filters and pagination
- `GET /sandboxes/{sandboxId}` - Get full sandbox details (including startup source and entrypoint)
- `DELETE /sandboxes/{sandboxId}` - Delete a sandbox
- `POST /sandboxes/{sandboxId}/snapshots` - Create a snapshot from a sandbox
- `GET /snapshots` - List snapshots with optional source sandbox, exact name, and state filtering plus pagination
- `GET /snapshots/{snapshotId}` - Get snapshot state and metadata
- `DELETE /snapshots/{snapshotId}` - Delete a snapshot
- `POST /sandboxes/{sandboxId}/pause` - Pause a sandbox (asynchronous); returns `409 Conflict` if it is already paused
- `POST /sandboxes/{sandboxId}/resume` - Resume a paused sandbox
- `POST /sandboxes/{sandboxId}/renew-expiration` - Set the absolute sandbox expiration time
- `PATCH /sandboxes/{sandboxId}/metadata` - Patch sandbox metadata (JSON Merge Patch, RFC 7396)
- `GET /sandboxes/{sandboxId}/endpoints/{port}` - Get an access endpoint and required headers; supports `use_server_proxy` and signed-endpoint `expires` parameters
- `GET/PUT/PATCH/DELETE /sandboxes/{sandboxId}/networkpolicy` - Inspect and manage the sandbox egress network policy (Fsb persists intent on the Sandbox CR; other backends proxy the sandbox-side egress service)
- `POST/GET/PATCH/DELETE /sandboxes/{sandboxId}/credential-vault` (+ sanitized `credentials` / `bindings` reads) - Manage the sandbox Credential Vault (fsb relays to the egress-held vault over FastPath; other backends proxy the sandbox-side egress service; values are write-only and never returned)
- `POST /templates` - Create a fsb template (asynchronous golden-image build)
- `GET /templates` - List templates with metadata filters and pagination
- `GET /templates/{templateId}` - Get template status and artifact references
- `DELETE /templates/{templateId}` - Delete a template

**Expiration updates:** `renew-expiration` sets `expiresAt` to the supplied future timestamp, which may be earlier than, equal to, or later than the current expiration. The configured `server.max_sandbox_timeout_seconds` applies only to `timeout` on creation; it does not cap expiration updates. See [Sandbox expiration](/getting-started/configuration#sandbox-expiration).

**Optional `Sandbox.allocation` response field:**
- Returned only when the runtime confirms the sandbox's current concrete Pool allocation.
- Omitted for unconfirmed allocations, non-Pool sandboxes, and allocations being released.
- This field is not a request echo, allocation history, or readiness signal, and does not expose Pod names or other Kubernetes-internal fields.

**Authentication:**
- HTTP Header: `OPEN-SANDBOX-API-KEY: your-api-key`
- Environment Variable: `OPEN_SANDBOX_API_KEY` (for SDK clients)

### 2. diagnostic-api.yml {#diagnostics}

[OpenAPI source](https://github.com/opensandbox-group/OpenSandbox/blob/main/specs/diagnostic-api.yml)

**Sandbox Diagnostics API**

Defines structured routing context and best-effort troubleshooting descriptors for sandbox diagnostic logs and events. Context identifies the runtime backend and its diagnostic identifiers. Log and event descriptors either embed plain-text content inline or return a download URL. This spec does not define a structured audit or observability model.

**Main Endpoints (base path `/v1`):**
- `GET /sandboxes/{sandboxId}/diagnostics/context` - Retrieve the runtime backend and backend-defined diagnostic identifiers
- `GET /sandboxes/{sandboxId}/diagnostics/logs` - Retrieve a diagnostic log content descriptor; `scope` is required
- `GET /sandboxes/{sandboxId}/diagnostics/events` - Retrieve a diagnostic event content descriptor; `scope` is required

**Authentication:**
- HTTP Header: `OPEN-SANDBOX-API-KEY: your-api-key`
- Environment Variable: `OPEN_SANDBOX_API_KEY` (for SDK clients)

`scope` is required. Docker and Kubernetes support `container`/`all` for logs and `runtime`/`all` for events. Fast Sandbox supports `runtime`/`all` events; log collection is not implemented there. Unsupported scopes return `DIAGNOSTICS_SCOPE_UNSUPPORTED`.

Context does not take a scope and is available independently of execd readiness.
The built-in server returns `docker` with container identifiers,
`kubernetes` with namespace/workload identifiers and an allocated Pod name when
available, or `fast-sandbox` with an empty identifier map. Kubernetes context
can be queried before a Pod is allocated. Unknown sandboxes return 404;
backends without context support return 501 `DIAGNOSTICS_NOT_IMPLEMENTED`.

Responses use `delivery: inline` with `content`, or `delivery: url` with
`contentUrl` and an optional expiry. The SDK does not download URL content;
inspect `truncated` and `warnings` before treating results as complete.
Python models and CLI JSON/YAML use snake_case fields such as `content_url`.
CLI raw output prints the content or URL without following it.

See [SDK diagnostics](/sdks/#diagnostics) for language support and [CLI diagnostics](/cli/#collect-diagnostics) for command examples.

### 3. execd-api.yaml

[OpenAPI source](https://github.com/opensandbox-group/OpenSandbox/blob/main/specs/execd-api.yaml)

**Code Execution API Inside Sandbox**

Defines interfaces for executing code, commands, and file operations within sandbox environments, providing complete code interpreter and filesystem management capabilities. Forward the headers returned by lifecycle endpoint resolution, including `X-EXECD-ACCESS-TOKEN` when required. Do not hard-code an execd address or assume the lifecycle API key alone authorizes direct sandbox access.

**Core Features:**
- **Code Execution**: Stateful code execution supporting Python, JavaScript, and other languages with context lifecycle management
- **Command Execution**: Shell command execution with foreground/background modes and polling endpoints for status/output
- **File Operations**: Complete CRUD operations for files and directories
- **Real-time Streaming**: Real-time output streaming via SSE (Server-Sent Events)
- **System Monitoring**: Real-time monitoring of CPU and memory metrics
- **Access Control**: Token-based API authentication via `X-EXECD-ACCESS-TOKEN`

**Main Endpoint Categories:**

**Health Check:**
- `GET /ping` - Service health check

**Code Interpreter:**
- `GET /code/contexts` - List active code execution contexts (filterable by language)
- `DELETE /code/contexts` - Delete all contexts for a language
- `DELETE /code/contexts/{context_id}` - Delete a specific context
- `POST /code/context` - Create a code execution context
- `POST /code` - Execute code in a context (streaming output)
- `DELETE /code` - Interrupt code execution

**Command Execution:**
- `POST /command` - Execute shell command (streaming output)
- `DELETE /command` - Interrupt command execution
- `GET /command/status/{id}` - Get foreground/background command status
- `GET /command/{id}/logs` - Fetch accumulated stdout/stderr for a background command

**Bash Session:**
- `POST /session` - Create a bash session
- `POST /session/{sessionId}/run` - Run command in a bash session (streaming output)
- `DELETE /session/{sessionId}` - Delete a bash session

**Filesystem:**
- `GET /files/info` - Get metadata for files
- `DELETE /files` - Delete files (not directories)
- `POST /files/permissions` - Change file permissions
- `POST /files/mv` - Move/rename files
- `GET /files/search` - Search files (supports glob patterns)
- `POST /files/replace` - Batch replace file content
- `POST /files/upload` - Upload files (multipart)
- `GET /files/download` - Download files (supports range requests)

**Directory Operations:**
- `GET /directories/list` - List directory contents with optional depth control
- `POST /directories` - Create directories with permissions (mkdir -p semantics)
- `DELETE /directories` - Recursively delete directories

**System Metrics:**
- `GET /metrics` - Get system resource metrics
- `GET /metrics/watch` - Watch system metrics in real-time (SSE stream)

**Isolated Execution (base path `/v1/isolated`):**
- `POST /session` - Create an isolated bash session
- `GET /sessions` - List isolated sessions
- `GET /capabilities` - Get isolator capabilities
- `GET /session/{sessionId}` - Get isolated session state
- `DELETE /session/{sessionId}` - Delete an isolated session
- `POST /session/{sessionId}/run` - Run a command in an isolated session (foreground SSE or background execution)
- `GET/DELETE /session/{sessionId}/runs/{runId}` - Get status or interrupt an isolated run
- `GET /session/{sessionId}/runs/{runId}/logs` - Retrieve isolated run logs
- `GET /session/{sessionId}/files/info` - Get file information
- `GET /session/{sessionId}/files/download` - Download a file
- `POST /session/{sessionId}/files/upload` - Upload a file
- `DELETE /session/{sessionId}/files` - Remove files
- `POST /session/{sessionId}/files/mv` - Rename or move files
- `POST /session/{sessionId}/files/permissions` - Change file permissions
- `POST /session/{sessionId}/files/replace` - Replace file content
- `GET /session/{sessionId}/files/search` - Search files
- `GET /session/{sessionId}/directories/list` - List directory contents
- `POST /session/{sessionId}/directories` - Create directories
- `DELETE /session/{sessionId}/directories` - Delete directories

`GET /session/{sessionId}/diff` and `POST /session/{sessionId}/commit` appear in the contract but are not implemented in the current execd: both return `503` with a not-supported error, and capabilities report `diff_supported: false` and `commit_supported: false`. Check `/v1/isolated/capabilities` before using runtime-dependent isolation features.

### 4. egress-api.yaml

[OpenAPI source](https://github.com/opensandbox-group/OpenSandbox/blob/main/specs/egress-api.yaml)

**Sandbox Egress Runtime API**

Defines the runtime egress policy interface exposed directly by the [egress sidecar](/architecture/network/egress) inside a sandbox. Unlike lifecycle operations, this API is reached by first resolving the sandbox endpoint for the egress port and then calling the sidecar endpoint directly.

**Core Features:**
- **Policy Inspection**: Retrieve the currently enforced egress policy and derived runtime mode
- **Policy Mutation**: Patch egress rules at runtime using sidecar merge semantics
- **Direct Sidecar Access**: Access via sandbox endpoint resolution instead of server-side lifecycle forwarding
- **Optional Sidecar Auth**: Supports endpoint-specific headers when the egress sidecar requires auth

**Main Endpoints:**
- `GET /policy` - Get the current egress policy
- `PATCH /policy` - Merge new egress rules into the current policy
- `DELETE /policy` - Remove specific egress rules from the current policy by target
- `POST/GET/PATCH/DELETE /credential-vault` - Create, inspect, mutate, or delete vault state
- `GET /credential-vault/credentials` and `GET /credential-vault/credentials/{credential_name}` - Read sanitized credential metadata
- `GET /credential-vault/bindings` and `GET /credential-vault/bindings/{binding_name}` - Read binding metadata

Credential values are write-only. Enable Credential Proxy and an egress policy before creating a vault. Template-backed (fsb) sandboxes use the server-routed `/sandboxes/{sandboxId}/credential-vault` endpoints above: the server relays revisions to the egress-held vault over the FastPath handler relay, the ack is synchronous, and values never appear on any read (see [Credential Vault](/guides/credential-vault)).

## Technical Features

### Streaming Output (Server-Sent Events)

Code execution and command execution interfaces use SSE for real-time streaming output, supporting the following event types:
- `init` - Initialization event
- `status` - Status update
- `stdout` / `stderr` - Standard output/error streams
- `result` - Execution result
- `execution_complete` - Execution completed
- `execution_count` - Execution count
- `error` - Error information

### Resource Limits

Supports flexible resource configuration (similar to Kubernetes):
```json
{
  "cpu": "500m",
  "memory": "512Mi",
  "gpu": "1"
}
```

### File Permissions

Supports Unix-style file permission management:
- Owner
- Group
- Permission mode values such as 644 or 755

---

### Architecture Index (`docs/architecture/index.md`)

---
title: Architecture
description: OpenSandbox architecture overview covering client SDKs, lifecycle control plane, runtime backends, and data-plane components.
---

# OpenSandbox Architecture

OpenSandbox is a general-purpose sandbox platform for AI applications. It provides client SDKs and tools, protocol definitions, a lifecycle control plane, Docker and Kubernetes runtime backends (supporting both container workloads and template-backed microVMs), and in-sandbox execution components for commands, files, code interpreters, browser automation, desktop environments, and training workloads.

This document describes the current repository architecture and the main boundaries between public contracts, server implementation, runtime providers, and sandbox data-plane components.

## Architecture Overview



The architecture distinguishes three primary interaction paths:

- **Lifecycle management** (orange arrows): Applications, agents, and operators send lifecycle requests (creating, listing, renewing, pausing, resuming, or deleting sandboxes, as well as managing snapshots and templates) to the FastAPI lifecycle server. The server persists metadata and delegates provisioning and orchestration to the configured runtime backend.
- **Execution and service access** (blue arrows): For command, file, PTY, and Jupyter code execution, or reaching user workload services, clients bypass lifecycle orchestration and connect directly to resolved endpoints—via direct host/pod addresses, the lifecycle server proxy, or an optional Kubernetes ingress gateway.
- **Outbound controls** (green arrows): Outbound requests from the sandbox to external APIs, model providers, or web services can be filtered and secured through egress controls (a per-sandbox sidecar for container workloads or a shared egress process per Fastlet pod for FastSandbox), with support for DNS/nftables policies and optional Credential Vault secret injection.

Runtime and networking capabilities depend on the selected backend; the diagram shows the shared architecture, not a requirement to deploy every component.

OpenSandbox is organized around six practical surfaces:

1. **Client surface** - SDKs, the `osb` CLI, and the MCP server used by applications, agents, and operators.
2. **Protocol surface** - OpenAPI contracts under `specs/` for lifecycle, diagnostics, in-sandbox execution, and egress policy.
3. **Lifecycle control plane** - the FastAPI server under `server/` that authenticates requests, validates config, persists server-managed records, and delegates lifecycle work to a configured runtime service.
4. **Runtime backends** - Docker for local and single-host deployments, and Kubernetes with container workload providers (BatchSandbox or `kubernetes-sigs/agent-sandbox`) alongside an optional FastSandbox integration for template-backed microVMs.
5. **Sandbox data plane** - the user workload and `execd` daemon inside a container or microVM, with optional Jupyter/code-interpreter support. Volume and egress integration depend on the runtime.
6. **Network and security plane** - endpoint resolution, server proxying, Kubernetes ingress gateway routing, secure endpoint access, egress policy enforcement, credential vault, resource limits, and secure container runtimes.

The split is intentional: SDKs and tools should depend on the public contracts, the server should own lifecycle orchestration, runtime providers should own platform-specific resource creation, and `execd`/egress should own operations that happen from inside the sandbox network and filesystem namespace.

The implementation is documented by architectural layer under `architecture/`:

| Layer | Responsibility | Documentation |
|---|---|---|
| Control plane | Lifecycle orchestration, workload orchestration, snapshots | [Server](/architecture/control-plane/server), [Kubernetes Controller](/architecture/control-plane/operator) |
| Data plane | In-sandbox execution, node-side collection | [Execd](/architecture/data-plane/execd), [Node Agent](/architecture/data-plane/node-agent) |
| Network | Inbound access, outbound policy | [Ingress](/architecture/network/ingress), [Egress](/architecture/network/egress), [Single-Host Network](/architecture/network/single-host-network) |
| Backend integration | Template-backed sandboxes on the fast-sandbox platform | [Fast Sandbox](/architecture/fast-sandbox/) |

## 1. Client Surface

The client surface is the developer-facing entry point for OpenSandbox.

### 1.1 Sandbox SDKs

The sandbox SDKs wrap lifecycle operations and in-sandbox operations behind language-native APIs:

- Python: `sdks/sandbox/python`
- JavaScript/TypeScript: `sdks/sandbox/javascript`
- Java/Kotlin: `sdks/sandbox/kotlin`
- C#/.NET: `sdks/sandbox/csharp`
- Go: `sdks/sandbox/go`

Common capabilities include:

- Create, list, inspect, pause, resume, renew, and delete sandboxes.
- Resolve service endpoints for sandbox ports.
- Execute commands with streamed output and background status/log polling.
- Manage files and directories.
- Read resource metrics from `execd`.
- Inspect or patch runtime egress policy when an egress sidecar is attached.

Generated OpenAPI clients live beside handwritten adapters. Generated code handles ordinary request/response APIs; handwritten layers cover SDK ergonomics, streaming, transport lifecycle, error mapping, and high-level models.

### 1.2 Code Interpreter SDKs

The code-interpreter SDKs build on the sandbox SDKs and `execd` code execution APIs. They manage code execution contexts and expose language-oriented code execution helpers.

The official code-interpreter image is maintained in [opensandbox-group/sandbox-images](https://github.com/opensandbox-group/sandbox-images) (previously located under `sandboxes/code-interpreter/`; see [Migration Reference](/reference/code-interpreter-image-migration)). It provides Python, Java, Node.js, and Go runtimes, and Jupyter kernels for Python, Java, TypeScript/JavaScript, Go, and Bash. Exact language versions are image-controlled and selected through environment variables such as `PYTHON_VERSION`, `JAVA_VERSION`, `NODE_VERSION`, and `GO_VERSION`.

### 1.3 CLI and MCP

The `osb` CLI under `cli/` is a terminal interface for day-to-day sandbox operations:

- `osb sandbox`: lifecycle and endpoint management
- `osb command`: command execution, background logs, and shell sessions
- `osb file`: file and directory operations
- `osb egress`: runtime egress policy inspection and mutation
- `osb devops`: low-level diagnostics
- `osb skills`: OpenSandbox-specific agent skill installation

The MCP server under `sdks/mcp/sandbox/python` exposes focused sandbox lifecycle, command, and text-file tools to MCP-capable clients such as Claude Code and Cursor.

## 2. Protocol Surface

OpenSandbox treats `specs/` as the public contract source of truth.

### 2.1 Lifecycle API

`specs/sandbox-lifecycle.yml` defines the lifecycle API, served by the server with the base path `/v1`.

Main resource groups:

- **Sandboxes**: create from an image, snapshot, or template; list, get, delete, pause, resume, renew expiration, and resolve port endpoints. FastSandbox also supports inspecting and managing runtime network policy through `GET/PUT/PATCH/DELETE /sandboxes/{sandboxId}/networkpolicy`, and reports the sandbox origin to clients via the `OPEN-SANDBOX-ORIGIN` response header on endpoint lookups.
- **Snapshots**: create a persistent snapshot from a sandbox, list snapshots, get snapshot state, and delete snapshots.
- **Templates**: create, list, inspect, and delete template definitions in the catalog used by FastSandbox (`/templates`).

Important request features:

- `image` or `snapshotId` for container startup, or `templateId` for the FastSandbox path in Kubernetes mode.
- `entrypoint`, environment variables, metadata, and opaque `extensions`.
- `resourceLimits` for CPU, memory, GPU, and future resource keys.
- `platform` constraints.
- `volumes` for host paths, platform-managed named volumes/PVCs, and OSSFS.
- `networkPolicy` for outbound network policy configuration.
- `secureAccess` for Kubernetes ingress gateway deployments that require endpoint credentials.

### 2.2 Diagnostics API

`specs/diagnostic-api.yml` defines best-effort diagnostic descriptors for sandbox logs and events. The server also exposes practical DevOps diagnostics routes that return plain text for operators and AI troubleshooting workflows.

### 2.3 Execd API

`specs/execd-api.yaml` defines the in-sandbox execution API exposed by `components/execd/`.

Main capabilities:

- Health check: `GET /ping`
- Code contexts and execution: `/code/contexts`, `/code/context`, `/code`
- Bash sessions: `/session`
- Commands: `/command`, command status, and background command logs
- Files and directories: `/files/*`, `/directories`
- Metrics: `/metrics`, `/metrics/watch`

Command and code execution use Server-Sent Events for streaming output. The current `execd` implementation also includes interactive PTY WebSocket endpoints under `/pty` for long-lived shell sessions.

### 2.4 Egress API

`specs/egress-api.yaml` defines the runtime policy and credential broker APIs exposed by `components/egress/`:

- **Policy**: `GET /policy` to inspect the current policy; `PATCH /policy` to mutate rules at runtime.
- **Credential Vault**: `GET`, `POST`, `PATCH`, and `DELETE` on `/credential-vault` to manage sandbox-local credential bindings and token injection without exposing raw secrets to the workload.

For container backends, callers resolve the sandbox endpoint for the egress sidecar port and connect directly (forwarding endpoint authentication headers when sidecar auth is enabled). For FastSandbox, runtime network policy is managed through the lifecycle API (`/sandboxes/{sandboxId}/networkpolicy`), while the shared egress process (one per Fastlet pod) routes policy and per-subject credential vault operations using internal request headers.

## 3. Lifecycle Control Plane

The lifecycle server under `server/` is a FastAPI application. It owns request validation, API-key authentication, server configuration, lifecycle orchestration, endpoint formatting, diagnostics, and server-managed persistence.

### 3.1 Server Structure

Key packages:

- `opensandbox_server/main.py`: app startup, middleware, router registration, runtime validation, and renew-intent startup.
- `opensandbox_server/api/`: lifecycle routes, proxy routes, pool routes, diagnostics routes, and request/response schemas.
- `opensandbox_server/services/`: lifecycle service interfaces and Docker/Kubernetes implementations.
- `opensandbox_server/services/k8s/`: Kubernetes workload providers, endpoint resolution, volume/egress helpers, informer support, and provider-specific mapping.
- `opensandbox_server/repositories/`: persistence adapters, used for server-managed snapshot records and the FastSandbox template catalog.
- `opensandbox_server/integrations/renew_intent/`: optional auto-renew-on-access integration.
- `opensandbox_server/middleware/`: API-key authentication and request ID middleware.

### 3.2 Runtime Service Selection

The server selects its lifecycle implementation based on `[runtime].type`:

- `docker` -> `DockerSandboxService`
- `kubernetes` -> `CompositeSandboxService`, which combines `KubernetesSandboxService` (backed by BatchSandbox or agent-sandbox providers) with `FastSandboxService` for template-based microVM execution.

The top-level `runtime.type` configuration option remains strictly `docker` or `kubernetes`. All implementations satisfy the same `SandboxService` interface, keeping API routes thin and delegating behavior to services. Under Kubernetes mode, `CompositeSandboxService` inspects creation requests: requests specifying a `templateId` dispatch to `FastSandboxService`, while container requests specifying an `image` or `snapshotId` dispatch to `KubernetesSandboxService`. Operations on existing sandboxes are routed by their ID prefix (IDs starting with `fsb-` route to `FastSandboxService`). Runtime-specific details stay behind the service boundary.

### 3.3 Server Persistence

The `[store]` configuration selects SQLite (the default, at `~/.opensandbox/opensandbox.db`) or PostgreSQL. The server persists snapshot metadata and the FastSandbox template catalog through separate repository adapters (`repositories/snapshots/` and `repositories/templates/`). The template catalog stored in the database is the public source of truth; Kubernetes `SandboxTemplate` resources drive golden-image builds and provide asynchronous status updates.

### 3.4 Server Proxy and Endpoint Resolution

The lifecycle endpoint API returns the reachable address for a service port inside a sandbox. Depending on runtime and configuration, the endpoint may be:

- A Docker host/bridge mapped endpoint.
- A Kubernetes ingress gateway endpoint.
- A server-proxied URL under `/sandboxes/{sandboxId}/proxy/{port}` when `use_server_proxy=true`.

Header-routed ingress endpoints include `OpenSandbox-Ingress-To` in their endpoint metadata. When the endpoint is rewritten to a server-proxied URL, the server removes only this ingress routing header and preserves other required endpoint headers.

The server proxy supports HTTP and WebSocket traffic and is also integrated with optional renew-on-access behavior. For HTTP responses, it strips hop-by-hop headers and the backend `Server` header while preserving an origin `Date`; the server adds a current `Date` only when the response does not already contain one. A root-relative `Location` value that starts with a single `/` is rebased under the same sandbox proxy route, while absolute URLs, network-path references (`//host/path`), and ordinary path-relative values are forwarded unchanged.

HTTP proxy responses preserve the sandbox service's status code, body, and `Content-Type`, including redirects and backend errors. The generated Server OpenAPI describes `200` and `default` responses with `*/*` and no fixed payload schema because the sandbox service controls the payload. Server-side validation and authentication still apply before forwarding; the explicit `422` validation response remains documented. These response declarations cover both root and `/v1` aliases, with and without a backend path, for every supported HTTP method.

## 4. Runtime Backends

### 4.1 Docker Runtime

The Docker runtime is the local and single-host backend. It talks directly to the Docker daemon and manages containers, timers, labels, volumes, ports, optional sidecars, and snapshots.

Core responsibilities:

- Pull public or private images, including per-request registry authentication.
- Create containers with CPU, memory, GPU, platform, capability, AppArmor, seccomp, PID, and secure-runtime settings.
- Stage the `execd` binary from `[runtime].execd_image` into the sandbox and install a bootstrap launcher before starting the user entrypoint.
- Support network modes `host`, `bridge`, `none`, `pasta`, `slirp4netns`, and custom user-defined networks. The user-mode modes (`pasta`, `slirp4netns`) expose no host-routable container IP and are reached through the published host ports.
- Allocate host ports for `execd` and user service endpoints in non-host network modes.
- Restore expiration timers for existing managed containers after server restart.
- Support host bind mounts, Docker named volumes via the `pvc` volume model, and OSSFS-backed mounts.
- Attach an egress sidecar when `networkPolicy` is requested and Docker networking is compatible.
- Create Docker-backed persistent snapshots as local images and restore sandboxes from those snapshot images.

Docker pause/resume uses container-level pause/resume. For sandboxes with an egress sidecar, pause freezes the sandbox container before the sidecar, while resume unfreezes the sidecar before the sandbox container. Docker snapshots are exposed through the public snapshot API.

### 4.2 Kubernetes Runtime

The Kubernetes runtime delegates actual workload creation to a workload provider selected by `kubernetes.workload_provider`.

Supported providers:

- `batchsandbox` - the default provider backed by OpenSandbox's Kubernetes controller and `BatchSandbox` CRD.
- `agent-sandbox` - a provider for `kubernetes-sigs/agent-sandbox`.

The Kubernetes server path handles:

- Kubernetes client initialization and informer-backed reads.
- Workload creation from image requests; `snapshotId` startup resolves to a stored restorable image when the snapshot record supports restore.
- Template merging for BatchSandbox and agent-sandbox manifests.
- Per-request image pull secrets where the provider supports them.
- Resource limits and GPU translation to Kubernetes extended resources.
- Platform constraints and RuntimeClass integration for secure runtimes.
- Volumes, egress sidecars, and secure endpoint access annotations.
- Endpoint resolution through direct workload data or ingress gateway configuration.
- Pause/resume delegation to providers.
- Plain-text diagnostics from Kubernetes resources.

### 4.3 BatchSandbox Controller

The Kubernetes controller under `kubernetes/` implements OpenSandbox-specific CRDs for high-throughput and pooled sandbox delivery:

- `BatchSandbox`: create one or many sandbox replicas from a pod template.
- `Pool`: maintain pre-warmed resources for fast allocation.
- `SandboxSnapshot`: snapshot records used by BatchSandbox pause/resume and the public snapshot workflow.

BatchSandbox supports both template-based creation and pool-based creation via `extensions.poolRef`. It also supports optional task orchestration for batch and RL-style workloads.

For supported single-replica BatchSandbox workloads, the rootfs pause/resume path commits the sandbox filesystem to an OCI image and releases runtime resources; resume recreates the workload from that image while preserving the sandbox ID. Workloads configured with a QEMU snapshot contract use a VM-state checkpoint and restore path instead.

The public snapshot API supports Docker and compatible `BatchSandbox` workloads. `DockerSnapshotRuntime` commits containers to local images, while `KubernetesSnapshotRuntime` creates a public `SandboxSnapshot` CR to commit and push images to an OCI registry. This is distinct from internal pause/resume, which releases compute while preserving sandbox continuity. Public snapshot support does not extend to every Kubernetes provider or VM runtime.

### 4.4 FastSandbox Integration

In Kubernetes mode, `FastSandboxService` connects OpenSandbox to the external `fast-sandbox` platform for template-backed microVM provisioning:

- **Routing**: `CompositeSandboxService` routes create requests with `templateId` to `FastSandboxService`, and routes operations on existing sandboxes by the `fsb-` ID prefix. FastSandbox is part of the Kubernetes composition, not a separate `runtime.type` value.
- **Control Protocol**: Communicates with an external `fast-sandbox` control plane via gRPC FastPath v2 (port 9090).
- **State**: FastPath owns mutations and live runtime operations; Kubernetes LIST/WATCH reads persisted `sandbox.fast.io/v1alpha2` resources and their observations.
- **Pause/resume**: Delegates to FastPath, which restores the checkpoint on resume. Clients must resolve endpoints again after restoration.
- **Networking**: The configured Fastlet pool supplies a shared egress process (one per Fastlet pod); the adapter sends network policy through FastPath Actions. This differs from attaching a per-sandbox container sidecar. The current server adapter rejects the `credentialProxy.enabled` creation flag and volume mounts on this path.

The adapter and endpoint integration live in this repository; FastPath and the microVM runtime are provided by the external platform. See the [service dispatch](https://github.com/opensandbox-group/OpenSandbox/blob/main/server/opensandbox_server/services/composite_service.py) and [FastSandbox adapter](https://github.com/opensandbox-group/OpenSandbox/blob/main/server/opensandbox_server/services/fast_sandbox/service.py) for the implementation boundaries, and the [Fast Sandbox integration](/architecture/fast-sandbox/) documentation for the full design.

## 5. Sandbox Data Plane

Each sandbox runs the user's image and entrypoint, with OpenSandbox control processes injected around it.

### 5.1 Execd

`components/execd/` is a Go daemon built with Gin. It runs inside the sandbox and exposes the execution API.

Responsibilities:

- Shell command execution with SSE streaming.
- Background command status and incremental log retrieval.
- Persistent bash sessions.
- Interactive PTY sessions over WebSocket.
- File and directory operations.
- Process isolation sessions for executing commands in isolated environments.
- Jupyter-backed code contexts and code execution.
- Local CPU/memory metrics and optional OpenTelemetry metrics export.
- Optional shared access token enforcement through `X-EXECD-ACCESS-TOKEN`.

In Docker, the server stages `execd` into the container and installs a bootstrap script. In Kubernetes BatchSandbox template mode, an init container copies `execd` and `bootstrap.sh` from the configured `execd_image` into an `emptyDir` volume mounted by the main sandbox container.

For FastSandbox, the template build includes `execd` in the guest image before the microVM starts.

### 5.2 Code Interpreter Runtime

The code-interpreter sandbox image starts Jupyter inside the sandbox. `execd` talks to Jupyter over HTTP/WebSocket and translates Jupyter kernel messages into OpenSandbox streaming events.

The Code Interpreter SDKs are optional high-level clients. The lower-level execution API remains available through the sandbox SDKs and direct `execd` clients.

### 5.3 Volumes

The lifecycle API exposes runtime-neutral volume models:

- `host`: bind a permitted host path.
- `pvc`: platform-managed named storage. Docker maps this to a Docker named volume; Kubernetes maps it to a PersistentVolumeClaim.
- `ossfs`: mount Alibaba Cloud OSS through the server/runtime integration.

Runtime providers validate and materialize these volume definitions differently, but the API shape stays shared. Volume mounts are supported for Docker and Kubernetes container workloads; the FastSandbox adapter currently rejects volume mounts.

### 5.4 Egress Sidecar

`components/egress/` enforces outbound network policy and provides credential proxying for sandboxes.

Capabilities:

- FQDN and wildcard-domain allow/deny rules.
- `dns` mode for DNS filtering.
- `dns+nft` mode for DNS plus nftables enforcement of resolved IPs and CIDR/IP rules where supported.
- Credential Vault / Proxy: transparent TLS MITM proxy (active in `dns+nft` mode) that provides scoped credential injection for outbound HTTPS requests based on host-managed secret bindings, keeping raw secrets out of the sandbox workload.
- Runtime policy inspection and patching through `/policy`.
- Optional sidecar authentication.
- Optional platform-enforced always-allow and always-deny overlays.

Docker starts the egress sidecar as a separate container and runs the main sandbox container in the sidecar network namespace. Kubernetes container providers append the egress sidecar to the pod spec and drop `NET_ADMIN` from the main sandbox container so only the sidecar mutates network rules. FastSandbox uses the shared egress process (one per Fastlet pod, outside the microVM guest), receiving policy actions through FastPath.

## 6. Networking and Access

### 6.1 Ingress

`components/ingress/` is a Kubernetes-oriented HTTP/WebSocket reverse proxy. It watches sandbox resources and routes traffic to sandbox ports.

Supported routing modes:

- Header mode: `OpenSandbox-Ingress-To: <sandbox-id>-<port>` or host parsing.
- URI mode: `/<sandbox-id>/<port>/<path>`.
- Wildcard host mode through server endpoint formatting.

For `BatchSandbox`, ingress reads endpoint data from the `sandbox.opensandbox.io/endpoints` annotation. For `agent-sandbox`, it reads `status.serviceFQDN`.

For FastSandbox routes, ingress asks FastPath to resolve the endpoint and forwards traffic through the returned Fastlet or central proxy address.

### 6.2 Secure Access

`secureAccess` is currently supported for Kubernetes sandboxes exposed through ingress gateway mode. When enabled, the server provisions endpoint credentials and returns required headers with endpoint responses. Signed route tokens are also supported when gateway secure-access signing keys are configured.

### 6.3 Auto-Renew on Access

The optional renew-intent integration extends sandbox TTL when access is observed. It can be triggered by server proxy requests or by ingress gateway events delivered through Redis. Per-sandbox opt-in is controlled by the `lifecycle.renewalPolicy` create field (OSEP-0009); the deprecated `extensions["access.renew.extend.seconds"]` key remains supported throughout 1.x.

## 7. Core Flows

### 7.1 Sandbox Creation

```text
Client / SDK / CLI / MCP
  -> POST /v1/sandboxes
  -> FastAPI lifecycle server validates request and config
  -> selected service creates a Docker container, Kubernetes workload, or FastSandbox microVM
  -> runtime stages execd or uses a prepared guest image, with backend-specific networking and storage
  -> sandbox reaches Running or reports Failed with status reason/message
```

Creation is asynchronous from the API perspective. Clients should poll `GET /v1/sandboxes/{sandboxId}` or use SDK readiness helpers.

### 7.2 Command, File, and Code Execution

```text
Client
  -> resolve execd endpoint from sandbox metadata or server proxy
  -> call execd API with X-EXECD-ACCESS-TOKEN when required
  -> execd runs command, file operation, session, PTY, or Jupyter code execution
  -> execd streams SSE/WebSocket output or returns structured responses
```

### 7.3 Service Exposure

```text
Client
  -> GET /v1/sandboxes/{sandboxId}/endpoints/{port}
  -> server returns Docker-mapped, ingress-gateway, or server-proxy endpoint
  -> client includes returned headers when secure access or sidecar auth requires them
  -> HTTP/WebSocket traffic reaches the target sandbox port
```

### 7.4 Egress Policy

```text
Create request with networkPolicy
  -> server validates runtime-specific network policy settings
  -> runtime attaches egress sidecar or binds policy actions to the shared Fastlet egress process
  -> sandbox outbound DNS/network traffic is filtered
  -> FastSandbox clients manage policy via GET/PUT/PATCH/DELETE /v1/sandboxes/{sandboxId}/networkpolicy
  -> container clients resolve the egress endpoint and PATCH /policy
```

### 7.5 Pause, Resume, and Snapshots

```text
Pause / resume
  -> lifecycle server delegates to runtime provider
  -> Docker pauses/resumes the sandbox container and its egress sidecar when present
  -> BatchSandbox uses rootfs commit/recreate or the configured QEMU VM-state restore path
  -> FastSandbox delegates pause/resume to external FastPath v2 service

Public snapshot API
  -> server persists snapshot metadata
  -> Docker runtime commits the sandbox to a local OCI image
  -> Kubernetes runtime (scoped to BatchSandbox) creates a public SandboxSnapshot CR that commits and pushes to an OCI registry
  -> create-from-snapshot resolves that image and starts a new sandbox
```

## 8. Design Principles

### Protocol First

Public behavior starts from OpenAPI contracts in `specs/`. SDKs and clients should align to those contracts, and generated outputs should be regenerated from source specs rather than patched as the only fix.

### Control Plane vs Data Plane

The lifecycle server should orchestrate and validate. Platform-specific provisioning belongs in runtime services/providers. In-sandbox operations belong in `execd` and egress sidecars.

### Runtime Neutral API, Runtime Specific Execution

The lifecycle API uses shared concepts such as resource limits, volumes, endpoints, network policy, and metadata. Docker and Kubernetes can materialize those concepts differently while preserving the API contract.

### Secure Defaults with Explicit Escape Hatches

The server supports API-key authentication, startup guardrails for unauthenticated mode, resource limits, capability drops, optional secure runtimes, egress controls, endpoint headers, and platform-specific network isolation. Less restrictive modes are intended for local development or explicit operator choice.

### Observable Failures

Sandbox state includes `state`, `reason`, `message`, and transition time. `execd` exposes metrics, the server exposes diagnostics, ingress/egress/execd support logs and OpenTelemetry metrics where implemented, and request IDs are propagated for debugging.

## 9. Common Use Cases

- **Coding agents**: run Claude Code, Gemini CLI, Codex CLI, Qwen Code, Kimi CLI, or other agent tools in isolated sandboxes.
- **AI code execution**: execute model-generated code with command/file/code-interpreter APIs and streamed feedback.
- **Browser automation**: run Chrome or Playwright with controlled filesystem and network behavior.
- **Remote development**: expose VS Code Web, desktops, VNC, or development servers through sandbox endpoints.
- **RL and evaluation workloads**: use Kubernetes BatchSandbox, Pool, and task orchestration for high-throughput sandbox delivery.
- **Enterprise isolation**: combine secure runtimes, ingress, egress, endpoint access headers, and Kubernetes deployment controls.

---

### Cli Index (`docs/cli/index.md`)

---
title: CLI
description: The osb command-line interface for managing sandboxes, executing commands, handling files, and installing agent skills.
---

# OpenSandbox CLI

`osb` is the command-line interface for OpenSandbox. It is built for the common day-to-day flows:

- create and manage sandboxes
- run commands inside a sandbox
- read and modify sandbox files
- inspect runtime egress policy
- manage sandbox-local Credential Vault state
- collect low-level diagnostics
- install OpenSandbox-specific skills for coding agents

It uses the OpenSandbox Python SDK under the hood and is intended to be the shortest path from a terminal to a working sandbox workflow.

## Install

::: code-group

```bash [pip]
pip install opensandbox-cli
```

```bash [uv]
uv tool install opensandbox-cli
```

```bash [pipx]
pipx install opensandbox-cli
```

:::

Confirm the install:

```bash
osb --help
osb --version
```

## Before You Start

Make sure an OpenSandbox server is reachable. If you are running locally, start the server first and then point the CLI at it.

```bash
opensandbox-server
```

## Quick Start

### 1. Initialize config

```bash
osb config init
osb config set connection.domain localhost:8080
osb config set connection.protocol http
osb config set connection.api_key <your-api-key>
osb config show -o json
```

If you want a non-default config file, choose it at the root command level for the whole invocation:

```bash
osb --config /tmp/dev.toml config init
osb --config /tmp/dev.toml config set connection.domain localhost:8080
osb --config /tmp/dev.toml config show -o json
```

### 2. Create a sandbox

```bash
osb sandbox create --image python:3.12 --timeout 30m -o json
```

If you set defaults first, later create commands can be shorter:

```bash
osb config set defaults.image python:3.12
osb config set defaults.timeout 30m
osb sandbox create -o json
```

### 3. Verify it is usable

```bash
osb sandbox get <sandbox-id> -o json
osb sandbox health <sandbox-id> -o json
```

### 4. Run a command inside the sandbox

Use `--` before the sandbox command payload.

```bash
osb command run <sandbox-id> -o raw --argv -- python -c "print(1 + 1)"
```

### 5. Read or write a file

```bash
osb file write <sandbox-id> /tmp/hello.txt -c "hello" -o json
osb file cat <sandbox-id> /tmp/hello.txt -o raw
```

### 6. Clean up

```bash
osb sandbox kill <sandbox-id> -o json
```

`kill` now returns `status: "deletion_requested"` instead of `terminated`; update scripts that match this value. It confirms request acceptance. To wait for deletion, see [cleanup completion](/architecture/control-plane/server#asynchronous-lifecycle-with-absolute-expiration).

## Common Tasks

### Create sandboxes

Basic:

```bash
osb sandbox create --image python:3.12
```

Private image:

```bash
osb sandbox create \
  --image my-registry.example.com/team/app:latest \
  --image-auth-username alice \
  --image-auth-password <token>
```

Manual cleanup mode:

```bash
osb sandbox create --image python:3.12 --timeout none
```

Explicit entrypoint argv:

```bash
osb sandbox create \
  --image python:3.12 \
  --entrypoint python \
  --entrypoint -m \
  --entrypoint http.server
```

Create with network policy and volumes:

```bash
osb sandbox create \
  --image python:3.12 \
  --network-policy-file network-policy.json \
  --volumes-file volumes.json
```

Create with Credential Vault proxy enabled:

```bash
osb sandbox create --image python:3.12 --network-policy-file network-policy.json --credential-proxy -o json
```

Create from a template (golden image):

```bash
osb sandbox create --template <template-id> --timeout 30m -o json
```

Template mode fixes the workload shape on the server: `--image`, `--env`, `--resource`, `--entrypoint`, `--volumes-file`, and `--credential-proxy` cannot be combined with `--template`. Only `--timeout` (required), `--metadata`, `--extension`, and `--network-policy-file` may accompany it. Only templates in the `Succeeded` phase can be used. See [Manage templates](#manage-templates).

Create from a snapshot:

```bash
osb sandbox create --snapshot-id <snapshot-id> --timeout 30m -o json
```

`--image`, `--template`, and `--snapshot-id` are mutually exclusive.

### Manage templates

Templates are golden-image builds; the build runs asynchronously, so poll `template get` until the status phase is `Succeeded`. Template management requires a Kubernetes-backed runtime.

```bash
osb template create --image python:3.12 --publish s3://bucket/publish -o json
osb template get <template-id> -o json
osb template list -o json
osb template delete <template-id> -o json
```

`template create` also accepts `--resource cpu=1 memory=512Mi disk=2Gi`, `--entrypoint` (repeat per argv item), `--env KEY=VALUE`, `--metadata KEY=VALUE`, `--format native|overlaybd`, `--readiness-probe`, and `--warmup-seconds`.

### Manage snapshots

Snapshots capture a sandbox's state and can back new sandboxes.

```bash
osb snapshot create <sandbox-id> --name golden -o json
osb snapshot get <snapshot-id> -o json
osb snapshot list --sandbox-id <sandbox-id> -o json
osb snapshot delete <snapshot-id> -o json
```

### List and inspect sandboxes

```bash
osb sandbox list
osb sandbox list -o json
osb sandbox list --state running --state paused
osb sandbox get <sandbox-id> -o json
osb sandbox metrics <sandbox-id>
osb sandbox metrics <sandbox-id> --watch -o raw
```

### Pause, resume, and renew

```bash
osb sandbox pause <sandbox-id> -o json
osb sandbox get <sandbox-id> -o json
# Repeat get with a deadline until status.state is Paused; stop on Failed.
# Only then resume:
osb sandbox resume <sandbox-id> --resume-timeout 60s -o json
osb sandbox renew <sandbox-id> --timeout 30m -o json
```

Pause is asynchronous. A successful pause response means the request was accepted. See [Pause and Resume](/guides/pause-resume) for runtime-specific behavior.

### Expose a service

```bash
osb sandbox endpoint <sandbox-id> --port 8080 -o json
```

Use the returned endpoint and headers together. The command resolves a route; it does not start a service or prove that the application is healthy.

### Run commands

Foreground streaming:

```bash
osb command run <sandbox-id> -o raw -- sh -lc 'echo ready'
```

Tracked background execution:

```bash
osb command run <sandbox-id> --background -o json -- sh -c "sleep 10; echo done"
osb command status <sandbox-id> <execution-id> -o json
osb command logs <sandbox-id> <execution-id> -o json
```

By default the CLI shell-quotes each argument and joins them into a command string. To use pipelines, redirection, or sandbox-side variable expansion, pass an explicit shell and script:

```bash
osb command run <sandbox-id> -o raw -- sh -c 'echo "$HOME"; printf "ready\n" | cat'
```

Use `--argv` for native execution without a shell. It requires execd support for the `argv` request field:

```bash
osb command run <sandbox-id> -o raw --argv -- python3 -c "import sys; print(sys.argv[1:])" "a b" '$HOME' "x'y" ""
osb command interrupt <sandbox-id> <execution-id> -o json
```

Foreground `command run` accepts `-o raw`; background execution accepts `table`, `json`, or `yaml`.

Background execution metadata is returned only after execd confirms startup. An execution error or a stream that ends before this confirmation makes the CLI exit non-zero without printing success metadata. A successful background start does not mean the process has finished; use `command status` and `command logs` to check its eventual result.

Persistent shell session:

```bash
osb command session create <sandbox-id> --workdir /workspace -o json
osb command session run <sandbox-id> <session-id> -o raw -- pwd
osb command session run <sandbox-id> <session-id> -o raw -- export FOO=bar
osb command session run <sandbox-id> <session-id> -o raw -- sh -c 'echo $FOO'
osb command session delete <sandbox-id> <session-id> -o json
```

### Open an interactive terminal

`osb terminal` attaches your current terminal to a real PTY inside the sandbox (colors, `stty`, and resize all behave like a local shell). Exit the remote shell (or press Ctrl-D) to leave; the remote exit code becomes the command's exit code.

```bash
osb terminal <sandbox-id>
osb terminal <sandbox-id> -w /workspace
```

Requires an interactive TTY, and is supported on Unix, macOS, and Linux. If the connection drops while the shell is still running, the session is kept server-side and its id is printed so it can be inspected or removed via the execd API; a clean exit tears the session down.

### Work with files

```bash
osb file upload <sandbox-id> ./local.txt /workspace/local.txt -o json
osb file download <sandbox-id> /workspace/result.json ./result.json -o json
osb file search <sandbox-id> /workspace --pattern "*.py" -o json
osb file info <sandbox-id> /workspace/main.py -o json
osb file replace <sandbox-id> /workspace/app.py --old old --new new -o json
osb file chmod <sandbox-id> /workspace/script.sh --mode 755 -o json
```

For regular files, `file download` replaces the local destination only after the entire download succeeds. If the download fails or you interrupt it, an existing file stays unchanged and temporary download files are removed. The destination directory must be writable so the CLI can stage the download before replacing the file.

Existing devices (such as `/dev/null`) and named pipes receive the download directly. They are not replaced, and bytes already written cannot be rolled back if the download fails or is interrupted.

Destinations that refer to standard output, such as `/dev/stdout` and `/dev/fd/1`, stream directly even when stdout is redirected to a regular file. These downloads omit the success message in all output formats so stdout contains only file bytes; errors still go to stderr. A failed or interrupted stream can contain partial data.

### Manage runtime egress policy

Inspect current policy:

```bash
osb egress get <sandbox-id> -o json
```

Patch specific rules:

```bash
osb egress patch <sandbox-id> --rule allow=pypi.org --rule deny=internal.example.com -o json
```

If you are debugging connectivity, verify behavior with an actual command:

```bash
osb command run <sandbox-id> -o raw -- curl -I https://pypi.org
```

### Manage Credential Vault

Credential Vault operations call the sandbox egress sidecar through the Python SDK. Create the sandbox with `--credential-proxy` and an explicit network policy before writing vault state. Template-backed sandboxes do not support Credential Vault. See [Credential Vault](/guides/credential-vault) for payload examples and revision checks.

```bash
osb credential-vault create <sandbox-id> --file vault.yaml -o json
osb credential-vault get <sandbox-id> -o json
osb credential-vault patch <sandbox-id> --file mutation.yaml -o json
osb credential-vault credential list <sandbox-id> -o json
osb credential-vault binding list <sandbox-id> -o json
osb credential-vault delete <sandbox-id> -o json
```

::: warning
Use `--file -` to read a JSON/YAML payload from stdin. Do not pass plaintext credential values as command-line flags; keep them in the payload stream or file.
:::

### Collect diagnostics

Use context to identify the runtime backend, then collect log and event descriptors.

```bash
osb diagnostics context <sandbox-id> -o json
osb diagnostics events <sandbox-id> --scope runtime -o raw
osb diagnostics events <sandbox-id> --scope all -o raw
osb diagnostics logs <sandbox-id> --scope container -o raw
osb diagnostics logs <sandbox-id> --scope all -o json
osb diagnostics events <sandbox-id> --scope runtime -o json
osb diagnostics logs <sandbox-id> --scope container -o yaml
```

`context` returns backend routing information without connecting to execd and
supports table, JSON, and YAML output. `--scope` is required for logs/events.
The built-in server supports
`container` and `all` for logs, and `runtime` and `all` for events. Docker/Kubernetes return
`DIAGNOSTICS_SCOPE_UNSUPPORTED` for unavailable scopes, including lifecycle events.
Best-effort scopes may include a `warnings` field when the backend can only
provide a subset. Raw output prints inline
diagnostic text, or the content URL when diagnostics are delivered as a
temporary URL; raw output does not download that URL. JSON/YAML use Python
model names such as `content_url`, `content_type`, and `expires_at` (the HTTP API
uses camelCase). See [Diagnostics](/api/#diagnostics) for runtime differences.
Older server builds may still return
`DIAGNOSTICS_NOT_IMPLEMENTED` for scoped diagnostics.

::: info
Legacy DevOps diagnostics remain experimental. Prefer `osb diagnostics logs/events` for stable API-backed log and event collection.
:::

```bash
osb devops inspect <sandbox-id> -o raw
osb devops summary <sandbox-id> -o raw
```

## Output Formats

Output selection is command-scoped, not global.

- `table`: human-readable tables and panels
- `json`: machine-readable JSON
- `yaml`: machine-readable YAML
- `raw`: unformatted text or streaming output

Examples:

```bash
osb sandbox list -o json
osb sandbox list -o yaml
osb file cat <sandbox-id> /workspace/hello.txt -o raw
```

Not every command supports every format. Use `--help` on the specific command when in doubt.

## Command Groups

The main command groups are:

- `osb sandbox`: lifecycle management
- `osb template`: golden-image template builds
- `osb snapshot`: snapshot management
- `osb command`: command execution and persistent sessions
- `osb terminal`: interactive PTY terminal into a sandbox
- `osb file`: file and directory operations
- `osb egress`: runtime egress policy
- `osb credential-vault`: sandbox-local credentials and bindings
- `osb diagnostics`: stable diagnostics logs and events
- `osb devops`: experimental legacy diagnostics
- `osb config`: local CLI configuration
- `osb skills`: bundled skills for AI tools

The CLI does not provide Client Pool or pool tracing commands; use the SDK/API for those workflows. A feature in the Python SDK is not automatically a CLI command.

Explore them directly:

```bash
osb sandbox --help
osb template --help
osb snapshot --help
osb command --help
osb file --help
osb skills --help
```

## Agent Skills

The CLI ships with built-in OpenSandbox skills for coding agents and agent-oriented tools.

Bundled skills:

- `sandbox-lifecycle`
- `command-execution`
- `file-operations`
- `network-egress`
- `sandbox-troubleshooting`

Supported targets:

| Target | Install location |
| --- | --- |
| `claude` | `./.claude/skills/<name>/SKILL.md` or `~/.claude/skills/<name>/SKILL.md` |
| `cursor` | `./.cursor/rules/` or `~/.cursor/rules/` |
| `codex` | `./.codex/skills/<name>/SKILL.md` or `~/.codex/skills/<name>/SKILL.md` |
| `copilot` | `./.github/copilot-instructions.md` or `~/.copilot/copilot-instructions.md` |
| `windsurf` | `./.windsurfrules` or `~/.windsurfrules` |
| `cline` | `./.clinerules` or `~/.cline/rules/opensandbox-<name>.md` |
| `opencode` | `./.agents/skills/<name>/SKILL.md` or `~/.agents/skills/<name>/SKILL.md` |

Common flows:

```bash
osb skills list
osb skills show sandbox-lifecycle
osb skills install sandbox-lifecycle --target codex --scope project
osb skills install --all-builtins --target codex --scope global
osb skills uninstall sandbox-troubleshooting --target claude --scope global
```

For scripts or agents, use structured output:

```bash
osb skills install sandbox-lifecycle --target codex --scope project -o json
```

## Configuration Model

The CLI resolves configuration in this order:

1. root CLI flags such as `--api-key`, `--domain`, `--protocol`, `--request-timeout`, `--config`
2. environment variables such as `OPEN_SANDBOX_API_KEY` and `OPEN_SANDBOX_DOMAIN`
3. config file, defaulting to `~/.opensandbox/config.toml`
4. SDK defaults

Config commands:

```bash
osb config init
osb config show
osb config set connection.domain localhost:8080
osb config set connection.protocol http
osb config set defaults.image python:3.12
osb config set defaults.timeout 30m
```

Example config file:

```toml
[connection]
api_key = "your-api-key"
domain = "localhost:8080"
protocol = "http"
request_timeout = 30
use_server_proxy = false

[output]
color = true

[defaults]
image = "python:3.12"
timeout = "30m"
```

## Development

For local development in this monorepo:

```bash
cd cli
uv sync
uv run osb --help
uv run pytest
```

This repository uses a local `uv` source override for the OpenSandbox Python SDK, so running from `cli/` will resolve against the checked-out SDK in the monorepo.

---

### Deployment Index (`docs/deployment/index.md`)

---
title: Kubernetes Deployment
description: Deploy OpenSandbox on Kubernetes — CRDs, controller, lifecycle server, and optional components, with Helm deployment order and configuration.
---

# Kubernetes Deployment

This guide covers deploying OpenSandbox on Kubernetes: the cluster foundation (CRDs), the controller, the lifecycle server, and the optional components (ingress gateway, node agent, fast-sandbox runtime).

Charts are versioned sources in the [OpenSandbox repository](https://github.com/opensandbox-group/OpenSandbox/tree/main/manifests/charts). They are installed from a checkout of a `release-X.Y.Z` tag — standalone chart packages are not published.

## Prerequisites

- Kubernetes 1.21.1+
- Helm 3.x
- `kubectl` configured for your cluster

## Deployment Order

Install the charts in this order:

```text
base → opensandbox-controller → fast-sandbox* → ingress-gateway → opensandbox-server → optional components
```

\* `fast-sandbox` is optional, but when used it must be installed **before** the ingress gateway and the server. `ingress-gateway` is required for Kubernetes deployments — sandbox Pods are ClusterIP-only, so client traffic routes through the gateway. It is installed **before the server** so the server announces it from the first install.

| Step | Chart | Why it comes here |
|------|-------|-------------------|
| 1 | `base` | Owns the `sandbox.opensandbox.io` CRDs, the fast-sandbox CRDs (`sandbox.fast.io`), and their RBAC. Everything else depends on these objects existing. Install once per cluster. |
| 2 | `opensandbox-controller` | Reconciles `BatchSandbox`, `Pool`, and `SandboxSnapshot` objects created by you and by the server. Requires the CRDs from `base`. |
| 3 | `fast-sandbox` (optional) | Firecracker runtime (`sandbox.fast.io`). Must be installed **before the server**: the server's `[runtime]`/fsb configuration points at the FastPath gRPC endpoint (`fast-sandbox-fastpath...svc:9090`) and watches `sandbox.fast.io` objects at startup. Also consumes the ServiceAccounts from `base` — keep the namespace values in sync with it. Skip if you only use the default Kubernetes runtime. |
| 4 | `ingress-gateway` | Required for Kubernetes deployments: sandbox Pods are ClusterIP-only and client traffic routes through the gateway. Installed before the server so the announcement is configured from the first install. When serving sandboxes through the fsb runtime, point `gateway.fastpathEndpoint` at the FastPath service from the previous step. |
| 5 | `opensandbox-server` | The lifecycle REST API that creates and deletes sandboxes. Requires the CRDs from `base`, a running controller, and — when serving sandboxes through the fsb runtime — the FastPath endpoint from the fast-sandbox release. Announces the ingress gateway through `server.gateway.*`. |
| 6 | `opensandbox-node-agent` (optional) | Node-level sandbox data collection. Order-independent. |

If you use the umbrella chart, this order is handled for you in a single release.

## Install

Check out the version you want to deploy:

```bash
git clone https://github.com/opensandbox-group/OpenSandbox.git
cd OpenSandbox
git checkout release-1.1.0   # or main for development
```

### Option 1: Umbrella chart (recommended)

One release installs every component with the correct ordering:

```bash
cd manifests/charts

# Package the sub-charts (charts/ is git-ignored, rebuilt every time)
helm dependency build opensandbox

helm install opensandbox opensandbox \
  --namespace opensandbox-system \
  --create-namespace
```

Optional components default to off. For Kubernetes deployments, enable the ingress gateway — sandbox Pods are ClusterIP-only and client traffic routes through it:

```bash
helm install opensandbox opensandbox \
  --namespace opensandbox-system \
  --create-namespace \
  --set ingress-gateway.enabled=true
```

### Option 2: Per-component releases

```bash
# 1. Cluster foundation: CRDs + RBAC
helm install base manifests/charts/base

# 2. Controller
helm install opensandbox-controller manifests/charts/controller \
  --namespace opensandbox-system \
  --create-namespace

# 3. Ingress gateway (required on Kubernetes; see Deployment Order)
helm install ingress-gateway manifests/charts/ingress-gateway \
  --namespace opensandbox-system

# 4. Lifecycle server
helm install opensandbox-server manifests/charts/server \
  --namespace opensandbox-system \
  --create-namespace
```

### Configure API authentication

By default, the server refuses to start without an API key in a non-interactive container. Create both the control-plane namespace and the default sandbox workload namespace, then store the key in a Kubernetes `Secret`:

```bash
kubectl create namespace opensandbox-system --dry-run=client -o yaml | kubectl apply -f -
kubectl create namespace opensandbox --dry-run=client -o yaml | kubectl apply -f -

read -s OPENSANDBOX_API_KEY
kubectl create secret generic opensandbox-api-key \
  --namespace opensandbox-system \
  --from-literal=api-key="${OPENSANDBOX_API_KEY}" \
  --dry-run=client -o yaml | kubectl apply -f -
unset OPENSANDBOX_API_KEY
```

Reference the Secret from a values file:

```yaml
# values-server.yaml
server:
  replicaCount: 1
  env:
    - name: OPENSANDBOX_SERVER_API_KEY
      valueFrom:
        secretKeyRef:
          name: opensandbox-api-key
          key: api-key
```

Use an external secret manager instead of creating the Secret manually in production environments.

The chart installs the server into `opensandbox-system`, while the default `configToml` creates sandbox and pool resources in `opensandbox`. If you change `[kubernetes].namespace` in `configToml`, create that namespace instead of `opensandbox` before submitting workloads.

::: warning Single-active Server default
The chart defaults to `server.replicaCount: 1`. Keep one active Lifecycle Server unless you deliberately use the PostgreSQL-backed Kubernetes public snapshot topology documented below. That exception coordinates public snapshots only; it does not provide general multi-replica Server HA. The Server Deployment uses the `Recreate` strategy so an upgrade stops the active Server before starting its replacement; expect a brief API interruption during upgrades.
:::

### Use PostgreSQL for server persistence

Create a Secret containing the PostgreSQL connection string:

```bash
read -s OPENSANDBOX_POSTGRESQL_DSN
kubectl create secret generic opensandbox-postgresql \
  --namespace opensandbox-system \
  --from-literal=dsn="${OPENSANDBOX_POSTGRESQL_DSN}" \
  --dry-run=client -o yaml | kubectl apply -f -
unset OPENSANDBOX_POSTGRESQL_DSN
```

In `values-server.yaml`, keep the default `server.replicaCount` at `1`, add the Secret-backed environment variable below, and add the shown `[store]` tables to the complete `configToml` value:

```yaml
server:
  replicaCount: 1
  env:
    - name: OPENSANDBOX_STORE_POSTGRESQL_DSN
      valueFrom:
        secretKeyRef:
          name: opensandbox-postgresql
          key: dsn

configToml: |
  # Keep the rest of the chart's complete server configuration here.
  [store]
  type = "postgresql"

  [store.postgresql]
  min_pool_size = 1
  max_pool_size = 10
  snapshot_recovery_interval_seconds = 15
```

::: info
The chart default remains one Server replica. You may explicitly set `server.replicaCount: 2` for multi-active public snapshot handling only when both replicas use the same PostgreSQL database and the Kubernetes runtime. SQLite and Docker snapshot execution do not support this multi-active topology.
:::

### Install the server with the API key and verify

Install the server referencing your values file:

```bash
helm install opensandbox-server manifests/charts/server \
  --namespace opensandbox-system \
  --create-namespace \
  --values values-server.yaml
```

Wait for the Deployment and verify the API health endpoint:

```bash
kubectl rollout status deployment/opensandbox-server \
  --namespace opensandbox-system \
  --timeout=180s

kubectl port-forward \
  --namespace opensandbox-system \
  service/opensandbox-server 8080:80
```

In another terminal:

```bash
curl --fail http://127.0.0.1:8080/health
```

### Important values

| Value | Purpose | Notes |
|-------|---------|-------|
| `server.image.repository` | Server image registry and repository | Override for a private mirror or custom build. |
| `server.image.tag` | Server image version | Defaults to the `release-<appVersion>` image published with the chart version; override only for custom builds. |
| `server.replicaCount` | Number of server Pods | Defaults to `1`; general multi-replica Server HA is not supported yet. |
| `server.env` | Additional container environment variables | Use it with `secretKeyRef` for `OPENSANDBOX_SERVER_API_KEY`. |
| `configToml` | Complete server configuration | Mounted at `/etc/opensandbox/config.toml`; overriding it replaces the complete default TOML, including the workload namespace. |
| `server.gateway.enabled` | Announce an ingress gateway to clients | Defaults to `false`. The gateway itself is deployed by the [ingress-gateway chart](https://github.com/opensandbox-group/OpenSandbox/tree/main/manifests/charts/ingress-gateway). |
| `server.service.type` | Service type for the server | Defaults to `ClusterIP`. Use `NodePort` or `LoadBalancer` for access from outside the cluster; pin the port with `server.service.nodePort`. |
| `namespaceOverride` | Namespace used by chart resources | Defaults to `opensandbox-system`. |

The server container and its Service use port `80`. Keep `[server].port = 80` when replacing `configToml` unless the chart templates are also updated to use a different port. The Service is `ClusterIP` by default; set `server.service.type` to reach the server from outside the cluster.

### Configure egress sidecar resources

When a create request includes `networkPolicy`, the lifecycle server adds an egress sidecar to each non-pooled sandbox Pod. Namespace `LimitRange` defaults apply to this container when it does not declare resources, which can reserve substantially more capacity than basic DNS/nft enforcement needs.

Add optional resource settings to the `[egress]` section of `configToml`:

```toml
[egress]
image = "opensandbox/egress:release-1.1.0"
requests = { cpu = "25m", memory = "64Mi" }
limits = { cpu = "250m", memory = "256Mi" }
```

You can omit either `requests` or `limits`. Treat these values as a starting point and tune them from observed usage; Credential Vault and transparent mitmproxy generally need more headroom than basic DNS/nft enforcement.

### Ingress gateway (required on Kubernetes)

Sandbox Pods on Kubernetes are ClusterIP-only — client traffic reaches sandboxes through the ingress gateway (`[ingress] mode = "gateway"`, the Kubernetes-oriented mode; the Docker runtime uses `direct` and does not need the gateway). Install the gateway **before the server**, then include the announcement in the server install:

```bash
helm install ingress-gateway manifests/charts/ingress-gateway \
  --namespace opensandbox-system \
  --set gateway.fastpathEndpoint=fast-sandbox-fastpath.opensandbox-system.svc:9090
```

Install the server with the announcement enabled (or `helm upgrade` an existing server release with the same flags):

```bash
helm install opensandbox-server manifests/charts/server \
  --namespace opensandbox-system \
  --set server.gateway.enabled=true \
  --set server.gateway.host=gateway.example.com \
  --values values-server.yaml
```

Keep `server.gateway.gatewayRouteMode` in sync with `gateway.gatewayRouteMode` of the gateway chart. For signed, expiring sandbox routes, configure the shared secure-access key ring on both charts — see [secure-access keys](https://github.com/opensandbox-group/OpenSandbox/blob/main/manifests/HELM-DEPLOYMENT.md#secure-access-keys-osep-0011).

### Optional: fast-sandbox runtime

The `fast-sandbox` chart adds the Firecracker (`sandbox.fast.io`) runtime. Install it **before the ingress gateway and the lifecycle server** (see [Deployment Order](#deployment-order)): the server's `[runtime]`/fsb configuration points at the FastPath gRPC endpoint this chart creates. It also requires `base` first, KVM-capable nodes, and companion images built from a pinned upstream commit — see the [fast-sandbox runtime deployment guide](https://github.com/opensandbox-group/OpenSandbox/blob/main/manifests/HELM-DEPLOYMENT.md#fast-sandbox-runtime-firecracker).

## Upgrade

Upgrade the umbrella release from a newer checkout:

```bash
git fetch --tags
git checkout release-1.2.0

cd manifests/charts
helm dependency build opensandbox
helm upgrade opensandbox opensandbox --namespace opensandbox-system
```

For a per-component server release:

```bash
git checkout release-1.2.0
helm upgrade opensandbox-server manifests/charts/server \
  --namespace opensandbox-system \
  --values values-server.yaml
```

For the complete values reference and local development installation, see the [`opensandbox-server` chart README](https://github.com/opensandbox-group/OpenSandbox/tree/main/manifests/charts/server).

## Operator Metrics

The operator (controller-manager) exposes standard [controller-runtime](https://book.kubebuilder.io/reference/metrics) Prometheus metrics — reconcile rate and latency (`controller_runtime_reconcile_*`), work-queue depth, client-go request counts, and Go runtime stats. The endpoint is **disabled by default** (`--metrics-bind-address=0`).

Enable it through the `opensandbox-controller` chart values:

| Value | Default | Purpose |
|-------|---------|---------|
| `controller.metrics.enabled` | `false` | Expose the `/metrics` endpoint (sets `--metrics-bind-address`) |
| `controller.metrics.port` | `8080` | Port for the metrics endpoint |
| `controller.metrics.secure` | `false` | Serve over HTTPS with authn/authz (`--metrics-secure`); set `false` for plain HTTP scraping |

```yaml
controller:
  metrics:
    enabled: true
    port: 8080
    secure: false   # plain HTTP, e.g. for a PodMonitoring/ServiceMonitor scrape
```

- With `secure: false` the endpoint is plain HTTP and can be scraped directly (no TLS or bearer token).
- With `secure: true` the controller-runtime filter authenticates and authorizes each scrape via `TokenReview`/`SubjectAccessReview`. The chart then provisions two `ClusterRole`s automatically:
  - `opensandbox-metrics-auth-role` (bound to the manager) — lets the controller run the auth checks.
  - `opensandbox-metrics-reader` (**not** bound by the chart) — grants `get` on the `/metrics` non-resource URL. Bind it to your scraper's `ServiceAccount` (e.g. Prometheus) and have the scraper present that account's bearer token.

Point your Prometheus stack at the `metrics` container port (for example via a `ServiceMonitor` or `PodMonitoring`).

### Business capacity metrics

The elected controller also exports low-cardinality business capacity metrics over OTLP/HTTP when `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` or `OTEL_EXPORTER_OTLP_ENDPOINT` is set. This is independent of the controller-runtime Prometheus endpoint and remains disabled when neither variable is configured.

```yaml
extraEnv:
  - name: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
    value: http://otel-collector.observability:4318/v1/metrics
```

| Metric | Unit | Attributes | Description |
|--------|------|------------|-------------|
| `controller.pool.pods` | `{pod}` | `namespace`, `pool_name`, `state` | Current Pool Pods, where `state` is `total`, `allocated`, `available`, or `updated` |
| `controller.pool.cpu.requested` | `{cpu}` | `namespace`, `pool_name`, `state` | Scheduler-equivalent CPU requests represented by total, allocated, or available Pool Pods |
| `controller.pool.memory.requested` | `By` | `namespace`, `pool_name`, `state` | Scheduler-equivalent memory requests represented by total, allocated, or available Pool Pods |
| `controller.batchsandbox.count` | `{batchsandbox}` | `namespace`, `phase`, `allocation_mode` | Current BatchSandbox objects by lifecycle phase and pool/direct mode |
| `controller.batchsandbox.pods` | `{pod}` | `namespace`, `state`, `allocation_mode` | Desired, current, allocated, and ready BatchSandbox Pod counts |
| `controller.capacity.collect.duration` | `s` | None | Time spent reading cached objects and collecting one capacity snapshot |

The metrics deliberately omit sandbox, BatchSandbox, and Pod identifiers. Only the leader exports them, so multiple controller replicas do not duplicate cluster totals. An unset initial BatchSandbox phase is exported as `Unknown`. Derive Pool utilization from `allocated / total` and calculate peak, valley, or percentile capacity in the telemetry backend. Actual CPU and memory usage remains available from kubelet/cAdvisor rather than being duplicated here.

Each collection reads all Pools and BatchSandboxes from the controller manager's informer cache, then performs one cached, owner-UID-indexed Pod list for every non-deleting Pool. Collection CPU and memory therefore grow linearly with the number of cached Pools, BatchSandboxes, and Pool-owned Pods, without issuing one API-server list request per Pool. The OpenTelemetry periodic reader exports every 60 seconds by default; `OTEL_METRIC_EXPORT_INTERVAL` can change the interval in milliseconds. Monitor `controller.capacity.collect.duration` and validate the target cluster scale before shortening that interval. If OTLP setup fails after an endpoint is configured, the controller continues reconciling and logs the failed setup stage together with the endpoint environment variable and a credential-stripped endpoint.

## Configure the Server for Kubernetes

Generate a Kubernetes-oriented server config:

```bash
opensandbox-server init-config ~/.sandbox.toml --example k8s
```

Key Kubernetes-specific configuration sections:

| Section | Purpose |
|---------|---------|
| `[kubernetes]` | Workload provider, BatchSandbox template file |
| `[agent_sandbox]` | Agent sandbox settings |
| `[ingress]` | Ingress gateway for sandbox traffic routing |
| `[secure_runtime]` | Secure container runtime (gVisor, Kata) |

See [Configuration](/getting-started/configuration) for the full reference.

## Components on Kubernetes

| Component | Deployment | Purpose |
|-----------|-----------|---------|
| CRDs + RBAC (`base`) | Cluster-scoped | `BatchSandbox`, `Pool`, `SandboxSnapshot` and `sandbox.fast.io` API types |
| Server | Deployment | Lifecycle control plane |
| Controller (operator) | Deployment | Manages BatchSandbox/Pool CRDs |
| Ingress gateway | Deployment | Routes traffic to sandboxes; required on Kubernetes deployments |
| Egress | Sidecar | Per-sandbox egress policy enforcement |
| Execd | Built into sandbox images | In-sandbox execution |
| Node agent | DaemonSet | Optional node-level sandbox data collection |
| fast-sandbox | Deployment + DaemonSet | Optional Firecracker control plane and node runtime |

## Uninstall

```bash
helm uninstall opensandbox -n opensandbox-system
```

CRDs carry the `helm.sh/resource-policy: keep` annotation and are retained across uninstalls; the `opensandbox-dataplane` namespace (created by `base` for fast-sandbox) is kept as well. Delete them manually once their data is no longer needed:

```bash
kubectl delete crd batchsandboxes.sandbox.opensandbox.io \
  pools.sandbox.opensandbox.io \
  sandboxsnapshots.sandbox.opensandbox.io
kubectl delete namespace opensandbox-dataplane
```

## Related

- [Kubernetes Overview](/architecture/control-plane/operator) — Operator features and CRDs
- [Pause & Resume](/guides/pause-resume) — Snapshot-based pause/resume on Kubernetes
- [Secure Container](/guides/secure-container) — gVisor and Kata on Kubernetes
- [Network Isolation on Kubernetes](/guides/network-isolation) — Egress policy design for Kubernetes
- [Helm charts source](https://github.com/opensandbox-group/OpenSandbox/tree/main/manifests/charts) — Chart sources and values reference

---

### Examples Index (`docs/examples/index.md`)

---
title: Examples
description: Real-world usage examples for OpenSandbox covering coding agents, browser automation, desktop environments, and training workloads.
---

# Examples

OpenSandbox provides ready-to-run examples covering SDK usage, agent integrations, browser automation, and training workloads.

::: tip
All example source code is available in the [`examples/`](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples) directory on GitHub.
:::

## Coding Agents

Run coding CLIs and AI agent frameworks inside isolated sandboxes.

| Example | Description |
|---------|-------------|
| [Claude Code](/examples/claude-code) | Run Claude Code CLI in a sandbox |
| [Gemini CLI](/examples/gemini-cli) | Run Gemini CLI in a sandbox |
| [Codex CLI](/examples/codex-cli) | Run OpenAI Codex CLI in a sandbox |
| [Apply Model Patches](/examples/apply-patch) | Apply Codex-style file patches with a custom sandbox image |
| [OpenCode](/examples/opencode) | Run the OpenCode coding agent in a sandbox |
| [Qwen Code](/examples/qwen-code) | Run Qwen Code CLI in a sandbox |
| [Kimi CLI](/examples/kimi-cli) | Run Kimi CLI (Moonshot AI) in a sandbox |
| [LangGraph](/examples/langgraph) | LangGraph state-machine workflow with sandbox |
| [Deep Agents](/examples/deep-agents) | Deep Agents file/shell tools running in a sandbox |
| [Google ADK](/examples/google-adk) | Google ADK agent using OpenSandbox tools |
| [OpenClaw](/examples/openclaw) | OpenClaw Gateway inside a sandbox |
| [DeerFlow](/examples/deer-flow) | DeerFlow agent turns with tools running in a sandbox |

## Browser & Desktop

Execute browser workloads and host desktop environments in sandboxes.

| Example | Description |
|---------|-------------|
| [Chrome](/examples/chrome) | Chromium sandbox with VNC and DevTools |
| [Playwright](/examples/playwright) | Playwright + Chromium headless testing |
| [Desktop](/examples/desktop) | Full desktop environment with VNC |
| [VS Code](/examples/vscode) | VS Code Web (code-server) in a sandbox |

## Core Usage

Fundamental sandbox operations and SDK workflows.

| Example | Description |
|---------|-------------|
| [Code Interpreter](/examples/code-interpreter) | End-to-end Code Interpreter SDK workflow |
| [Scheduled Client Pool](/examples/client-pool-schedule) | Adjust idle capacity by business hours using Go's pool Resize API |
| [AIO Sandbox](/examples/aio-sandbox) | All-in-One sandbox setup |
| [Agent Sandbox](/examples/agent-sandbox) | Kubernetes agent-sandbox integration |
| [AKS + Kata](/examples/aks-kata) | AKS deployment with Kata VM isolation, ingress, egress, and Credential Vault |
| [Windows](/examples/windows) | Windows sandbox via KVM/QEMU |
| [Harbor Evaluation](/examples/harbor-evaluation) | Run a Harbor agent evaluation, one sandbox per trial |

## Storage

Persistent and shared storage patterns for sandboxes.

| Example | Description |
|---------|-------------|
| [Host Volume Mount](/examples/host-volume-mount) | Mount host directories into sandboxes |
| [Docker PVC Volume](/examples/docker-pvc-volume-mount) | Docker named volume mounts |
| [Docker OSSFS Volume](/examples/docker-ossfs-volume-mount) | Docker OSSFS (OSS FUSE) mounts |
| [rclone Volume Mount](/examples/rclone-volume-mount) | Remote storage through external Docker plugins or Kubernetes CSI volumes |
| [Kubernetes PVC](/examples/kubernetes-pvc-volume-mount) | Kubernetes PersistentVolumeClaim mounts |

## How to Run

1. Start the OpenSandbox server (see [Quick Start](/getting-started/))
2. Set environment variables: `export SANDBOX_DOMAIN=...`, `export SANDBOX_API_KEY=...`
3. Add provider-specific variables as needed (e.g., `ANTHROPIC_AUTH_TOKEN`, `OPENAI_API_KEY`)
4. Navigate to the example directory and run: `python main.py`

::: tip
Most examples include a `main.py` entry point. Follow each example's run instructions for other languages. Some also include a `Dockerfile` for containerized execution.
:::

---

### Getting Started Index (`docs/getting-started/index.md`)

---
title: Quick Start
description: Get OpenSandbox running locally in minutes with Docker and Python.
---

# Quick Start

OpenSandbox is a general-purpose sandbox platform for AI applications, offering multi-language SDKs, unified sandbox APIs, and Docker/Kubernetes runtimes.

## Prerequisites

- **Docker** Engine 20.10+ (for local execution)
- **Python** 3.10+ (for the server and Python SDK)
- **uv** (recommended) or pip

## 1. Start the Server

```bash
# Generate a starter config
uvx opensandbox-server init-config ~/.sandbox.toml --example docker

# Start the server
uvx opensandbox-server
```

Verify the server is running:

```bash
curl http://127.0.0.1:8080/health
# → {"status": "healthy"}
```

## 2. Install an SDK

::: code-group

```bash [Python]
pip install opensandbox
```

```bash [JavaScript]
npm install @alibaba-group/opensandbox
```

```bash [Go]
go get github.com/alibaba/OpenSandbox/sdks/sandbox/go
```

```bash [C#]
dotnet add package Alibaba.OpenSandbox
```

:::

For Kotlin/Java, see [Installation](/getting-started/installation).

## 3. Create and Use a Sandbox

This example uses the sandbox SDK and a standard Python image. If the server has an API key configured, set `OPEN_SANDBOX_API_KEY` in your client environment.

```python
import asyncio
from datetime import timedelta

from opensandbox import Sandbox
from opensandbox.config import ConnectionConfig
from opensandbox.models import WriteEntry


async def main() -> None:
    sandbox = await Sandbox.create(
        "python:3.12",
        timeout=timedelta(minutes=10),
        connection_config=ConnectionConfig(
            domain="localhost:8080",
            use_server_proxy=True,
        ),
    )
    try:
        execution = await sandbox.commands.run("python -c 'print(1 + 1)'")
        for output in execution.logs.stdout:
            print(output.text)

        await sandbox.files.write_files([
            WriteEntry(path="/tmp/hello.txt", data="Hello World", mode=644)
        ])
        print(await sandbox.files.read_file("/tmp/hello.txt"))
    finally:
        await sandbox.destroy()


if __name__ == "__main__":
    asyncio.run(main())
```

`use_server_proxy=True` routes sandbox traffic through the lifecycle server, which is useful when the client cannot reach container addresses directly. `destroy()` terminates the remote sandbox and closes local SDK resources. Closing the client alone does not terminate the remote sandbox.

## 4. Try the CLI

```bash
pip install opensandbox-cli

osb config init
osb config set connection.domain localhost:8080
osb config set connection.protocol http
osb config set connection.use_server_proxy true
osb sandbox create --image python:3.12 --timeout 30m -o json
osb command run <sandbox-id> -o raw --argv -- python -c "print(1 + 1)"
osb sandbox kill <sandbox-id> -o json
```

## Next Steps

- [Installation](/getting-started/installation) — Detailed installation for all SDKs and runtimes
- [Configuration](/getting-started/configuration) — Server configuration reference
- [Architecture](/architecture/) — How OpenSandbox works under the hood
- [Guides](/guides/) — Client Pool, tracing, diagnostics, and security features
- [SDK capability matrix](/sdks/#capability-coverage) — Check support before choosing a language
- [Examples](/examples/) — Real-world usage examples

---

### Guides Index (`docs/guides/index.md`)

---
title: Feature Guides
description: Guides to sandbox lifecycle, isolation, networking, and security.
---

# Feature Guides

Use these guides to add capabilities to a working [sandbox application](/getting-started/). Check the [SDK capability matrix](/sdks/#capability-coverage) for language support.

## Lifecycle and security

- [Pause and resume](/guides/pause-resume)
- [Lifecycle hooks](/guides/lifecycle-hooks)
- [Isolation sessions](/guides/isolation-sessions)
- [Credential Vault](/guides/credential-vault)
- [Chained upstream proxy](/guides/egress-upstream-proxy)
- [Secure access](/guides/secure-access)
- [Secure container runtimes](/guides/secure-container)
- [Network isolation on Kubernetes](/guides/network-isolation)
- [Multi-tenancy](/guides/multi-tenancy)
- [Windows sandboxes](/guides/windows-sandbox)

## Operations

- [Telemetry](/guides/telemetry)

---

### Sdks Index (`docs/sdks/index.md`)

---
title: SDKs
description: Choose an OpenSandbox SDK and compare lifecycle, Client Pool, tracing, and diagnostics support.
---

# SDKs

OpenSandbox provides five sandbox SDKs for lifecycle management, command execution, file operations, and resource metrics. This site follows the repository's default branch; a feature merged here may require a newer package than the one you have installed. Check the [release index](https://github.com/opensandbox-group/OpenSandbox/releases) when upgrading.

## Installation

| Language | Package | Install |
| --- | --- | --- |
| [Python](/sdks/python) | `opensandbox` | `pip install opensandbox` |
| [JavaScript/TypeScript](/sdks/javascript) | `@alibaba-group/opensandbox` | `npm install @alibaba-group/opensandbox` |
| [Kotlin/Java](/sdks/kotlin) | `com.alibaba.opensandbox:sandbox` | Gradle/Maven |
| [Go](/sdks/go) | `github.com/alibaba/OpenSandbox/sdks/sandbox/go` | `go get github.com/alibaba/OpenSandbox/sdks/sandbox/go` |
| [C#/.NET](/sdks/csharp) | `Alibaba.OpenSandbox` | `dotnet add package Alibaba.OpenSandbox` |

## Capability coverage

“Yes” means a public SDK entry point exists. Availability also depends on the server runtime and the execd version installed in the sandbox.

| Capability | Python async/sync | JavaScript/TypeScript | Kotlin/Java | Go | C#/.NET |
| --- | --- | --- | --- | --- | --- |
| Create, connect, renew, pause/resume, kill | Yes | Yes | Yes | Yes | Yes |
| Snapshots and template management | Yes | Yes | Yes | Yes | Yes |
| Resource requests, volumes, lifecycle hooks | Yes | Yes | Yes | Yes | Yes |
| Commands, files, resource metrics | Yes | Yes | Yes | Yes | Yes |
| Egress policy and Credential Vault | Yes | Yes | Yes | Yes | Yes |
| Isolated sessions | Yes | Yes | Yes | Yes | Yes |
| [Client Pool](/guides/client-pool), including Redis | Yes | Yes | Yes | Yes | No |
| [Pool warmup tracing](/sdks/observability#pool-warmup-tracing) | Yes | Yes, fewer attributes | Yes | No | No |
| [Diagnostic context, logs/events](/api/#diagnostics) | Yes | Yes | Yes | Yes | Yes |
| [Create-latency telemetry](/sdks/observability#creation-metrics) | Yes | Yes | Yes | Yes | Yes |

### Differences that affect application code

- **Pools:** Go exposes different warmup controls; the fields accepted by the default pool creator also differ across languages. See the [pool configuration matrix](/guides/client-pool#configuration).
- **Go command control:** interrupt, command status, and accumulated command logs are exposed by `ExecdClient`, rather than the high-level `Sandbox` wrapper. Neither `Sandbox.CreateSession` nor `ExecdClient.CreateSession` accepts an initial working directory. Set `Cwd` when running a command in the session instead.
- **Go connect/resume readiness:** pass `ReadyOptions` to request readiness checks. Without it, connecting resolves the endpoint without checking sandbox health.
- **Metrics streaming:** Go exposes `ExecdClient.WatchMetrics`; the other SDKs' stable metrics services provide point-in-time reads. The [CLI](/cli/) also has a metrics stream via `osb sandbox metrics --watch`.
- **Timeout units:** use Python `timedelta` and JVM `Duration`; JavaScript and C# command timeouts use seconds. Go `RunCommandRequest.Timeout` uses **milliseconds**.

Template-backed sandboxes require an explicit TTL and inherit their workload configuration from the published template. Their egress policy is managed through the lifecycle API; they do not have a sandbox-side Credential Vault.

## Feature guides

- [Client Pool](/guides/client-pool): keep a ready buffer, select an acquire policy, share state through Redis, and retire a pool namespace.
- [Observability](/sdks/observability): configure pool warmup traces and creation metrics, understand the default settings, and locate slow startup phases.

## Diagnostics

All sandbox SDKs expose diagnostic context, logs, and events on both `Sandbox`
and `SandboxManager`. Use a manager when execd is not ready: these requests go
to the lifecycle server without connecting to the sandbox.

| Language | Context | Logs / events |
| --- | --- | --- |
| Python async/sync | `get_diagnostic_context` | `get_diagnostic_logs` / `get_diagnostic_events` |
| Kotlin/Java, JavaScript | `getDiagnosticContext` | `getDiagnosticLogs` / `getDiagnosticEvents` |
| Go | `GetDiagnosticContext` | `GetDiagnosticLogs` / `GetDiagnosticEvents` |
| C# | `GetDiagnosticContextAsync` | `GetDiagnosticLogsAsync` / `GetDiagnosticEventsAsync` |

Manager methods take a sandbox ID; sandbox methods use the current instance.
Context returns the backend type, backend-defined identifiers, and lifecycle state
when available. Log/event methods also require a scope such as `container` or
`runtime`. Availability depends on the server backend. URL-delivered content is
not downloaded automatically; inspect `truncated` and `warnings`.
C# `SdkDiagnosticsOptions` separately controls local SDK logging.

## Lifecycle and cleanup

High-level image/snapshot creation defaults to a 10-minute TTL. Configure the TTL explicitly for your workload. To disable expiration, use Python `timeout=None`, JavaScript `timeoutSeconds: null`, Kotlin `timeout(null)`, or the Go/C# `ManualCleanup` option. Template creation requires a TTL.

`close()`, `DisposeAsync()`, and context-manager exit release local client resources; they do not kill the remote sandbox. Kill it in a `finally`/`defer` block, or use Python's `destroy()` helper. Pool acquisitions are consumed once and are not returned to the idle buffer.

## CLI

Use the [CLI](/cli/) to manage sandboxes, run commands, and work with files from a terminal.

```bash
uv tool install opensandbox-cli
```

## MCP server

The [MCP server](/sdks/mcp) exposes sandbox operations to MCP-capable clients:

```bash
pip install opensandbox-mcp
```

---

### Architecture Fast Sandbox Index (`docs/architecture/fast-sandbox/index.md`)

---
title: Fast Sandbox
description: OpenSandbox integration with the fast-sandbox platform — template-backed sandboxes delivered through Fastlet pools and the FastPath control plane.
---

# Fast Sandbox Integration

Fast Sandbox is a runtime backend that connects OpenSandbox to the external [fast-sandbox](https://github.com/opensandbox-group/fast-sandbox) platform. It provisions template-backed **Firecracker microVM** sandboxes inside pre-warmed **Fastlet** pods, removing the per-sandbox Kubernetes scheduler, watch propagation, and kubelet path from the creation hot path.

The adapter and endpoint integration live in this repository. FastPath, the Fastlet runtime, and the `sandbox.fast.io` CRDs are owned by the fast-sandbox platform and consumed as-is.

## How it fits

- **Backend selection**: Fast Sandbox is not a separate `runtime.type`. Under `runtime.type = "kubernetes"`, `CompositeSandboxService` routes create requests carrying `templateId` to `FastSandboxService`, and dispatches operations on existing `fsb-` prefixed sandbox IDs to it. Image- and snapshot-based requests keep using the Kubernetes workload providers.
- **Control protocol**: the server and ingress talk to the fast-sandbox control plane (FastPath v2) over gRPC. gRPC ends at FastPath: the FastPath/Reconciler-to-Fastlet control protocol is JSON over HTTP (`/api/v2/fastlet/*`). FastPath owns mutations and live runtime operations; reads of persisted state use Kubernetes LIST/WATCH on `sandbox.fast.io` custom resources.
- **Tenancy**: a tenant provider maps each authenticated tenant to a fast-sandbox namespace; request-scoped operations resolve it from context, background work falls back to a lookup by sandbox ID.



👉 <a href="/architecture/fast-sandbox/architecture.html" target="_blank" rel="noopener">Interactive architecture diagram</a> — explore sandbox creation, template artifact flow, checkpoints, and deployment topology in a full-view explorable map.

## Core components

| Component | Responsibility | Design goal |
|---|---|---|
| FastPath (gRPC control plane) | Serves every mutation and live operation: create, update, expiry, endpoint resolution, snapshots | Constant-time create via in-memory placement; restartable because durable state lives in CRDs, not here |
| Reconcilers (controller) | Drive CRs to reality: build templates, run pools, place sandboxes, finish snapshots | Converge by observation — no leader election, replay from CRs after any restart |
| Sandbox CRDs (`sandbox.fast.io`) | Durable record of intent, placement, policy bindings, and observations per sandbox | Intent survives every component failure; the CR is the source of truth |
| Fastlet (pre-warmed pod) | Hosts many sandboxes in one network domain; provides runtime capacity and shared services | Density with isolation: pre-provisioned network slots, shared egress and DNS amortized across sandboxes |
| Runtime agent (per node) | Owns Firecracker VM lifecycle over a unix socket; node readiness checks and self-labeling; installs assets | Node autonomy: the control plane delegates, the node verifies itself before admitting workloads |
| [DART](https://github.com/data-accelerator/dart) (per node) | Peer-to-peer artifact delivery between node state roots | Amortize the origin: each block fetched from the store roughly once cluster-wide |
| Egress process (per Fastlet) | Multi-sandbox outbound policy: subjects, Sandbox Actions handler, DNS and network enforcement | Fail-closed per-sandbox policy without per-sandbox sidecars — policy may be late, never open |
| Template builder | Converts OCI images into golden-image artifacts (guest wiring, init, execd baked in) | Workload fixed before any request: creates express intent, not configuration |
| Janitor | Sweeps orphaned resources of failed operations | Reconciliation finishes interrupted work; the janitor removes what should no longer exist |

## Design principles

- **No new public API**: template-based creation and `fsb-` ID dispatch ride the existing lifecycle contract. SDKs work unchanged across backends, and no sandbox-to-backend registration store exists — the ID prefix is the dispatcher.
- **CRD-first control**: durable per-sandbox intent lives in `sandbox.fast.io` resources; FastPath gRPC carries the mutation hot path. The Kubernetes scheduler, watch propagation, and kubelet leave the create path — the durable write does not.
- **Fail-closed multi-tenant egress**: every subject denies traffic from registration until its data plane is ready — policy delivery can be late, never early-open.
- **Authenticated routing**: sandbox routes are signed `f1.` scopes verified at ingress; endpoint issuance is decoupled from readiness.
- **Firecracker by template**: golden images are built for Firecracker microVMs — the runtime shape is fixed at build time, not per request.

## Documentation

| Page | Content |
|---|---|
| <a href="/architecture/fast-sandbox/architecture.html" target="_blank" rel="noopener">Interactive Architecture</a> | Explorable component map: sandbox creation, template artifact flow, checkpoints, and deployment topology |
| [Templates](/architecture/fast-sandbox/templates) | Template catalog, golden-image builds, artifact publication |
| [Scheduling](/architecture/fast-sandbox/scheduling) | Pools and Fastlets, the create hot path, lifecycle and pause/resume |
| [Networking](/architecture/fast-sandbox/networking) | Network slots, inbound route scopes and resolution, outbound egress enforcement |
| [Pause, Resume, and Snapshots](/architecture/fast-sandbox/checkpoints) | Checkpoint artifacts, pause/resume across Fastlets, snapshot publication and restore |
| [High Availability](/architecture/fast-sandbox/ha) | What survives which failure: control plane, nodes, sandboxes |
| [Storage](/architecture/fast-sandbox/storage) | Artifact store, peer-to-peer block delivery, node state roots |
| [Firecracker](/architecture/fast-sandbox/firecracker) | Host requirements, per-VM networking model, agent delegation |
| [Performance](/architecture/fast-sandbox/performance) | ACK create, request, snapshot, and same-node pause/resume measurements |

---

### Console Docs 00 Design Principles (`console/docs/00-design-principles.md`)

# Design principles

## Goals

- Ship the console under `console/` with **minimal coupling** to lifecycle server internals; prefer a **BFF** over browser-held API keys.
- **Tenant developers** use a tenant API key → see sandboxes in their namespace only.
- **Platform admins** use a separate BFF admin token; the BFF aggregates Lifecycle calls **server-side** with per-tenant keys (optional read-only Kubernetes RBAC for platform pages).

## Avoid (large cross-cutting PRs)

| Avoid | Why |
|-------|-----|
| Full server auth overhaul in the same PR as the UI | High review cost and conflict with `main` |
| Browser storage of `OPEN-SANDBOX-API-KEY` | Use httpOnly BFF session instead |
| Runtime-specific diagnostics in core UI | Stay on Diagnostics API |
| Everything in one merge (history DB, Grafana, image catalog, K8s ops) | Split PRs; see [05-feature-matrix.md](./05-feature-matrix.md) |

## Do

| Principle | Approach |
|-----------|----------|
| Decoupled delivery | `console/` + standalone Deployments (optional Helm subchart later) |
| Single auth path to Lifecycle | Tenant key → BFF session; admin → `BFF_ADMIN_TOKEN` |
| Runtime summary | BFF computes `runtimeSummary` from `createdAt` / `expiresAt` / status |
| Pool | Call existing `/v1/pools`; UI states **platform-level, not tenant-isolated** |
| Phased delivery | P0 sandbox CRUD; P1 snapshots/diagnostics; P2 admin + K8s views |

## Phasing

```text
P0  Tenant: login, list/detail/create/renew/delete/endpoint, runtimeSummary
P1  Snapshots, diagnostics, pools (admin)
P2  Admin cross-tenant list/stats, K8s workloads/events, component health
P3  Optional: PostgreSQL usage/history, OIDC (likely still at BFF layer)
```

## References

- [OSEP-0006: Developer Console](../../../oseps/0006-developer-console.md) — capability reference; this implementation uses **BFF session auth**, not OSEP Phase 1 server RBAC.
- Optional server-side rows: `[store.lifecycle_audit]` in [server configuration](../../server/configuration.md).

---

### Console Docs 01 Architecture (`console/docs/01-architecture.md`)

# Architecture

## Components

```mermaid
flowchart LR
  Browser[Browser SPA]
  BFF[Console BFF]
  Life[Lifecycle Server /v1]
  K8s[K8s API read-only]
  Browser --> BFF
  BFF -->|tenant key proxy| Life
  BFF -->|admin optional| K8s
```

- **SPA**: static assets (nginx or same-origin reverse proxy). No Lifecycle API key in the bundle.
- **BFF**: sessions, tenant resolution, admin multi-tenant list aggregation, `runtimeSummary`.
- **Server**: unchanged Lifecycle behavior; multi-tenant isolation via `[tenants]` and API keys.

## Dual login

| Role | Login | BFF session | Data scope |
|------|-------|-------------|------------|
| Developer | `POST /api/auth/tenant` + tenant API key | tenant session (tenant name only) | single tenant / namespace |
| Admin | `POST /api/auth/admin` + `BFF_ADMIN_TOKEN` | admin session | all tenants via server-side key rotation |

Admin mutations proxy to the owning tenant key where implemented.

## Shared `tenants.toml`

Server and BFF should mount the **same** `tenants.toml` (ConfigMap or Secret):

- Server validates keys and injects tenant context.
- BFF validates keys for login; admin aggregation uses keys **only on the server**, never returned to the browser.

## Platform constraints (do not mislead in UI)

- **Pool API (`/v1/pools`)** is not tenant-isolated.
- With `[tenants]` enabled, there is no global `server.api_key`.
- Proxy routes still require an API key (BFF can proxy on behalf of the tenant).

## Lifecycle proxy

`LifecycleClient` calls the Lifecycle HTTP API with `httpx` and forwards the upstream status code and JSON body. The BFF then adds `runtimeSummary`. It does not use `opensandbox.SandboxManager`.

That is a deliberate boundary:

- The session picks the tenant API key per request. `ConnectionConfig.api_key` is fixed when a manager is constructed. A per-tenant manager cache would cover static tenant keys, but `/health` and `/version` are unauthenticated and sit outside `/v1`.
- The SPA depends on the Lifecycle error body (`code` / `message`) and status code. The SDK returns typed models and raises `SandboxException`, which drops unknown fields and replaces the upstream error.
- A short-lived `httpx` client does not reuse the SDK connection pool or retry policy. Pooling can be added on this client later without taking a dependency on SDK models.

---

### Console Docs 02 Bff Config And Secrets (`console/docs/02-bff-config-and-secrets.md`)

# BFF configuration and secrets

## Required environment variables

| Variable | Required | Description |
|----------|----------|-------------|
| `LIFECYCLE_API_BASE` | yes | Lifecycle root including `/v1`, e.g. `http://opensandbox-server.opensandbox-system.svc:8090/v1` |
| `TENANTS_TOML_PATH` | yes | Path to **`tenants.toml`** (same content as server mount) |
| `BFF_SESSION_SECRET` | yes | Session cookie signing secret (≥32 random chars) |
| `BFF_ADMIN_TOKEN` | yes | Admin login password; **never** in frontend build |
| `BFF_COOKIE_SECURE` | no | Set `true` behind HTTPS |
| `BFF_CORS_ORIGINS` | no | Comma-separated SPA origins. Default `http://localhost:5173`. `*` is rejected because session cookies use credentials |
| `BFF_HTTP_TIMEOUT_SECONDS` | no | Upstream timeout; default `30` |

### Optional: node-agent integration

| Variable | Default | Description |
|----------|---------|-------------|
| `BFF_K8S_PROBE_ENABLED` | `false` | Platform probes, node-agent, workloads/events APIs |
| `BFF_K8S_INSECURE_SKIP_TLS_VERIFY` | `false` | Skip Kubernetes API TLS verify (temporary; independent of server `[kubernetes] insecure_skip_tls_verify`) |
| `BFF_K8S_SYSTEM_NAMESPACE` | `opensandbox-system` | Controller/ingress namespace |
| `BFF_K8S_CONTROLLER_DEPLOYMENT` | `opensandbox-controller-manager` | Match `kubectl get deploy -n opensandbox-system` |
| `BFF_K8S_INGRESS_DEPLOYMENT` | `opensandbox-ingress-gateway` | Same |
| `BFF_K8S_NODE_AGENT_NAMESPACE` | `opensandbox-system` | DaemonSet namespace |
| `BFF_K8S_NODE_AGENT_LABEL_SELECTOR` | `app.kubernetes.io/component=node-agent` | Pod selector |
| `BFF_K8S_NODE_AGENT_PROBE_PORT` | `8080` | node-agent health port |

Platform probes, the BFF ServiceAccount, and node-agent archive logs are not part of this PR. The deployment example uses the namespace default ServiceAccount.

Future optional features (history PostgreSQL, Grafana monitor) are documented in [07-sandbox-history.md](./07-sandbox-history.md) and tracked on branch `feat/console-extended-scope`, not in the MVP PR.

## `tenants.toml` source

1. Maintain ConfigMap `opensandbox-tenants` `data.tenants.toml` (see [tenants-configmap.example.yaml](../k8s/tenants-configmap.example.yaml)).
2. Mount the same file on server and BFF pods (`subPath: tenants.toml`).

`TENANTS_TOML_PATH` must point to a **`.toml` file**, not a Kubernetes YAML file.

## Secret structure

See [console-bff-secret.example.yaml](../k8s/console-bff-secret.example.yaml): `BFF_SESSION_SECRET`, `BFF_ADMIN_TOKEN`. Tenant keys live only in mounted `tenants.toml`.

## Credentials separation

| Credential | Storage | Use |
|------------|---------|-----|
| Tenant `api_keys[]` | `tenants.toml` | Developer login; BFF → Lifecycle |
| `BFF_ADMIN_TOKEN` | BFF Secret | Admin BFF login only |

## Local `.env`

Copy [bff/.env.example](../bff/.env.example) to `bff/.env` (gitignored).

## Security checklist

- [ ] Tenant keys not in SPA bundle or `localStorage`
- [ ] RBAC: only server + BFF service accounts read `tenants.toml`
- [ ] Admin token rotation process
- [ ] Ingress TLS; `BFF_COOKIE_SECURE=true`
- [ ] Rate-limit or restrict admin login exposure

---

### Console Docs 03 Bff Api (`console/docs/03-bff-api.md`)

# BFF HTTP API (SPA)

Base path: `/api`. Cookie session required except login and health.

## Authentication

### `POST /api/auth/tenant`

```json
{ "apiKey": "..." }
```

- 200: `{ "role": "tenant", "tenant": "...", "namespace": "..." }` + Set-Cookie
- 401: invalid key

### `POST /api/auth/admin`

```json
{ "adminToken": "..." }
```

- 200: `{ "role": "admin" }` + Set-Cookie

### `POST /api/auth/logout`

Clears session.

### `GET /api/auth/me`

`{ "role": "tenant"|"admin", "tenant"?, "namespace"? }`

## Tenant sandboxes

Proxies Lifecycle; responses include BFF **`runtimeSummary`**.

| Method | Path | Lifecycle |
|--------|------|-----------|
| GET | `/api/sandboxes` | `GET /sandboxes` |
| GET | `/api/sandboxes/{id}` | `GET /sandboxes/{id}` |
| POST | `/api/sandboxes` | `POST /sandboxes` |
| DELETE | `/api/sandboxes/{id}` | DELETE |
| POST | `/api/sandboxes/{id}/renew-expiration` | same |
| POST | `/api/sandboxes/{id}/pause` | same (202) |
| POST | `/api/sandboxes/{id}/resume` | same (202) |
| POST | `/api/sandboxes/{id}/snapshots` | same (202) |
| GET | `/api/sandboxes/{id}/diagnostics/logs` | query `scope` required |
| GET | `/api/sandboxes/{id}/diagnostics/events` | same |
| GET | `/api/sandboxes/{id}/endpoints/{port}` | same |

### Snapshots (tenant)

| Method | Path |
|--------|------|
| GET | `/api/snapshots` |
| GET | `/api/snapshots/{id}` |
| DELETE | `/api/snapshots/{id}` |

### `runtimeSummary`

```json
{
  "wallClockSeconds": 3842,
  "remainingSeconds": 900,
  "asOf": "2026-09-28T08:00:00Z",
  "basis": "createdAt"
}
```

## Admin

### `GET /api/admin/sandboxes`

Aggregates per-tenant lists; optional `tenant=` filter. Pagination includes `tenantErrors` for partial failures.

### Admin sandbox ops

Query **`tenant`** required for `{id}` read/write, diagnostics, etc.

### Admin snapshots

`GET/DELETE /api/admin/snapshots[/{id}]?tenant=`

### Admin Kubernetes (read-only)

`GET /api/admin/k8s/workloads`, `GET /api/admin/k8s/events` — requires `BFF_K8S_PROBE_ENABLED` and RBAC.

### `GET /api/admin/stats/runtime`

Aggregated KPIs from admin sandbox list.

### Pools (admin)

Proxies `/v1/pools` (**not tenant-isolated**; BFF uses first tenant key in `tenants.toml`).

### `GET /api/admin/platform/summary`

Server health/version plus component placeholders.

## Health

- `GET /health` → `{ "status": "ok" }`
- `GET /api/version` → proxies server `/version`

## Errors

```json
{ "code": "UNAUTHORIZED", "message": "..." }
```

Lifecycle errors forward `code` / `message` when present.

---

### Api Implementation Compatibility (`docs/api/implementation-compatibility.md`)

---
title: Implementation Compatibility
description: Published OpenSandbox contracts, reference implementation boundaries, and current SDK expectations for independent implementations.
---

# Implementation compatibility

OpenSandbox separates sandbox lifecycle management, in-sandbox execution, and workload-specific services. This page describes the current repository's interfaces and client behavior. It does not define a new v1 conformance profile or a capability-negotiation protocol. Pin the spec revision and SDK version you target: an implementation of one API is not automatically compatible with every feature exposed by a sandbox SDK.

## Contracts and processes

The [published OpenAPI files](/api/) are the starting point for implementation:

| Contract | Interface and consumers |
| --- | --- |
| [`sandbox-lifecycle.yml`](https://github.com/opensandbox-group/OpenSandbox/blob/main/specs/sandbox-lifecycle.yml) | Control plane: creation, inspection, deletion, expiration, endpoint resolution, pause/resume, snapshots, templates, and lifecycle network-policy operations. Used by sandbox SDKs and management clients. |
| [`diagnostic-api.yml`](https://github.com/opensandbox-group/OpenSandbox/blob/main/specs/diagnostic-api.yml) | Additional lifecycle-server routes for diagnostic log/event descriptors. Needed when clients use diagnostics; not the command-output API. |
| [`execd-api.yaml`](https://github.com/opensandbox-group/OpenSandbox/blob/main/specs/execd-api.yaml) | Data plane: health, commands, files, directories, code contexts, metrics, and isolated-session operations. Used by the execution services in sandbox SDKs and Code Interpreter clients. |
| [`egress-api.yaml`](https://github.com/opensandbox-group/OpenSandbox/blob/main/specs/egress-api.yaml) | Egress policy and Credential Vault management. A separate API from execd; the reference deployment normally serves it through an egress component. |

The wire contract does not require an independent implementation to run the Go `execd` binary. A native execution service can serve the execd operations that its clients use. Compatibility requires their behavior as well as their route names: request/response schemas, authentication headers, file transfer semantics, SSE event payloads and completion, errors, and context/command lifetimes all matter. Implementing only the lifecycle contract does not supply SDK command, file, or code execution. Conversely, an execd-compatible service alone does not implement sandbox creation or endpoint resolution.

The reference server's binary injection, container bootstrap, init/supervision, Jupyter integration, and proxy layout are implementation mechanisms. A backend can use different mechanisms, but must preserve the observable semantics of the operations it claims to support. OpenAPI coverage is not complete for every reference extension: for example, the current [execd router](https://github.com/opensandbox-group/OpenSandbox/blob/main/components/execd/pkg/web/router.go) exposes `/pty` HTTP/WebSocket routes which are not yet in `execd-api.yaml`. Do not infer support for such extensions from OpenAPI compatibility alone.

## Workload APIs and endpoint resolution

`GET /v1/sandboxes/{sandboxId}/endpoints/{port}` resolves access to a service on the requested sandbox port. It does not convert that service into an execd API or impose execd's command/file schemas on it. In the [AIO example](https://github.com/opensandbox-group/OpenSandbox/blob/main/examples/aio-sandbox/main.py), OpenSandbox manages the sandbox and resolves port `8080`, then `AioSandboxClient` speaks the image's own shell/file/browser API.

Endpoint resolution is agnostic to the workload's application API; transport support still depends on the selected network path. For example, the [Docker embedding proxy](/architecture/network/single-host-network) forwards HTTP, SSE, and WebSocket traffic. An arbitrary port number does not imply a generic TCP/UDP tunnel. Preserve the returned address (including proxy path) and required headers, and use a client suitable for the service and transport. Passing through execd's proxy does not make the destination process execd.

## Current Python SDK endpoint expectations

The following describes [`SandboxSync`](https://github.com/opensandbox-group/OpenSandbox/blob/main/sdks/sandbox/python/src/opensandbox/sync/sandbox.py) in the current tree, rather than a portable protocol requirement for all clients:

| Flow | Endpoint resolution before the health check |
| --- | --- |
| `create()` from an image or snapshot | Resolves execd port `44772`, then egress port `18080`. If the execd endpoint reports template origin, policy operations use the lifecycle service, but this create path has already resolved the egress endpoint. |
| `create_from_template()` | Resolves `44772`; uses lifecycle network-policy operations without resolving `18080`. |
| `connect()` | Resolves `44772` and reads the endpoint response's `OPEN-SANDBOX-ORIGIN` header. Template origin selects lifecycle network policy; other origins also resolve `18080`. |

A custom `health_check` replaces the default execd `/ping` readiness probe. It does **not** remove these endpoint lookups or the construction of SDK service adapters. `skip_health_check=True` also still waits for endpoint publication. This explains why the AIO example's custom health check does not make it a lifecycle-only SDK client. Resolving an endpoint is distinct from invoking its API: service adapters are constructed before the application chooses operations.

An independent backend targeting these high-level flows must account for these lookups, returned headers, and the operations it will expose. A client using only the lifecycle HTTP API and a workload-specific client can choose a narrower integration. Verify other SDK languages and versions separately; the table is not a promise that all SDKs have identical startup behavior. Template origin is an existing routing signal, not a general capability manifest.

## Backend-dependent features and unsupported operations

The presence of an operation in a spec or SDK does not mean every runtime, image, or deployment enables it:

| Feature | Current boundary |
| --- | --- |
| Pause/resume and snapshots | Depend on the workload provider, runtime, and storage configuration. In particular, the Kubernetes rootfs snapshot committer rejects gVisor RuntimeClasses. Do not infer snapshot support from basic create/execute support. |
| Templates | Template management uses the Kubernetes Fast Sandbox integration; the reference Docker server rejects it with HTTP `501`. |
| Egress policy and Credential Vault | Depend on egress configuration and backend routing. Template-backed Python sandboxes manage policy through the lifecycle service; `credential_vault` access raises an SDK exception for that origin. Policy support does not imply Vault support. |
| Code execution | The reference execd uses Jupyter and installed kernels for code contexts. A shell-only image does not thereby support notebook-style code execution. A native implementation must reproduce the code API semantics if it claims that feature. |
| PTY and isolated sessions | Depend on platform facilities and reference extensions. `/v1/isolated/capabilities` describes the isolator/hardening environment, not all lifecycle and execution features. Isolated diff/commit currently report unsupported and are not implemented. |

There is no single general capability-negotiation endpoint in the current contracts. Document a backend's supported operations and prerequisites, and use feature-specific discovery where available. Unsupported requests should produce an explicit failure with an actionable error, not a fabricated success. Use the operation's specified error shape and distinguish an unsupported feature from missing authorization, missing resources, or a temporarily unready service.

There is also no universal unsupported-operation status to substitute everywhere. Current reference examples include HTTP `501` for unsupported template management, HTTP `409` for the snapshot service's unsupported-runtime preflight, and HTTP `503` for unimplemented isolated diff/commit. See the [template handler](https://github.com/opensandbox-group/OpenSandbox/blob/main/server/opensandbox_server/api/templates.py), [snapshot service](https://github.com/opensandbox-group/OpenSandbox/blob/main/server/opensandbox_server/services/snapshot_service.py), and [isolated API notes](/api/). These describe present behavior, not a new uniform error convention.

## Choosing an initial compatibility target

Start with a named client version and an explicit operation set. For a command and file integration, validate lifecycle create/inspect/delete and endpoint resolution, authentication and readiness, command SSE completion and errors, and the file operations the client needs. Add code contexts, snapshots, or policy/Vault operations only when the backend can provide their semantics. Use the repository's [SDK end-to-end tests](https://github.com/opensandbox-group/OpenSandbox/tree/main/tests) for the selected operations and document unimplemented features. This is a practical integration target, not a claim of full protocol conformance.

---

### Community Code Of Conduct (`docs/community/code-of-conduct.md`)

---
title: Code of Conduct
description: OpenSandbox community code of conduct.
---

# Code of Conduct

The code of conduct is maintained in the repository root as the single source of truth.

<a href="https://github.com/opensandbox-group/OpenSandbox/blob/main/CODE_OF_CONDUCT.md" target="_blank">
  Read the Code of Conduct on GitHub →
</a>

---

### Community Contributing (`docs/community/contributing.md`)

---
title: Contributing
description: Guide for contributing to the OpenSandbox project.
---

# Contributing to OpenSandbox

The contributing guide is maintained in the repository root to keep GitHub's contributor experience consistent.

<a href="https://github.com/opensandbox-group/OpenSandbox/blob/main/CONTRIBUTING.md" target="_blank">
  Read the Contributing Guide on GitHub →
</a>

## Contributor Advancement

OpenSandbox maintains an explicit advancement process for contributors to become component maintainers or project maintainers. For nomination requirements, voting periods, and approval thresholds, see the project governance guide:

<a href="https://github.com/opensandbox-group/OpenSandbox/blob/main/GOVERNANCE.md#becoming-a-maintainer" target="_blank">
  Read the Maintainer Advancement Process on GitHub →
</a>

---

### Community Oseps (`docs/community/oseps.md`)

---
title: Enhancement Proposals (OSEPs)
description: Index of OpenSandbox Enhancement Proposals covering major features, architectural changes, and API modifications.
---

# OpenSandbox Enhancement Proposals

OpenSandbox Enhancement Proposals (OSEPs) are the mechanism for proposing major features, architectural changes, or modifications to the core API and security model. Small bug fixes and minor improvements do not require an OSEP.

See the [OSEP contributing guide](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/CONTRIBUTING.md) for information on how to create and merge proposals.

## Proposal Index

| OSEP | Title | Status | Last Updated |
|:----:|:-----:|:------:|:------------:|
| [OSEP-0001](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0001-fqdn-based-egress-control.md) | FQDN-based Egress Control | implemented | 2026-01-22 |
| [OSEP-0002](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0002-kubernetes-sigs-agent-sandbox-support.md) | kubernetes-sigs/agent-sandbox Support | implemented | 2026-01-23 |
| [OSEP-0003](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0003-volume-and-volumebinding-support.md) | Volume Support | implementing | 2026-02-11 |
| [OSEP-0004](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0004-secure-container-runtime.md) | Pluggable Secure Container Runtime Support | implemented | 2026-07-27 |
| [OSEP-0005](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0005-client-side-sandbox-pool.md) | Client-Side Sandbox Pool | implemented | 2026-07-27 |
| [OSEP-0006](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0006-developer-console.md) | Developer Console for Sandbox Operations | implementable | 2026-03-06 |
| [OSEP-0007](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0007-fast-sandbox-runtime-support.md) | Fast Sandbox Runtime Support | provisional | 2026-02-08 |
| [OSEP-0008](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0008-pause-resume-rootfs-snapshot.md) | Pause and Resume via Rootfs Snapshot | implementing | 2026-03-13 |
| [OSEP-0009](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0009-auto-renew-sandbox-on-ingress-access.md) | Auto-Renew Sandbox on Ingress Access | implemented | 2026-03-23 |
| [OSEP-0010](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0010-opentelemetry-instrumentation.md) | OpenTelemetry Metrics and Logs (execd, egress, and ingress) | implementing | 2026-04-12 |
| [OSEP-0011](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0011-secure-access-endpoint.md) | Secure Access on GetEndpoint and Signed Endpoint | implemented | 2026-04-25 |
| [OSEP-0012](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0012-credential-vault.md) | Credential Vault and Credential Proxy | implemented | 2026-06-23 |
| [OSEP-0013](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0013-isolated-execution-api.md) | Isolated Execution API | implementing | 2026-06-23 |
| [OSEP-0014](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0014-multi-tenancy.md) | Multi-Tenancy Support for Kubernetes Runtime | implemented | 2026-07-27 |
| [OSEP-0015](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0015-pod-snapshot.md) | Spec-Driven Pod Snapshot for Pause and Resume | draft | 2026-06-27 |
| [OSEP-0016](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0016-unified-umbrella-release-governance.md) | Unified Umbrella Release Governance | implemented | 2026-09-21 |
| [OSEP-0017](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0017-resilient-sdk-transport.md) | Resilient SDK Transport | implementing | 2026-07-22 |
| [OSEP-0019](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0019-node-agent-sandbox-collection.md) | Node Agent for Node-Level Sandbox Collection | implementing | 2026-08-11 |

## Status Definitions

| Status | Meaning |
|--------|---------|
| **draft** | Proposal is under initial development, not yet accepted |
| **provisional** | Proposal accepted in principle, design may still evolve |
| **implementable** | Design is final and ready for implementation |
| **implementing** | Implementation is in progress |
| **implemented** | Fully implemented and available |

## Submitting a Proposal

::: tip
Before writing a full OSEP, consider opening a GitHub Issue or Discussion to gauge community interest and get early feedback on your idea.
:::

To submit a new OSEP:

1. Fork the repository
2. Copy the OSEP template
3. Fill in the proposal details
4. Submit a pull request

See the [OSEP contributing guide](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/CONTRIBUTING.md) for the full process.

---

### Community Release Automation (`docs/community/release-automation.md`)

---
title: Release Automation
description: How OpenSandbox releases work — one umbrella release, one tag, every artifact.
---

# Release Automation

OpenSandbox ships as a **unified umbrella release** (OSEP-0016): every artifact of a release — platform images, Helm charts (in-repo), CLI, and all SDKs — carries the same `X.Y.Z`, driven by a single workflow. Per-component and per-SDK release entry points have been removed; the umbrella is the only release path. See [Versioning](/community/versioning) for naming and cadence.

## Release Preparation (manual, on the release branch)

```bash
# 1. one-command bump: chart versions, image references, SDK versions,
#    and dependency ranges rewritten to the target version in one commit
manifests/release/create-umbrella-release.sh --version 1.1.0 --bump-only

# 2. hand-author the release notes (copy docs/releases/TEMPLATE.md),
#    then push
cp docs/releases/TEMPLATE.md docs/releases/1.1.0.md
$EDITOR docs/releases/1.1.0.md
git push
```

## Prereleases (rc)

An rc is always cut on the **line-birth version** (`X.Y.0-rc.N` — there is no `X.Y.Z-rc` for `Z > 0`). It validates that the platform deploys and the fan-out runs: images are published, **all packages are held (not published)**, and only the umbrella tag is minted (marked `--prerelease` on GitHub). rc runs are approval-free: no `release` environment gate, and the BOM PR merges without waiting for review (if the release-branch ruleset still mandates review, add a ruleset bypass for the release bot — the workflow fails fast instead of waiting).

```bash
# 1. bump (channel=rc is derived from the suffix)
manifests/release/create-umbrella-release.sh --version 1.1.0-rc.1 --bump-only

# 2. notes, then push
cp docs/releases/TEMPLATE.md docs/releases/1.1.0-rc.1.md
$EDITOR docs/releases/1.1.0-rc.1.md
git push

# 3. dispatch
gh workflow run release-umbrella.yml \
  --ref release-1.1 \
  -f version=1.1.0-rc.1 -f dry_run=false
```

Repeat with `-rc.2`, `-rc.3`, … as fixes land — every rc is a full rebuild and previous rc artifacts stay frozen in the registries. The stable release of the line (`1.1.0`, no suffix) then publishes packages for real. Prereleases are never resolved by default (PyPI / NuGet / Maven / Go hide them; npm publishes under the `rc` dist-tag).

## Release Execution (workflow)

Dispatch `.github/workflows/release-umbrella.yml` (`dry_run=false`, `version`, `channel`, `release_branch`). Stable releases require a release approver other than the triggerer to approve the `release` environment; rc releases and dry runs run approval-free. The workflow then runs:

| Stage | What happens |
|---|---|
| preflight | commit reachability + notes presence (`docs/releases/<version>.md`) |
| scan | version-consistency scan (release-blocking) |
| build | 13 images built and pushed directly to `release-X.Y.Z` (run-scoped staging tags while the `UMBRELLA_PUBLISH_ENABLED` gate is closed); all packages built and held |
| BOM | digests pinned into `docs/releases/<version>.yaml`, pushed to a `release/<version>` branch; the workflow opens a PR to the release branch and merges it (`C_bom`) — stable waits for a code-owner approval (required by the branch ruleset), rc merges immediately |
| publish | once the BOM PR merges (all images green + code-owner approval), packages publish in parallel per registry (PyPI, npm, NuGet, Maven Central); images are already public at `release-X.Y.Z` from the build stage |
| tag | `release-X.Y.Z` + `sdks/sandbox/go/vX.Y.Z` minted on `C_bom`, GitHub Release created with the notes and BOM |

A failed release never mints its git tag or GitHub Release, so the release feed never shows a partial release; dispatch a dry run (`dry_run=true`) to keep the fan-out healthy between release windows.

### Dry runs

`dry_run=true` exercises everything except publishing: images are built **locally** (single-arch, `--load` — no registry push, no credentials needed), packages are built and held as workflow artifacts, the BOM lands on the release branch through the BOM PR (one code-owner approval, as on any change to a protected branch) with local image IDs standing in for registry digests, and no git tags are minted. To rehearse a release in a fork, prepare the release branch (bump + notes) and dispatch the workflow with `dry_run=true` — the `UMBRELLA_PUBLISH_ENABLED` variable does not need to exist there. The fork must enable **Allow GitHub Actions to create and approve pull requests** (Settings → Actions → General → Workflow permissions); `pull-requests: write` in the workflow alone cannot override that setting being off.

## Release Artifacts

- `docs/releases/<version>.yaml` — BOM, authoritative for image digests
- `docs/releases/<version>.md` — hand-authored release notes (mirrored to the GitHub Release)
- container images: `opensandbox/<component>:release-X.Y.Z` on Docker Hub, GHCR, and ACR
- packages: PyPI (server, CLI, 3 SDKs), npm (2), Maven Central (5 Kotlin/JVM coordinates), NuGet (2) — all at the umbrella version
- Helm charts: **not published**; they ship in-repo at the release tag

## Release Runbook

### Publish and rollback

Registry legs (PyPI, npm, NuGet, Maven Central) run in parallel and independently — there is no cross-ecosystem ordering. Publish steps are idempotent: a version already visible on its registry is skipped, so re-runs never collide with registry immutability (a released Maven version is immutable; the `sandbox-bom` POM's presence on Central is the skip marker). A successful upload is not re-verified against the public index — registries' simple indexes can lag an upload by minutes. On a failure, remediate already-published legs:

| Ecosystem | Revocation |
|---|---|
| Maven Central | not released if its leg failed; after release, immutable — deprecate only |
| NuGet | `dotnet nuget delete <pkg> <version>` (72 h), else unlist |
| npm | `npm unpublish <pkg>@<version>` (72 h), else `npm deprecate` |
| PyPI | manual yank via project settings (no scriptable API) |
| images | `crane delete <image>:release-X.Y.Z` |

Always file a P0 release incident with the run link before remediating. Global mirror caches may retain revoked artifacts; revocation is authoritative upstream only.

### N-1 emergency backport (CVSS ≥ 8.0, ≤ 72 h, one-shot per line)

1. Land the fix on `main` (standard PR review).
2. `git checkout release-X.(Y-1) && git cherry-pick <sha> && git push`.
3. Dispatch `release-umbrella.yml` with `version=X.(Y-1).Z`, `release_branch=release-X.(Y-1)`, `dry_run=false`. The full fan-out reruns; the release notes must link the CVE and name the equivalent current-line snapshot.

Anything that is not a qualifying CVE waits for the next line birth.

### Staging cleanup

Run-scoped staging image tags (`staging-<sha>-<runid>`) exist only when a run does not publish release tags (dry runs, or real runs while the `UMBRELLA_PUBLISH_ENABLED` gate is still closed). They are namespaced and never referenced by user-facing surfaces; a scheduled job deletes staging tags whose run did not complete within 14 days. Held package artifacts use the default 14-day workflow-artifact retention.

## Legacy Releases

Historical per-component tags (`server/v0.2.3`, `docker/execd/v1.1.0`, …) are frozen and keep resolving forever. Verification instructions for those older artifacts (and their workflow identities) are in [Release Verification](/community/release-verification).

---

### Community Release Verification (`docs/community/release-verification.md`)

---
title: Release Verification
description: Verify signed OpenSandbox releases for source archives, container images, packages, and Helm charts.
---

# Release Verification

OpenSandbox signs public release outputs without changing the normal install commands. Verification is optional for day-to-day use, but supported for users who need supply chain integrity checks.

This process applies to releases produced after the signing workflows were introduced. Older releases may not have attestations or signatures.

## Signing Model

OpenSandbox uses these signing paths:

- Source code releases: the Generic Release workflow uploads an explicit `opensandbox-<tag>.tar.gz` source archive and `SHA256SUMS` file to the GitHub Release, then creates GitHub/Sigstore provenance attestations for both files.
- Container images: the component and server image workflows sign Docker Hub, GitHub Container Registry (GHCR), and Alibaba Cloud Container Registry (ACR) image digests with `cosign` keyless signing, and publish provenance attestations to all three registries.
- Python and CLI packages: wheels and source distributions are attested before `uv publish`.
- JavaScript packages: the workflow runs `pnpm pack`, attests the generated npm tarball, and publishes that same tarball.
- C# packages: NuGet `.nupkg` files are attested before publication.
- Go SDK modules: the `sdks/sandbox/go/v<version>` source release archive is attested by the Generic Release workflow.
- Helm charts created by the current release workflow: packaged chart `.tgz` files are attested and published with an attested `SHA256SUMS` file so consumers can verify the exact release bytes.
- Java/Kotlin packages: Maven Central publications are signed by the Gradle Maven publish signing configuration. Download the `.asc` signature next to the Maven artifact and verify it with OpenPGP tooling.

Release tags may also be signed with `manifests/release/create-release.sh --sign-tag` when the release operator has a local git signing key configured. Do not rely on signed tags alone for generated deliverables; verify the artifact you are installing.

## Trust Roots and Keys

Most OpenSandbox release signatures are keyless Sigstore signatures created by GitHub Actions OpenID Connect (OIDC). There is no long-lived OpenSandbox private key for these signatures, so there is no project public key file to download. The public certificates and signed bundles are retrieved by `gh` or `cosign` from GitHub's attestation service, OCI registries, and Sigstore transparency infrastructure.

Expected identity values:

- Repository: `opensandbox-group/OpenSandbox`
- OIDC issuer: `https://token.actions.githubusercontent.com`

::: warning Legacy workflow identities (pre-umbrella releases only)
The per-target publish workflows below were removed when OpenSandbox moved to unified umbrella releases. They remain the verification identity for artifacts published before that point. New umbrella releases are produced by `release-umbrella.yml` and `release-packages.yml`.
:::

- Component image workflow: `opensandbox-group/OpenSandbox/.github/workflows/publish-components.yml`
- Server image workflow: `opensandbox-group/OpenSandbox/.github/workflows/publish-server.yml`
- CLI package workflow: `opensandbox-group/OpenSandbox/.github/workflows/publish-cli.yml`
- Python package workflow: `opensandbox-group/OpenSandbox/.github/workflows/publish-python-sdks.yml`
- JavaScript package workflow: `opensandbox-group/OpenSandbox/.github/workflows/publish-js-sdks.yml`
- C# package workflow: `opensandbox-group/OpenSandbox/.github/workflows/publish-csharp-sdks.yml`
- Helm chart workflow: `opensandbox-group/OpenSandbox/.github/workflows/publish-helm-chart.yml`

Set the repository identity for the release you are verifying:

```bash
REPOSITORY="opensandbox-group/OpenSandbox"
WORKFLOW_REPOSITORY="${REPOSITORY}"
WORKFLOW_REPOSITORY_URL="https://github.com/${WORKFLOW_REPOSITORY}"
```

For releases produced before the GitHub organization migration, use the historical identity instead:

```bash
REPOSITORY="alibaba/OpenSandbox"
WORKFLOW_REPOSITORY="${REPOSITORY}"
WORKFLOW_REPOSITORY_URL="https://github.com/${WORKFLOW_REPOSITORY}"
```

If you run the release workflows from a downstream fork, replace `opensandbox-group/OpenSandbox` in the verification commands with that fork's `owner/repository` identity.

Private signing material is not stored in GitHub Releases, Docker Hub, GHCR, ACR, PyPI, npm, Maven Central, NuGet, or Helm chart downloads. Java/Kotlin Maven Central signing keys are held only in GitHub Actions secrets.

## Verify Source Releases

> Source archives are produced only for releases before 2026-08 (when the `release-generic.yml` workflow was removed). Newer releases have no source archive; skip this section for them.

Set the release tag first:

```bash
TAG="server/v0.1.13"
SAFE_TAG="${TAG//\//-}"
```

Download the signed source archive and checksum file:

```bash
gh release download "$TAG" \
  --repo "$REPOSITORY" \
  --pattern "opensandbox-${SAFE_TAG}.tar.gz" \
  --pattern "SHA256SUMS"
```

Check the archive digest:

```bash
sha256sum -c SHA256SUMS
```

Verify the source archive attestation:

```bash
gh attestation verify "opensandbox-${SAFE_TAG}.tar.gz" \
  --repo "$REPOSITORY" \
  --signer-workflow "${WORKFLOW_REPOSITORY}/.github/workflows/release-generic.yml"
```

Verify the checksum file attestation:

```bash
gh attestation verify SHA256SUMS \
  --repo "$REPOSITORY" \
  --signer-workflow "${WORKFLOW_REPOSITORY}/.github/workflows/release-generic.yml"
```

The Generic Release workflow is started with `workflow_dispatch`, so its provenance `source-ref` is the ref selected when the workflow was dispatched (normally `refs/heads/main`), not the release tag created by the job.

## Verify Container Images

Install `cosign` and `gh`, then resolve the image digest. Always verify by digest, not by mutable tag alone.

Release images are published with the same component name and digest in all three official registries:

| Registry | Image name pattern |
| --- | --- |
| Docker Hub | `docker.io/opensandbox/<component>` |
| GitHub Container Registry | `ghcr.io/opensandbox-group/opensandbox/<component>` |
| Alibaba Cloud Container Registry | `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/<component>` |

The component can be `execd`, `ingress`, `egress`, `controller`, `task-executor`, `image-committer`, or `nodeagent`. The server image uses the component name `server`.

::: tip Code Interpreter Image Verification
The standalone `code-interpreter` environment image is published and maintained from [opensandbox-group/sandbox-images](https://github.com/opensandbox-group/sandbox-images).
- **Existing image releases** (such as `v1.1.0` and earlier) continue using the OpenSandbox workflow identity (`.github/workflows/publish-components.yml`).
- **New releases** from `opensandbox-group/sandbox-images` use its `release.yml` workflow identity. Users and operators verifying signatures or provenance for new releases must follow the verification documentation in [opensandbox-group/sandbox-images](https://github.com/opensandbox-group/sandbox-images).
:::

```bash
IMAGE="docker.io/opensandbox/execd"
TAG="v1.0.15"
DIGEST="$(docker buildx imagetools inspect "${IMAGE}:${TAG}" --format '{{.Manifest.Digest}}')"
IMAGE_REF="${IMAGE}@${DIGEST}"
```

Verify the cosign keyless signature:

```bash
cosign verify "$IMAGE_REF" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp "^${WORKFLOW_REPOSITORY_URL}/.github/workflows/publish-components.yml@refs/tags/(docker|k8s)/[^/]+/v?[0-9].*$"
```

Verify the registry provenance attestation:

```bash
gh attestation verify "oci://${IMAGE_REF}" \
  --repo "$REPOSITORY" \
  --bundle-from-oci \
  --signer-workflow "${WORKFLOW_REPOSITORY}/.github/workflows/publish-components.yml"
```

For the server image, use `docker.io/opensandbox/server` and the server workflow:

```bash
IMAGE="docker.io/opensandbox/server"
TAG="v0.1.13"
DIGEST="$(docker buildx imagetools inspect "${IMAGE}:${TAG}" --format '{{.Manifest.Digest}}')"
IMAGE_REF="${IMAGE}@${DIGEST}"

cosign verify "$IMAGE_REF" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp "^${WORKFLOW_REPOSITORY_URL}/.github/workflows/publish-server.yml@refs/tags/server/v[0-9].*$"
```

GHCR and ACR images use the same digest and identity checks with their respective image names, for example:

```bash
IMAGE="ghcr.io/opensandbox-group/opensandbox/execd"
# or
IMAGE="sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/execd"
```

## Verify Packages

Download the package file from its normal package registry, then verify the attestation against the OpenSandbox repository and the expected release tag.

Python and CLI packages:

```bash
python -m pip download opensandbox-server==0.1.13 --no-deps
gh attestation verify opensandbox_server-0.1.13*.whl \
  --repo "$REPOSITORY" \
  --signer-workflow "${WORKFLOW_REPOSITORY}/.github/workflows/publish-server.yml" \
  --source-ref refs/tags/server/v0.1.13
```

JavaScript packages:

```bash
npm pack @alibaba-group/opensandbox@0.1.7
gh attestation verify alibaba-group-opensandbox-0.1.7.tgz \
  --repo "$REPOSITORY" \
  --signer-workflow "${WORKFLOW_REPOSITORY}/.github/workflows/publish-js-sdks.yml" \
  --source-ref refs/tags/js/sandbox/v0.1.7
```

C# packages:

```bash
gh attestation verify Alibaba.OpenSandbox.1.0.0.nupkg \
  --repo "$REPOSITORY" \
  --signer-workflow "${WORKFLOW_REPOSITORY}/.github/workflows/publish-csharp-sdks.yml" \
  --source-ref refs/tags/csharp/sandbox/v1.0.0
```

Helm charts:

```bash
HELM_CHART="opensandbox"
HELM_VERSION="<chart-version>"
HELM_TAG="helm/${HELM_CHART}/${HELM_VERSION}"
HELM_PACKAGE="${HELM_CHART}-${HELM_VERSION}.tgz"

gh release download "$HELM_TAG" \
  --repo "$REPOSITORY" \
  --pattern "$HELM_PACKAGE" \
  --pattern SHA256SUMS
sha256sum -c SHA256SUMS
gh attestation verify "$HELM_PACKAGE" \
  --repo "$REPOSITORY" \
  --signer-workflow "${WORKFLOW_REPOSITORY}/.github/workflows/publish-helm-chart.yml" \
  --source-ref "refs/tags/${HELM_TAG}"
gh attestation verify SHA256SUMS \
  --repo "$REPOSITORY" \
  --signer-workflow "${WORKFLOW_REPOSITORY}/.github/workflows/publish-helm-chart.yml" \
  --source-ref "refs/tags/${HELM_TAG}"
```

The checksum binds the downloaded package to the digest recorded by the release workflow. The attestation verifies that the package with that digest was produced by the expected OpenSandbox workflow. Change `HELM_CHART` and `HELM_VERSION` to verify a different Helm release.

Manual Helm releases must select the exact existing release tag as their dispatch ref. Their provenance `source-ref`, like a tag-triggered Helm release, therefore identifies that protected Helm tag.

### Helm Release Status

A stable `opensandbox` umbrella chart is marked `production-ready` only after the release workflow installs the exact packaged `.tgz` identified by the published digest into an ephemeral Kind cluster. The workflow does not rebuild the chart between the runtime gate and publication. The gate must verify:

- The controller and server workloads become Ready.
- The required OpenSandbox CRDs are established and usable.
- Authenticated server access succeeds, while missing or invalid credentials are rejected.
- A BatchSandbox completes the create, command-execution, and delete lifecycle.

This status covers the default core profile named in the Release notes. It does not certify optional ingress, egress policy, snapshot/pause-resume, node-agent, upgrade, or multi-architecture lanes unless those profiles are listed separately.

Standalone `opensandbox-controller`, `opensandbox-server`, and `opensandbox-node-agent` chart releases are `package-verified` only. Their validation covers the packaged chart and does not claim the integrated Kind runtime coverage of a stable umbrella release.

Each Helm GitHub Release records the package SHA-256 digest, source ref and commit, validation workflow run, validation profile, and—when runtime-tested— the requested core image references, registry RepoDigests, and image IDs in its release notes. The profile distinguishes the stable umbrella Kind runtime gate from package-only verification. Because chart defaults remain documented image version tags, `production-ready` records the publication-time gate result; it does not claim that a registry tag can never be changed later.

Java/Kotlin Maven artifacts:

```bash
curl -O https://repo1.maven.org/maven2/com/alibaba/opensandbox/sandbox/1.0.10/sandbox-1.0.10.jar
curl -O https://repo1.maven.org/maven2/com/alibaba/opensandbox/sandbox/1.0.10/sandbox-1.0.10.jar.asc
KEY_ID="$(gpg --list-packets sandbox-1.0.10.jar.asc | awk '/keyid/ { print $NF; exit }')"
gpg --keyserver hkps://keys.openpgp.org --recv-keys "$KEY_ID"
gpg --verify sandbox-1.0.10.jar.asc sandbox-1.0.10.jar
```

If verification cannot find an attestation for a release that predates this process, use a newer release as the signed release evidence.

---

### Community Versioning (`docs/community/versioning.md`)

---
title: Versioning and Releases
description: Unified umbrella versioning for OpenSandbox — one version shared by every image, chart, CLI, and SDK.
---

# Versioning and Releases

OpenSandbox ships as a **unified umbrella release**: every artifact of a release — server and component images, Kubernetes controller and task-executor images, Helm chart, CLI, and all SDKs — carries the **same `X.Y.Z`**, cut from a single release commit and pinned in a signed BOM. Full design: [OSEP-0016](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0016-unified-umbrella-release-governance.md).

::: warning Rollout status
Umbrella release tooling is being rolled out in phases (dry-run → rc → GA, see the OSEP's Migration section). Until GA, per-component tags remain the operative release mechanism ([Release Automation](/community/release-automation)). Historical per-component tags are frozen at that point — never deleted, never extended — and keep resolving forever.
:::

## First version: `1.1.0`

The first umbrella release is **`release-1.1.0`** and doubles as the GA declaration. `1.0.0` is deliberately skipped: Maven Central (`com.alibaba.opensandbox:sandbox` up to `1.0.19`) and the Go module proxy (`sdks/sandbox/go` up to `v1.0.5`) already consumed those versions, and both registries are immutable. The umbrella starts at the lowest new **line** (`X.Y.0`) above every already-consumed version — `1.0.20` would clear the registries but is not a valid line birth (`Z > 0` is reserved for in-line snapshots), so `1.1.0` it is.

## Naming rules

| Artifact | Format (example at `1.4.0`) |
|---|---|
| Git tag | `release-1.4.0` |
| Container images | `opensandbox/{server,execd,ingress,egress,image-committer,controller,task-executor}:release-1.4.0` |
| Go SDK (VCS tags) | `sdks/sandbox/go/v1.4.0`, `sdks/sandbox/go/poolredis/v1.4.0` — same commit as the umbrella tag |
| CLI / SDKs / server on PyPI | bare `1.4.0` |

Git and image tags share the same `release-` string: `git checkout release-1.4.0` and the image you pull are the same release. Package registries use the bare semver core because they reject prefixes.

## Scope

- **Covered**: platform runtime images (server, execd, ingress, egress, image-committer, controller, task-executor; the fast-sandbox family ships as `opensandbox/fsb-*`), CLI, and all published SDKs (Python, JavaScript, Kotlin/JVM, .NET, Go).
- **Helm charts are not published.** Charts live in-repo and are versioned at the release tag; render and deploy yourself:

  ```
  git checkout release-1.1.0
  helm template ./manifests/charts/opensandbox | kubectl apply -f -
  ```

  GitOps platforms can point directly at the repo path and tag.
- **Not covered**: sandbox template images such as `opensandbox/code-interpreter`. They are chosen by the user at sandbox-creation time and version independently in [opensandbox-group/sandbox-images](https://github.com/opensandbox-group/sandbox-images).

## Cadence and support

- A line is born every 2 weeks (`X.Y.0`); in-line snapshots (`X.Y.Z`, `Z > 0`) ship on demand. Pre-releases look like `X.Y.0-rc.N` and publish images only — packages are held until the line's stable release.
- **Latest line only, no LTS.** When `X.(Y+1).0` ships, `X.Y.*` is EOL except for a single emergency-CVE window (CVSS ≥ 8.0, ≤ 72h from disclosure, one-shot `X.(Y-1).Z` snapshot).
- There are no per-component hotfixes: a backport is a full umbrella rebuild at the new `X.Y.Z`.

To verify a release you installed, see [Release Verification](/community/release-verification).

---

### Deployment Ack (`docs/deployment/ack.md`)

---
title: ACK 部署
description: 在阿里云容器服务 Kubernetes 版（ACK）上部署 OpenSandbox —— 创建集群、新增节点、渲染部署各组件及 ACK 环境注意事项。
---

# ACK 部署

OpenSandbox 可部署在任何标准 Kubernetes 集群上，ACK 完全兼容标准 Kubernetes API。组件安装细节见 [Kubernetes 部署](/deployment/)，本文介绍在 ACK 上部署的具体步骤与注意事项。

## 前提条件

- 阿里云账号，已开通容器服务 ACK 并完成默认角色授权
- Kubernetes 1.21.1+
- Helm 3.x（仅用作模板渲染器）、`kubectl`
- 集群 KubeConfig 凭证（见[第二步](#第二步-连接集群)）

## 第一步：创建 ACK 集群

推荐使用 **ACK 托管集群（Pro）**，参考[创建 ACK 托管集群](https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/user-guide/create-an-ack-managed-cluster-2/)完成创建，关键配置如下：

| 配置项 | 建议 |
|--------|------|
| 集群版本 | ≥ 1.21 |
| 网络插件 | Terway（沙箱 Pod 拥有 VPC 内可直接路由的 Pod IP） |
| 初始节点池 | 2–3 台通用型实例，承载控制面组件 |
| 安全组 | 放行 Worker 节点间 VPC 内互通 |

## 第二步：连接集群

参考[获取集群 KubeConfig 并通过 kubectl 工具连接集群](https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/user-guide/obtain-the-kubeconfig-file-of-a-cluster-and-use-kubectl-to-connect-to-the-cluster/)，配置 `~/.kube/config` 后验证：

```bash
kubectl get nodes
```

## 第三步：新增节点

为沙箱工作负载创建**独立节点池**（可开启自动伸缩），与控制面组件隔离，参考[创建和管理节点池](https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/user-guide/create-a-node-pool/)；已有 ECS 实例可通过[添加已有节点](https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/user-guide/add-existing-ecs-instances-to-an-ack-cluster/)接入集群。日常管理参考[节点管理](https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/user-guide/node-management/)。

::: tip 污点隔离
沙箱节点池配置污点（Taint）时，需在 BatchSandbox Pod 模板中添加对应的 Toleration，否则沙箱 Pod 无法调度。
:::

::: warning fast-sandbox 节点池要求
fast-sandbox（Firecracker）对节点有两个要求：Worker 节点必须是**裸金属服务器**（依赖 KVM）；节点池内**所有节点的 CPU 型号必须一致**。混用不同代际的机型会导致微虚拟机快照无法跨节点恢复，因此建议锁定单一实例规格并单独建节点池。
:::

## 第四步：部署 OpenSandbox

采用 GitOps 渲染模式部署：`helm template` 将 chart 渲染为 YAML，经 `kubectl diff` 审阅后用 `kubectl apply --server-side` 应用到集群。渲染产物与 values 文件纳入 Git 管理。

按组件顺序部署（与 [Deployment Order](/deployment/#deployment-order) 一致）：

```text
base → controller → fast-sandbox* → ingress-gateway → server
```

\* `fast-sandbox` 为可选组件；启用时需在 ingress-gateway 与 server 之前部署。

注意：

- 不要与 `helm install` / `helm upgrade` 混用，两种方式会互相覆盖
- 使用 `--server-side`（Server-Side Apply）：CRD 体量超过 client-side apply 的注解上限

准备仓库与命名空间：

```bash
git clone https://github.com/opensandbox-group/OpenSandbox.git
cd OpenSandbox
git checkout release-1.1.0   # 部署对应发布版本
mkdir -p out                 # 渲染产物目录

# 渲染部署不会自动创建 namespace，需提前创建
kubectl create namespace opensandbox-system --dry-run=client -o yaml \
  | kubectl apply --server-side -f -
kubectl create namespace opensandbox --dry-run=client -o yaml \
  | kubectl apply --server-side -f -
```

### 1. 部署 base（CRD + RBAC）

```bash
helm template base manifests/charts/base > out/base.yaml

kubectl diff -f out/base.yaml
kubectl apply --server-side -f out/base.yaml
```

应用成功后输出 21 行 `serverside-applied`（1 Namespace + 7 CRD + 11 ClusterRole + 2 ClusterRoleBinding）。验证 CRD：

```bash
kubectl get crd | grep -E 'sandbox\.(opensandbox|fast)\.io'
```

```text
batchsandboxes.sandbox.opensandbox.io               2026-09-28T08:48:54Z
pools.sandbox.opensandbox.io                        2026-09-28T08:48:55Z
sandboxes.sandbox.fast.io                           2026-09-28T08:49:04Z
sandboxpools.sandbox.fast.io                        2026-09-28T08:48:59Z
sandboxsnapshots.sandbox.fast.io                    2026-09-28T08:49:06Z
sandboxsnapshots.sandbox.opensandbox.io             2026-09-28T08:48:57Z
sandboxtemplates.sandbox.fast.io                    2026-09-28T08:49:07Z
```

### 2. 部署 controller

```bash
helm template opensandbox-controller manifests/charts/controller \
  -n opensandbox-system \
  --set controller.replicaCount=2 > out/controller.yaml

kubectl diff -f out/controller.yaml
kubectl apply --server-side -f out/controller.yaml

kubectl rollout status deployment/opensandbox-controller-manager \
  --namespace opensandbox-system --timeout=180s
```

应用成功后输出 6 行 `serverside-applied`（SA + Role/Binding + ClusterRole/Binding + Deployment）。rollout 与 Pod 状态：

```text
deployment "opensandbox-controller-manager" successfully rolled out

opensandbox-controller-manager-865887c57-mvbr9   1/1   Running   0   6m25s   10.79.205.208   ap-southeast-1.10.79.205.204
opensandbox-controller-manager-865887c57-rhgxn   1/1   Running   0   37s     10.79.205.209   ap-southeast-1.10.79.205.203
```

### 3. 部署 fast-sandbox

必须在 server 之前部署：server 启动时会连接 FastPath gRPC 端点。

#### 准备制品仓库（OSS）

fast-sandbox 的 golden image 与快照存放在 S3 兼容的制品仓库。ACK 上建议使用与集群同 region 的 OSS，并通过 VPC 内网域名访问（避免公网流量）：

1. 在与集群相同的 region 创建 OSS Bucket（如 `my-sandbox-images`）
2. 创建 RAM 子账号 AK/SK，授予该 Bucket 的读写权限
3. VPC 内网 Endpoint 格式为 `https://oss-<region>.internal.aliyuncs.com`（如 `https://oss-ap-southeast-1.internal.aliyuncs.com`）

#### 配置凭证 Secret

::: warning
registry.json 的最终 schema 由 fast-sandbox 上游编译工具生成，以下为**占位符**示例，仅用于让 Pod 正常启动；实际拉取制品前需按工具产物补全。
:::

```bash
cat > /tmp/registry.json <<'EOF'
{
  "endpoint": "https://oss-ap-southeast-1.internal.aliyuncs.com",
  "bucket": "my-sandbox-images",
  "accessKeyId": "<YOUR_ACCESS_KEY_ID>",
  "accessKeySecret": "<YOUR_ACCESS_KEY_SECRET>"
}
EOF

kubectl create secret generic fast-sandbox-agent-registry \
  -n opensandbox-system \
  --from-file=registry.json=/tmp/registry.json \
  --dry-run=client -o yaml | kubectl apply --server-side -f -
rm /tmp/registry.json   # AK/SK 不落盘、不进 Git
```

渲染并部署（`artifactStore` 与 Bucket、Endpoint 保持一致）：

```bash
helm template fast-sandbox manifests/charts/fast-sandbox \
  -n opensandbox-system \
  --set artifactStore.store=s3://my-sandbox-images/publish \
  --set artifactStore.endpoint=https://oss-ap-southeast-1.internal.aliyuncs.com \
  > out/fast-sandbox.yaml

kubectl diff -f out/fast-sandbox.yaml
kubectl apply --server-side -f out/fast-sandbox.yaml
```

应用成功后输出 13 行 `serverside-applied`（PDB + SA + Secret/ConfigMap + RBAC + 2 Service + DaemonSet + Deployment）。验证 Pod 状态与节点标签：

```text
fast-sandbox-controller-7d79b6447b-8q5bp   1/1   Running
firecracker-runtime-hkg9d                  2/2   Running
firecracker-runtime-z8mq9                  2/2   Running

NAME                           STATUS   KVM    FIRECRACKER-NODE
ap-southeast-1.10.79.205.203   Ready    true   true
ap-southeast-1.10.79.205.204   Ready    true   true
```

runtime 就绪后会为节点自动打 4 个标签（`kvm`、`firecracker-node`、`cpu-identity`、`cpu-template`）。其中 `cpu-identity` 标识 CPU 型号（厂商-家族-型号），同一节点池内必须一致，否则微虚拟机快照无法跨节点恢复：

```bash
kubectl get nodes -L sandbox.fast.io/kvm,fast-sandbox.io/cpu-identity,fast-sandbox.io/firecracker-node
```

::: warning 节点需加载 kvm 内核模块
ECS 裸金属节点默认不加载 kvm 模块，readiness 会报 `/dev/kvm unusable: device does not exists`。此时需在节点上执行 `modprobe kvm kvm_amd`（可通过 privileged Pod 的 `nsenter -t 1 -m` 或节点 SSH），再执行 `kubectl rollout restart daemonset/firecracker-runtime -n opensandbox-system` 让 Pod 重建 `/dev`。模块加载在节点重启后失效，需重新执行；建议通过节点池 bootstrap 脚本或自定义镜像持久化。
:::

模板构建、镜像准备等前置步骤见 [fast-sandbox runtime 部署指南](https://github.com/opensandbox-group/OpenSandbox/blob/main/manifests/HELM-DEPLOYMENT.md#fast-sandbox-runtime-firecracker)。

### 4. 部署 ingress-gateway

Kubernetes 环境中沙箱 Pod 只有 ClusterIP，客户端流量须经 ingress-gateway 路由。在 server 之前部署，这样 server 首次安装即可完成网关公告配置。

`providerType=fast-sandbox` 时网关强制要求 secure-access 签名密钥环（OSEP-0011）：server 用它签发路由令牌，网关用它验证。先生成密钥，部署 server 时复用：

```bash
KEY=$(openssl rand -base64 32)   # 保存好，部署 server 时复用

helm template ingress-gateway manifests/charts/ingress-gateway \
  -n opensandbox-system \
  --set gateway.providerType=fast-sandbox \
  --set gateway.fastpathEndpoint=fast-sandbox-fastpath.opensandbox-system.svc:9090 \
  --set "gateway.secureAccess.keys[0].key_id=a" \
  --set "gateway.secureAccess.keys[0].key=$KEY" \
  > out/ingress-gateway.yaml

kubectl diff -f out/ingress-gateway.yaml
kubectl apply --server-side -f out/ingress-gateway.yaml

kubectl rollout status deployment/opensandbox-ingress-gateway \
  --namespace opensandbox-system --timeout=180s
```

验证：

```text
deployment "opensandbox-ingress-gateway" successfully rolled out

opensandbox-ingress-gateway-66bd4bf7b7-59nfz   1/1   Running   0   67s
opensandbox-ingress-gateway-66bd4bf7b7-dbc2v   1/1   Running   0   56s
```

### 5. 部署 lifecycle server

创建 API Key Secret（生产环境建议使用外部密钥管理系统）：

```bash
read -s OPENSANDBOX_API_KEY
kubectl create secret generic opensandbox-api-key \
  --namespace opensandbox-system \
  --from-literal=api-key="${OPENSANDBOX_API_KEY}" \
  --dry-run=client -o yaml | kubectl apply -f -
unset OPENSANDBOX_API_KEY
```

在 values 文件中引用 Secret 并配置网关公告。注意 `secureAccess` 必须与 ingress-gateway 使用同一把密钥（`activeKey` 对应 key_id）；`gatewayRouteMode` 需与 ingress-gateway chart 的 `gateway.gatewayRouteMode` 一致（默认均为 `header`）：

```yaml
# values-server.yaml
server:
  env:
    - name: OPENSANDBOX_SERVER_API_KEY
      valueFrom:
        secretKeyRef:
          name: opensandbox-api-key
          key: api-key
  gateway:
    enabled: true
    host: opensandbox-ingress-gateway.opensandbox-system.svc
    secureAccess:
      activeKey: a
      keys:
        - key_id: a
          key: <部署网关时生成的 base64 密钥>
```

::: info
`configToml`（沙箱工作负载命名空间、镜像地址、运行时等）与 PostgreSQL 持久化等也在这个文件中配置，渲染前补齐。完整参考见 [Kubernetes 部署](/deployment/)。
:::

渲染并部署：

```bash
helm template opensandbox-server manifests/charts/server \
  -n opensandbox-system \
  -f values-server.yaml > out/server.yaml

kubectl diff -f out/server.yaml
kubectl apply --server-side -f out/server.yaml

kubectl rollout status deployment/opensandbox-server \
  --namespace opensandbox-system --timeout=180s

kubectl port-forward --namespace opensandbox-system \
  service/opensandbox-server 8080:80
curl --fail http://127.0.0.1:8080/health
```

预期输出：

```text
deployment "opensandbox-server" successfully rolled out

{"status":"healthy"}
```

### 可选：部署共享 SandboxPool

SandboxPool（`sandbox.fast.io`）是 fast-sandbox 的容量与策略单元，定义 Fastlet 的预热规模（`capacity`）、运行时 profile（`runtime`）、每个沙箱的资源规格（`sandboxResources`）与调度约束（`fastletTemplate`），同一池中的沙箱共享这些设置。仓库提供了示例 `manifests/examples/sandboxpool-fast-sandbox.yaml`：

```yaml
apiVersion: sandbox.fast.io/v1alpha2
kind: SandboxPool
metadata:
  name: shared-pool
  namespace: opensandbox-dataplane
spec:
  runtime: firecracker
  sandboxResources:
    cpu: "1"
    memory: 2Gi
    pids: 256
  capacity:
    poolMin: 5
    poolMax: 10
    bufferMin: 0
    bufferMax: 2
  maxSandboxesPerPod: 4
  fastletTemplate:
    spec:
      containers:
      - name: fastlet
        image: opensandbox/fsb-fastlet:release-1.1.1-rc.1
        imagePullPolicy: IfNotPresent
        env:
        - name: FAST_SANDBOX_RUNTIME_AGENT_SOCKET
          value: /run/fast-sandbox/firecracker/runtime.sock
        volumeMounts:
        - name: agent-socket
          mountPath: /run/fast-sandbox/firecracker
      volumes:
      - name: agent-socket
        hostPath:
          path: /run/fast-sandbox/firecracker
          type: DirectoryOrCreate
      nodeSelector:
        fast-sandbox.io/firecracker-node: "true"
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchLabels:
                  app: sandbox-fastlet
              topologyKey: kubernetes.io/hostname
  warmImages: []
```

注意：`fastletTemplate` 必须包含名为 `fastlet` 的容器（fast-sandbox companion 镜像），并挂载节点上 runtime-agent 的 UDS socket，否则 controller 会拒绝处理。需要 egress 网络策略时，参考集成环境的完整示例 `scripts/fast-sandbox-env/manifests/pool/firecracker-egress-pool.yaml`（含 egress sidecar 与 `infraComponents`/`actionHandlers` 声明）。

部署并验证：

```bash
kubectl apply --server-side -f manifests/examples/sandboxpool-fast-sandbox.yaml

kubectl get pods -n opensandbox-dataplane
kubectl get sandboxpool shared-pool -n opensandbox-dataplane -o jsonpath='{.status.conditions[?(@.type=="RuntimeReady")].status}'
```

`poolMin=5` 表示预热 5 个 Fastlet（反亲和分散到各节点），全部就绪后 `RuntimeReady` 转为 `True`：

```text
NAME                        READY   STATUS    RESTARTS   AGE
shared-pool-fastlet-5nh85   2/2     Running   0          54s
shared-pool-fastlet-cxrln   2/2     Running   0          54s
shared-pool-fastlet-gzhml   2/2     Running   0          54s
shared-pool-fastlet-wnlsx   2/2     Running   0          54s
shared-pool-fastlet-zdrd4   2/2     Running   0          54s

True
```

### 升级

切换到新版本标签，重新渲染全部组件，diff 审阅后逐个 apply：

```bash
git fetch --tags
git checkout release-1.2.0

helm template base manifests/charts/base > out/base.yaml
helm template opensandbox-controller manifests/charts/controller \
  -n opensandbox-system > out/controller.yaml
helm template ingress-gateway manifests/charts/ingress-gateway \
  -n opensandbox-system \
  --set gateway.fastpathEndpoint=fast-sandbox-fastpath.opensandbox-system.svc:9090 \
  > out/ingress-gateway.yaml
helm template opensandbox-server manifests/charts/server \
  -n opensandbox-system -f values-server.yaml > out/server.yaml

kubectl diff -f out/base.yaml && kubectl apply --server-side -f out/base.yaml
kubectl diff -f out/controller.yaml && kubectl apply --server-side -f out/controller.yaml
kubectl diff -f out/server.yaml && kubectl apply --server-side -f out/server.yaml
```

`kubectl diff` 在存在差异时退出码为 `1`，脚本化时不应视为失败。

### 对外暴露 Server

在 `values-server.yaml` 中设置 `server.service.type: LoadBalancer`（ACK 会自动创建 SLB），重新渲染并 apply。生产环境建议使用内网 SLB，沙箱流量经由 ingress gateway。

## ACK 环境注意事项

| 主题 | 说明 |
|------|------|
| 网络插件 | 推荐 Terway；Flannel 模式下 Pod IP 为 overlay 地址，对外暴露沙箱前需确认路由可达 |
| fast-sandbox | 必须使用裸金属节点（KVM）且 CPU 型号一致，否则跳过该组件，使用默认运行时。详见 [fast-sandbox runtime](https://github.com/opensandbox-group/OpenSandbox/blob/main/manifests/HELM-DEPLOYMENT.md#fast-sandbox-runtime-firecracker) |
| 镜像拉取 | 默认镜像托管在 Docker Hub，VPC 内建议通过 ACR 同步后覆盖 `server.image.repository` 及 `configToml` 中的沙箱镜像地址 |
| Server 持久化 | 生产环境建议使用 RDS for PostgreSQL（见 [PostgreSQL 持久化](/deployment/#use-postgresql-for-server-persistence)） |
| 监控 | Controller 的 Prometheus 指标见 [Operator Metrics](/deployment/#operator-metrics)，可接入 ARMS Prometheus |

## 卸载

按部署的逆序删除渲染产物。注意：删除 `out/base.yaml` 会连同 CRD 一起删除（kubectl 不理会 `resource-policy: keep`），并级联删除所有自定义资源：

```bash
kubectl delete -f out/server.yaml
kubectl delete -f out/ingress-gateway.yaml
kubectl delete -f out/fast-sandbox.yaml      # 如部署
kubectl delete -f out/controller.yaml
kubectl delete -f out/base.yaml
```

如需保留 CRD、仅删除组件，跳过 `out/base.yaml` 即可；CRD 清理见 [Kubernetes 部署 → Uninstall](/deployment/#uninstall)。

## 相关链接

- [Kubernetes 部署](/deployment/) — 组件完整安装与配置
- [Kubernetes Controller 概述](/architecture/control-plane/operator) — CRD 与控制器机制
- [ACK 快速入门](https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/getting-started/)
- [创建 ACK 托管集群](https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/user-guide/create-an-ack-managed-cluster-2/)
- [创建和管理节点池](https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/user-guide/create-a-node-pool/)
- [添加已有节点](https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/user-guide/add-existing-ecs-instances-to-an-ack-cluster/)

---

### Examples Agent Sandbox (`docs/examples/agent-sandbox.md`)

---
title: Agent Sandbox
description: Create a kubernetes-sigs/agent-sandbox instance and run a command using the OpenSandbox Python SDK.
---

# Agent-Sandbox Example

This example creates a sandbox backed by `kubernetes-sigs/agent-sandbox` and executes `echo hello world` via the OpenSandbox Python SDK.

## Prerequisites

- A Kubernetes cluster with agent-sandbox >= v0.5.0 controller and CRDs installed, serving `agents.x-k8s.io/v1beta1`.
- OpenSandbox server configured with Kubernetes runtime and `workload_provider = "agent-sandbox"`.
- Sandbox image should include `bash` (default example uses `ubuntu:22.04`).

## Compatibility and migration

The server provider and ingress informer use `agents.x-k8s.io/v1beta1` only. This API is available starting in agent-sandbox v0.5.0; v1.0.0 removes `v1alpha1`. Upgrade older controller and CRD installations before using this integration; there is no `v1alpha1` fallback.

For an existing installation, first upgrade agent-sandbox to a release that serves both versions (such as v0.5.0), with its conversion webhook healthy, then upgrade the OpenSandbox server and ingress together. Complete the upstream resource/storage migration before installing an agent-sandbox release that removes the alpha API.

Update custom AgentSandbox templates to use `apiVersion: agents.x-k8s.io/v1beta1` and remove `spec.replicas`. The server sets `spec.operatingMode: Running` and `spec.service: true`, including when merging a custom template. Service creation is required for ingress routing through `status.serviceFQDN`.

The server applies these fields when creating new sandboxes; it does not rewrite existing resources. On v0.5.0, alpha-to-beta conversion preserves an omitted `service` field. If an existing sandbox has no Service/FQDN, explicitly enable `spec.service: true` on that resource and wait for `Ready=True` and a populated `status.serviceFQDN` before using ingress. This can restore routing without recreating the Sandbox. Keep intentionally service-free resources unchanged.

The configured `shutdownPolicy`, pod template merging, and absolute `shutdownTime` semantics are preserved. Requests without an expiration omit `shutdownTime`, including any value supplied by a template. Status and endpoint resolution still use `Ready`, `selector`, `podIPs`, and `serviceFQDN`.

OpenSandbox's own BatchSandbox API remains `sandbox.opensandbox.io/v1alpha1`.

## Pause and resume

This provider supports the standard `POST /v1/sandboxes/{id}/pause` and `POST /v1/sandboxes/{id}/resume` endpoints. Both are implemented by patching the `Sandbox` CRD's `spec.operatingMode` field.

Pause patches `spec.operatingMode: Suspended`. The agent-sandbox controller terminates and destroys the Pod but retains the `Sandbox` object, so the `sandboxId` stays stable across pause/resume cycles. The sandbox reports `Running` → `Pausing` → `Paused`; while the Pod is terminating, the status `reason` comes from the `Suspended` condition (e.g. `PodTerminating`).

Resume patches `spec.operatingMode: Running`. The controller recreates the Pod from the Sandbox spec, so expect scheduling and image-pull latency before the sandbox is usable again. The sandbox reports `Paused` → `Resuming` → `Pending` → `Running`.

| | Preserved? |
|--|-----------|
| Persistent volume (PVC) data | ✅ Yes — volumes are retained with the Sandbox object |
| `sandboxId` and sandbox metadata | ✅ Yes |
| Memory / running processes | ❌ No — the Pod is destroyed |
| Root filesystem runtime state | ❌ No — the Pod is recreated from the original image on resume |

Anything not written to a persistent volume is lost on pause.

A paused sandbox still expires at its original timeout; a retained expired sandbox reports `Terminated` and can no longer be paused or resumed.

## Start OpenSandbox server

1. Install the server package and fetch the example config for agent-sandbox:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
```

2. Update `~/.sandbox.toml` with the following sections:

```toml
[runtime]
type = "kubernetes"
execd_image = "opensandbox/execd:v1.1.0"

[kubernetes]
namespace = "default"
# kubeconfig_path = "/absolute/path/to/kubeconfig"  # optional if running in-cluster
workload_provider = "agent-sandbox"

[agent_sandbox]
shutdown_policy = "Delete"
```

3. Start the server:

```shell
opensandbox-server
```

## Run the example

```shell
uv pip install opensandbox
uv run python examples/agent-sandbox/main.py
```

## Expected output

```text
command output: hello world
```

## References

- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/agent-sandbox)

---

### Examples Aio Sandbox (`docs/examples/aio-sandbox.md`)

---
title: AIO Sandbox
description: Create and access an All-in-One (AIO) Sandbox via OpenSandbox.
---

# All-in-One (AIO) Sandbox Example

This example demonstrates how to create and access an [All-in-One (AIO) Sandbox](https://github.com/agent-infra/sandbox) via OpenSandbox.

## Start OpenSandbox server [local]

You can find the latest version [here](https://github.com/agent-infra/sandbox/pkgs/container/sandbox).

You can pre-pull the target image which is used in the example.

::: info Docker runtime requirement
The server is configured with `runtime.type = "docker"` by default, so it **must** be able to connect to a running Docker daemon.

- **Docker Desktop**: ensure Docker Desktop is running, then verify with `docker version`.
- **Colima (macOS)**: start it first (`colima start`) and export the socket before starting the server:

```shell
export DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock"
```
:::

```shell
# pre-pull target image
docker pull ghcr.io/agent-infra/sandbox:latest
```

Then, start the OpenSandbox server, you can obtain stdout log from terminal.

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

::: tip
`opensandbox-server` runs in the foreground and will keep the current terminal session busy. The example code lives in this repository -- clone it and, in a new terminal window/tab, `cd` into the project root before running the AIO sandbox creation steps below.

If you see errors like `FileNotFoundError: [Errno 2] No such file or directory` from `docker/transport/unixconn.py`, it usually means the Docker unix socket is missing / Docker daemon is not running.
:::

## Create and Access the AIO Sandbox Instance

This example uses a fixed configuration for quick start:
- OpenSandbox server: `http://localhost:8080`
- Image: `ghcr.io/agent-infra/sandbox:latest`
- AIO port: `8080`
- Timeout: `300s`

Install dependencies with uv under project root:

```shell
uv pip install opensandbox agent-sandbox==0.0.18
```

Run the example (it will create a sandbox via OpenSandbox, wait until it's Running, then connect to it via agent-sandbox):

```shell
uv run python examples/aio-sandbox/main.py
```

Subsequently, you will instantiate an AIO sandbox, navigate to Google, capture a screenshot, and download it to your local environment.

```text
Creating AIO sandbox with image=ghcr.io/agent-infra/sandbox:latest on OpenSandbox server http://localhost:8080...
[check] sandbox ready after 7.1s
AIO portal endpoint: 127.0.0.1:56123
...
Screenshot saved to sandbox_screenshot.png
```



## More examples

For more examples of using the AIO Sandbox, refer to agent-infra/sandbox [examples](https://github.com/agent-infra/sandbox/tree/main/examples).

## References

- [AIO Sandbox](https://github.com/agent-infra/sandbox/tree/main)
- [AIO Sandbox Python SDK](https://github.com/agent-infra/sandbox/tree/main/sdk/python)
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/aio-sandbox)

---

### Examples Aks Kata (`docs/examples/aks-kata.md`)

---
title: AKS + Kata
description: Deploy OpenSandbox onto an AKS cluster with Kata VM isolation and interact with a Kata-isolated sandbox step by step, covering ingress, egress, and Credential Vault.
---

# AKS Kata Example

Deploy OpenSandbox onto an AKS cluster with `kata-vm-isolation`, then interact with a Kata-isolated sandbox step by step.

::: warning Demo credentials
The Helm values in this example use **insecure, well-known demo credentials** (`api_key = "aks-kata-demo-key"` and a fixed `secureAccess` signing key) so the walkthrough works out of the box with local `kubectl port-forward`. **Change them before exposing the server or gateway beyond local port-forwarding.** See [Security Model](#security-model).
:::

## Prerequisites

- AKS cluster with `kubectl get runtimeclass` showing `kata-vm-isolation`
- Helm 3
- Python 3.10+ with `pip install opensandbox requests`
- Azure OpenAI resource (endpoint + API key)

## Files

The example lives in [`examples/aks-kata`](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/aks-kata):

| File | Purpose |
|------|---------|
| `main.py` | CLI tool — create, inspect, and operate on sandboxes one step at a time |
| `controller-values.yaml` | Helm values for the OpenSandbox controller |
| `server-values.yaml` | Helm values for the lifecycle server and ingress gateway |
| `batchsandbox-template-configmap.yaml` | BatchSandbox template that pins pods onto Kata nodes |

## 1. Install OpenSandbox

From the repo root:

```bash
kubectl create namespace opensandbox-system --dry-run=client -o yaml | kubectl apply -f -
kubectl create namespace opensandbox --dry-run=client -o yaml | kubectl apply -f -

kubectl apply -f examples/aks-kata/batchsandbox-template-configmap.yaml

# The controller chart no longer ships the CRDs; install base first on a fresh cluster
helm upgrade --install base ./manifests/charts/base

helm upgrade --install opensandbox-controller ./manifests/charts/controller \
  --namespace opensandbox-system \
  -f examples/aks-kata/controller-values.yaml

helm upgrade --install opensandbox-server ./manifests/charts/server \
  --namespace opensandbox-system \
  -f examples/aks-kata/server-values.yaml
```

Wait for the deployments:

```bash
kubectl rollout status deploy/opensandbox-controller-manager -n opensandbox-system --timeout=180s
kubectl rollout status deploy/opensandbox-server -n opensandbox-system --timeout=180s
kubectl rollout status deploy/opensandbox-ingress-gateway -n opensandbox-system --timeout=180s
```

## 2. Start port-forwards

Open two separate terminals:

```bash
# Terminal 1 — lifecycle server
kubectl port-forward -n opensandbox-system svc/opensandbox-server 18080:80

# Terminal 2 — ingress gateway
kubectl port-forward -n opensandbox-system svc/opensandbox-ingress-gateway 28080:80
```

## 3. Set environment variables

```bash
export SANDBOX_DOMAIN=http://127.0.0.1:18080
export SANDBOX_API_KEY=aks-kata-demo-key

export AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
export AZURE_OPENAI_API_KEY=your-real-key
# Optional:
# export AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini
```

## 4. Enable Pause/Resume (Optional)

Pause commits the sandbox rootfs as an OCI image and pushes it to a registry. Resume recreates the sandbox from that image. Without a registry, the `pause` and `resume` steps will log an error and continue.

The steps below use Azure Container Registry (ACR). Any OCI registry works — see the [Pause / Resume guide](/guides/pause-resume) for the full guide.

### Step 1: Grant AcrPush to the kubelet identity

```bash
KUBELET_ID=$(az aks show \
  -g <resource-group> -n <cluster-name> \
  --query "identityProfile.kubeletidentity.clientId" -o tsv)

ACR_ID=$(az acr show --name <acr-name> --query id -o tsv)

az role assignment create --assignee "$KUBELET_ID" --role AcrPush --scope "$ACR_ID"
```

### Step 2: Create the push/pull secret

```bash
ACR_PASSWORD=$(az acr credential show --name <acr-name> \
  --query "passwords[0].value" -o tsv)

kubectl create secret docker-registry acr-snapshot-push-secret \
  --docker-server=<acr-name>.azurecr.io \
  --docker-username=<acr-name> \
  --docker-password="$ACR_PASSWORD" \
  --namespace=opensandbox
```

### Step 3: Upgrade the controller

```bash
helm upgrade opensandbox-controller ./manifests/charts/controller \
  --namespace opensandbox-system \
  --reuse-values \
  --set controller.snapshot.registry=<acr-name>.azurecr.io/opensandbox-snapshots \
  --set controller.snapshot.snapshotPushSecret=acr-snapshot-push-secret \
  --set controller.snapshot.resumePullSecret=acr-snapshot-push-secret

kubectl rollout status deploy/opensandbox-controller-manager \
  -n opensandbox-system --timeout=90s
```

::: tip
`controller.snapshot.containerdSocketPath` defaults to `""` in the chart, which allows the controller to use its built-in default (`/var/run/containerd/containerd.sock`) without passing the `--containerd-socket-path` flag unless explicitly configured. If your nodes use a non-default containerd socket and you deploy a controller image that supports the flag, set this value accordingly.
:::

## 5. Use `main.py`

All `main.py` commands below are run from the example directory:

```bash
cd examples/aks-kata
```

### Run everything end to end

```bash
python3 main.py all
```

This creates a sandbox, runs every demo step, and deletes it at the end.

### Run one step at a time

#### Create a sandbox

```bash
python3 main.py create
```

Expected output:

```
Creating Kata-isolated sandbox on AKS...
  OpenSandbox API: http://127.0.0.1:18080
  Sandbox image:   sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0

SANDBOX_ID=96045ee2-6614-435c-9fa8-d4b6f9592598

Use --sandbox-id 96045ee2-6614-435c-9fa8-d4b6f9592598 for subsequent steps.
```

Save the ID for the following steps:

```bash
export SANDBOX_ID=96045ee2-6614-435c-9fa8-d4b6f9592598
```

Verify the pod is running on a Kata node:

```bash
kubectl get pod -n opensandbox -o wide
# NODE column should show aks-sandboxagent-*

kubectl get pod -n opensandbox -o jsonpath='{.items[0].spec.runtimeClassName}'
# Expected: kata-vm-isolation

python3 main.py exec --sandbox-id $SANDBOX_ID -c "uname -r"
# Expected: 6.6.137.mshv1-1.azl3 (mshv = Microsoft Hypervisor = Kata VM guest kernel)
```

#### Set up Credential Vault

```bash
python3 main.py credentials --sandbox-id $SANDBOX_ID
```

Expected output:

```
[credentials] Credential Vault configured.
```

#### Ask the LLM a question

```bash
python3 main.py llm --sandbox-id $SANDBOX_ID -q "What is the capital of France? Reply in one word."
```

Expected output:

```
[llm] Question: What is the capital of France? Reply in one word.
[llm] Model:    gpt-4o-mini
[llm] Answer:   Paris
```

The sandbox only has a fake API key — the real key is injected by the egress sidecar via Credential Vault. You can verify:

```bash
python3 main.py exec --sandbox-id $SANDBOX_ID -c "echo \$AZURE_OPENAI_API_KEY"
# Expected: fake-key-inside-sandbox
```

#### Run commands inside the sandbox

```bash
python3 main.py exec --sandbox-id $SANDBOX_ID -c "uname -a"
# [exec][stdout] Linux ...-0 6.6.137.mshv1-1.azl3 ... x86_64 GNU/Linux

python3 main.py exec --sandbox-id $SANDBOX_ID -c "cat /etc/os-release | head -3"
# [exec][stdout] PRETTY_NAME="Ubuntu 24.04.4 LTS"
# [exec][stdout] NAME="Ubuntu"
# [exec][stdout] VERSION_ID="24.04"

python3 main.py exec --sandbox-id $SANDBOX_ID -c "ls -la /tmp/www/"
# Shows the HTTP server root directory
```

#### Fetch from the sandbox HTTP server

The sandbox runs `python3 -m http.server 8080` serving `/tmp/www/`. Traffic goes through the ingress gateway with secure-access headers.

```bash
# Write a file into the sandbox, then fetch it via the gateway
python3 main.py exec --sandbox-id $SANDBOX_ID -c "echo hello > /tmp/www/greeting.txt"
python3 main.py http --sandbox-id $SANDBOX_ID -p /greeting.txt
# [http] GET /greeting.txt -> 200 (6 bytes)
# hello

# Directory listing
python3 main.py http --sandbox-id $SANDBOX_ID -p /
# Shows HTML directory listing

# 404 for missing file
python3 main.py http --sandbox-id $SANDBOX_ID -p /nonexistent.txt
# [http] GET /nonexistent.txt -> 404
```

#### Check sandbox status

```bash
python3 main.py status --sandbox-id $SANDBOX_ID
# Sandbox:  96045ee2-6614-435c-9fa8-d4b6f9592598
# State:    Running
# Image:    ...code-interpreter:v1.1.0
```

#### Pause and resume

Pause snapshots the container rootfs to the registry (1–5 min). Resume recreates the sandbox from that snapshot. Requires step 4 above.

```bash
python3 main.py pause --sandbox-id $SANDBOX_ID
# [lifecycle] pausing sandbox...
# [lifecycle] sandbox is PAUSED

python3 main.py status --sandbox-id $SANDBOX_ID
# State: Paused

# Pod is gone, but the BatchSandbox CR and snapshot remain
kubectl get pods -n opensandbox
# No resources found
kubectl get sandboxsnapshot -n opensandbox
# Shows snapshot with phase Succeed

python3 main.py resume --sandbox-id $SANDBOX_ID
# [lifecycle] resuming sandbox...
# [setup] sandbox state: Resuming, waiting...
# [lifecycle] state after resume: Running
# [http] GET /index.html -> 200 (filesystem preserved!)

python3 main.py status --sandbox-id $SANDBOX_ID
# State: Running
```

#### Delete the sandbox

```bash
python3 main.py delete --sandbox-id $SANDBOX_ID
# [lifecycle] sandbox 96045ee2-... deleted.

kubectl get pods -n opensandbox
# No resources found
```

Without `-c`, `-q`, or `-p`, the `exec`, `llm`, and `http` steps run their built-in demo sequences.

## What gets installed

| Component | Namespace | Purpose |
|-----------|-----------|---------|
| `opensandbox-controller` | `opensandbox-system` | Kubernetes operator — manages BatchSandbox CRDs, pools, and snapshots |
| `opensandbox-server` | `opensandbox-system` | Lifecycle API — creates/deletes sandboxes, proxies SDK calls |
| `opensandbox-ingress-gateway` | `opensandbox-system` | Routes external HTTP traffic into sandbox pods with auth |
| BatchSandbox template | `opensandbox-system` | Pins sandbox pods onto Kata-capable AKS nodes via `nodeSelector` |

The server is configured with:

- `workload_provider = "batchsandbox"`
- `secure_runtime.type = "kata"` / `k8s_runtime_class = "kata-vm-isolation"`
- egress sidecar (`dns+nft` mode)
- ingress gateway (`header` routing with `secureAccess`)

## Security Model

::: warning
`server-values.yaml` ships with **demo-only** credentials: `api_key = "aks-kata-demo-key"` and a fixed base64 `secureAccess` signing key. They exist so the local port-forward walkthrough works without extra setup. Replace both with unique secret values (for example a random key and `openssl rand -base64 32` for the signing key) before exposing the server or gateway to anything other than `127.0.0.1`.
:::

### Kata VM isolation

Each sandbox runs inside a dedicated Kata VM (`runtimeClassName: kata-vm-isolation`) on an AKS Kata node pool (`nodeSelector: kubernetes.azure.com/kata-vm-isolation: "true"`). The sandbox sees its own guest kernel, not the host kernel.

### Egress isolation

The create request sets a deny-by-default outbound policy. Only explicitly allowed hosts (Azure OpenAI, pypi.org, files.pythonhosted.org) can be reached.

### Credential Vault

The sandbox only sees a fake `AZURE_OPENAI_API_KEY`. The real key is stored in Credential Vault and injected by the egress sidecar as the `api-key` header only for matching outbound requests to `https://<your-resource>.openai.azure.com/openai/*`.

### Ingress gateway + secureAccess

All external traffic to sandbox services flows through the ingress gateway. The gateway validates the `OpenSandbox-Secure-Access` token and routes via the `OpenSandbox-Ingress-To` header. Both are returned by the SDK's `get_endpoint()` call — callers never need to construct them manually.

### Per-service auth

Even if someone bypasses the gateway and reaches the pod directly:
- **execd** (port 44772) requires its own access token header
- **egress sidecar** (port 18080) requires the `OPENSANDBOX-EGRESS-AUTH` header
- Only the user's HTTP server (port 8080) has no built-in auth — that's what secureAccess protects

## Cleanup

Remove everything installed by this example:

```bash
# 1) Delete any running sandboxes
kubectl delete batchsandbox --all -n opensandbox

# 2) Uninstall Helm releases
helm uninstall opensandbox-server -n opensandbox-system
helm uninstall opensandbox-controller -n opensandbox-system

# 3) Remove the BatchSandbox template
kubectl delete configmap aks-kata-batchsandbox-template -n opensandbox-system

# 4) Remove the snapshot push/pull secret (if created)
kubectl delete secret acr-snapshot-push-secret -n opensandbox --ignore-not-found

# 5) Delete the namespaces
kubectl delete namespace opensandbox
kubectl delete namespace opensandbox-system
```

---

### Examples Apply Patch (`docs/examples/apply-patch.md`)

---
title: Apply Model Patches
description: Apply Codex-style file patches in an OpenSandbox sandbox with a custom image.
---

# Apply Model Patches

OpenSandbox exposes file operations and command execution, but the default `execd` image does not include a patch interpreter. The `/files/replace` API performs literal string replacement; it does not understand contextual hunks, file creation, deletion, or moves.

If your coding agent emits the patch format used by OpenAI Codex, install the standalone `apply_patch` executable in a custom sandbox image and run it inside the sandbox. This keeps the patch dialect in the agent image instead of adding vendor-specific behavior to the OpenSandbox file API.

## Build a custom image

The following example builds the executable from a pinned Codex revision and copies it into the OpenSandbox `execd` image. Pin `CODEX_REF` to a reviewed commit in production so image builds remain reproducible.

```dockerfile
FROM rust:alpine AS apply-patch-builder

ARG CODEX_REF=<reviewed-commit>
RUN apk add --no-cache build-base git musl-dev
WORKDIR /src
RUN git clone --depth 1 https://github.com/openai/codex.git . \
    && git fetch --depth 1 origin ${CODEX_REF} \
    && git checkout ${CODEX_REF}
WORKDIR /src/codex-rs
RUN cargo build --release --locked -p codex-apply-patch

FROM opensandbox/execd:v1.1.0
COPY --from=apply-patch-builder /src/codex-rs/target/release/apply_patch /usr/local/bin/apply_patch
RUN chmod 0755 /usr/local/bin/apply_patch
```

Build and publish the image, then use its name when creating the sandbox:

```shell
docker build --build-arg CODEX_REF=<reviewed-commit> \
  -t my-registry/opensandbox-execd-with-patch:1 .
docker push my-registry/opensandbox-execd-with-patch:1
```

The builder and runtime stages must produce compatible binaries. If your base image is not Alpine, use a builder with the matching C library and toolchain.

## Apply a patch from the Python SDK

Write the model output to a temporary file, then invoke `apply_patch` through the existing command API. The executable accepts either one patch argument or the patch on standard input; using a file avoids shell quoting and argument length problems.

Run the following snippet inside an async function. It assumes `/workspace/hello.py` already exists and contains `print("hello")`.

```python
import sys

from opensandbox import Sandbox

patch = """*** Begin Patch
*** Update File: /workspace/hello.py
@@
-print("hello")
+print("hello from OpenSandbox")
*** End Patch
"""

async with (await Sandbox.create(image="my-registry/opensandbox-execd-with-patch:1")) as sandbox:
    await sandbox.files.write_file("/tmp/change.patch", patch)
    result = await sandbox.commands.run(
        "cd /workspace && apply_patch < /tmp/change.patch"
    )
    stdout = "".join(message.text for message in result.logs.stdout)
    stderr = "".join(message.text for message in result.logs.stderr)
    print(stdout, end="")
    print(stderr, end="", file=sys.stderr)
    if result.exit_code != 0:
        raise RuntimeError(f"apply_patch failed (exit code: {result.exit_code})")
```

Patch paths are resolved relative to the command working directory unless the patch uses an absolute path. Keep the working directory and patch paths inside the sandbox workspace, and treat patch content as untrusted input from the model.

## Operational notes

- `apply_patch` can add, update, delete, and move files and matches update hunks using surrounding context.
- A failed patch can leave earlier file changes committed. Check the command exit status and inspect the affected files before retrying.
- The default OpenSandbox images remain unchanged. Use the custom image only for agents that need this patch format.
- For agents that already manage their own editing tools, continue using the regular file and command APIs instead of installing this executable.

See the [OpenAPI specifications](/api/) and the [Codex CLI example](/examples/codex-cli) for the surrounding sandbox setup.

---

### Examples Chrome (`docs/examples/chrome.md`)

---
title: Chrome
description: Run Chrome Browser with OpenSandbox runtime for remote debugging via DevTools.
---

# Chrome Browser in OpenSandbox

This example runs Chrome Browser with OpenSandbox runtime.

The image starts a VNC server (`Xtigervnc :1`) and launches Chromium with remote debugging enabled on port `9222`.

## Getting Chrome image

You can build the image from source or pull it from Docker Hub.

### Build from source

```shell
cd examples/chrome
docker build -t opensandbox/chrome .
```

### Pull an existing image

```shell
docker pull opensandbox/chrome:latest

# use acr from china
# docker pull sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/chrome:latest
```

## Start OpenSandbox server

Start the OpenSandbox server and tail stdout from the terminal:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Create and access a Chrome sandbox

Build/pull the image above, then create a sandbox with image `opensandbox/chrome:latest` and an entrypoint that keeps it alive (e.g., `["/bin/sh", "-c", "sleep infinity"]`), or reuse `tail -f /dev/null`. Make sure the runtime exposes ports `5901` and `9222` for VNC/DevTools.

```shell
uv pip install opensandbox
uv run python examples/chrome/main.py
```

Then fetch endpoints for 5901/9222 to connect with a VNC client or DevTools, like:

```text
execd daemon running with endpoint='127.0.0.1:48379/proxy/44772'
VNC running with endpoint='127.0.0.1:48379/proxy/5901'
DevTools running with endpoint='127.0.0.1:48379/proxy/9222'/json
```

```text
[ {
   "description": "",
   "devtoolsFrontendUrl": "https://chrome-devtools-frontend.appspot.com/serve_rev/@.../inspector.html?ws=127.0.0.1:52302/devtools/page/...",
   "faviconUrl": "https://www.gstatic.com/images/branding/searchlogo/ico/favicon.ico",
   "id": "2215AF60AC345E4BA6D822389CFC743B",
   "title": "Google",
   "type": "page",
   "url": "https://www.google.com.hk/",
   "webSocketDebuggerUrl": "ws://127.0.0.1:52302/devtools/page/2215AF60AC345E4BA6D822389CFC743B"
} ]
```

Or you can use it by MCP client, more information please refer to: [chrome-devtools-mcp](https://github.com/ChromeDevTools/chrome-devtools-mcp).

## References

- [chrome-devtools-mcp](https://github.com/ChromeDevTools/chrome-devtools-mcp)
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/chrome)

---

### Examples Claude Code (`docs/examples/claude-code.md`)

---
title: Claude Code
description: Access Claude via the claude-cli npm package in an OpenSandbox container.
---

# Claude Code Example

Access Claude via the `claude-cli` npm package in OpenSandbox.

## Start OpenSandbox server [local]

Pre-pull the code-interpreter image (includes Node.js):

```shell
docker pull sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0

# use docker hub
# docker pull opensandbox/code-interpreter:v1.1.0
```

Then start the local OpenSandbox server, stdout logs will be visible in the terminal:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Create and Access the Claude Sandbox

```shell
# Install OpenSandbox package
uv pip install opensandbox

# Run the example (requires SANDBOX_DOMAIN / SANDBOX_API_KEY / ANTHROPIC_AUTH_TOKEN)
uv run python examples/claude-code/main.py
```

The script installs the Claude CLI (`npm i @anthropic-ai/claude-code@latest`) at runtime (Node.js is already in the code-interpreter image), then sends a simple request `claude "Compute 1+1=?."`. Auth is passed via `ANTHROPIC_AUTH_TOKEN`, and you can override endpoint/model with `ANTHROPIC_BASE_URL` / `ANTHROPIC_MODEL`.



## Headless Mode and Session Resume

The example script also runs the two patterns used by coding-agent integrations: a headless run with structured output, and a follow-up turn that resumes the same conversation.

### Turn 1: headless run with structured output

```shell
claude -p "Remember this for later: my favorite sandbox number is 42." --output-format json
```

With `-p` (print mode) the CLI answers the prompt and exits. `--output-format json` prints the reply, `session_id`, usage, and cost metadata as a single JSON object, so the script extracts `session_id` for the next turn.

### Turn 2: resume the session

```shell
claude -p "What is my favorite sandbox number? Reply with just the number." \
  --resume <session_id> --output-format json
```

`--resume` continues a specific conversation with its context intact — the reply recalls "42" from turn 1. `--continue` resumes the most recent conversation instead; an explicit `--resume <session_id>` stays deterministic when several conversations live in the same sandbox.

::: tip Concurrent follow-ups
Add `--fork-session` to `--resume` / `--continue` to create a new session ID instead of reusing the original, so parallel follow-ups on the same conversation don't overwrite each other.
:::

### Permission prompts in unattended runs

There is no terminal to answer permission prompts in a `-p` run: requests that would prompt are denied, so a task that needs a tool (file edits, shell commands) can fail. Two common remedies for agent workloads:

- `--dangerously-skip-permissions` (equivalent to `--permission-mode bypassPermissions`) runs tools without prompts — a common choice inside an ephemeral OpenSandbox container, where the sandbox itself is the isolation boundary.
- `--permission-mode dontAsk` stays fail-closed: only tools pre-approved by your permission rules run — the standard pattern for locked-down CI. (`--permission-prompts none`, Claude Code v2.1.259+, is the print-mode equivalent.)

::: warning The interactive UI needs a TTY
The interactive REPL (`claude "..."`) renders its UI, including permission prompts, on a terminal. Through a plain command pipe there is no way to answer a prompt, so the run can wait indefinitely. Use `-p` for scripted runs, or drive the interactive UI over a [PTY session](/architecture/data-plane/execd).
:::

To receive output as it is generated (token-level events) instead of a single final JSON object, use `--output-format stream-json --verbose --include-partial-messages`.

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Sandbox service address |
| `SANDBOX_API_KEY` | _(optional for local)_ | API key if your server requires authentication |
| `SANDBOX_IMAGE` | `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0` | Sandbox image to use |
| `ANTHROPIC_AUTH_TOKEN` | _(required)_ | Your Anthropic auth token |
| `ANTHROPIC_BASE_URL` | _(optional)_ | Anthropic API endpoint (e.g., self-hosted proxy) |
| `ANTHROPIC_MODEL` | `claude_sonnet4` | Model name |

## References

- [claude-code](https://www.npmjs.com/package/claude-code) - NPM package for Claude Code CLI
- [Claude Code headless mode](https://code.claude.com/docs/en/headless) - Print mode, output formats, and session resume
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/claude-code)

---

### Examples Client Pool Schedule (`docs/examples/client-pool-schedule.md`)

---
title: Scheduled Client Pool Sizing
description: Adjust a Go SandboxPool's idle target during business hours using an application-level controller and Resize.
---

# Scheduled Client Pool Sizing

Keep more sandboxes ready during business hours and reduce idle capacity at night or on weekends. This Go example evaluates a small application-owned schedule and calls the existing `SandboxPool.Resize` API. The SDK's reconciler handles creating and removing idle sandboxes.

The schedule uses **Monday–Friday, 09:00 inclusive to 21:00 exclusive**, in an IANA time zone such as `Asia/Shanghai` or `America/New_York`. Its default targets are five idle sandboxes during business hours and zero outside them. Edit `desiredMaxIdle` in the example to implement your own demand or budget policy.

Source: [`examples/client-pool-schedule/`](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/client-pool-schedule).

## Run

Install Go 1.20 or newer, check out this repository, and start an [OpenSandbox server](/getting-started/). The example's Go module uses the SDK from the same checkout through a local `replace` directive.

```bash
cd examples/client-pool-schedule
export OPEN_SANDBOX_DOMAIN=localhost:8080
export OPEN_SANDBOX_API_KEY=your-api-key
go run . -timezone Asia/Shanghai -peak-idle 5 -offpeak-idle 0
```

Use `OPEN_SANDBOX_PROTOCOL=https` for an HTTPS server. The example routes sandbox health checks through the server proxy. It uses `ubuntu:22.04` with an entrypoint that keeps the sandbox alive; use `-image` to select another compatible image. Run `go run . -h` for all flags.

::: warning Sandbox resources
This command creates real sandboxes when the scheduled target is positive. Use small targets while trying it out. Ctrl+C stops reconciliation, then drains the example's remaining idle sandboxes and waits for their best-effort kill requests to finish before exiting. Failed kills may leave sandboxes running until their TTL expires. Sandboxes already acquired by your application remain the caller's responsibility.
:::

## How resizing works

- The pool starts with the target for the current time, avoiding an initial business-hours warmup when started at night.
- Every 30 seconds, the controller compares the desired target with `Snapshot().MaxIdle`. It calls `Resize` only when those targets differ, so a temporary idle deficit does not trigger repeated resize calls.
- Target updates are asynchronous. The reconcile interval, sandbox startup and readiness time, and warmup concurrency determine how quickly capacity converges. Move the business-hours start earlier if capacity must be ready before traffic arrives.
- Scale-down removes surplus **idle** sandboxes. Acquired sandboxes are outside the idle buffer. A target of zero stops replenishment; it does not disable direct creation on acquire or act as a total sandbox quota.
- The time zone is evaluated on each check, including daylight-saving changes. The example embeds Go's timezone database so it also works on hosts without system zoneinfo files.
- A snapshot or resize error stops the example and runs cleanup. It does not call `Start` or clear namespace-destroy state to recover from an error.

## Multiple application processes

This runnable example uses an **in-memory store**, so its pool belongs to one process. For a distributed pool, configure every participant with the same Redis store and `pool_name`, and a unique `owner_id`; see the [client pool guide](/guides/client-pool).

Run only one application-level scheduling controller for each shared namespace, or elect one in your application. `Resize` updates the shared target, so you do not need a cron job on every worker. The pool's warmup leader election does not elect a leader for your external scheduling policy. Avoid competing manual and scheduled writers: this example reapplies the scheduled target on the next check. Coordinate worker startup and restarts with the controller, because starting a pool can write its configured initial target to the store.

The example's shutdown drain is intended for its single-process pool. Do not copy that drain into the shutdown of one worker of a shared pool: it would drain the shared idle buffer while other workers may still replenish it. Use the documented namespace retirement procedure when retiring an entire distributed pool.

## Test without a server

```bash
go test ./...
go vet ./...
```

Tests cover weekday and weekend boundaries, time-zone conversion, daylight-saving transitions, and target updates through the real SDK's in-memory state store. A local HTTP test server verifies that shutdown waits for pending kill requests.

---

### Examples Code Interpreter (`docs/examples/code-interpreter.md`)

---
title: Code Interpreter
description: Complete demonstration of running Python code using the Code Interpreter SDK with OpenSandbox.
---

# Code Interpreter Sandbox

Complete demonstration of running Python code using the Code Interpreter SDK.

## Getting Code Interpreter image

::: info Image Source
The container image source and environment definitions are maintained in [opensandbox-group/sandbox-images](https://github.com/opensandbox-group/sandbox-images).
:::

Pull the prebuilt image from a registry:

```shell
docker pull sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0

# use docker hub
# docker pull opensandbox/code-interpreter:v1.1.0
```

## Start OpenSandbox server [local]

Start the local OpenSandbox server:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Create and access the Code Interpreter Sandbox

```shell
# Install OpenSandbox packages
uv pip install opensandbox opensandbox-code-interpreter

# Run the example (requires SANDBOX_DOMAIN / SANDBOX_API_KEY)
uv run python examples/code-interpreter/main.py
```

The script creates a Sandbox + CodeInterpreter, runs a Python code snippet and prints stdout/result, then terminates the remote instance. Cleanup is attempted in a `finally` block even if interpreter setup or code execution raises an exception.

## Environment variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Sandbox service address |
| `SANDBOX_API_KEY` | _(optional)_ | API key if your server requires authentication |
| `SANDBOX_IMAGE` | `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0` | Sandbox image to use |

## Example output

```text
=== Python example ===
[Python stdout] Hello from Python!

[Python result] {'py': '3.14.2', 'sum': 4}

=== Java example ===
[Java stdout] Hello from Java!

[Java stdout] 2 + 3 = 5

[Java result] 5

=== Go example ===
[Go stdout] Hello from Go!
3 + 4 = 7


=== TypeScript example ===
[TypeScript stdout] Hello from TypeScript!

[TypeScript stdout] sum = 6
```

## Code Interpreter Sandbox from pool

### Start OpenSandbox server [k8s]

Install the k8s OpenSandbox operator, and create a pool:

```
/* Detailed source-code truncated for AI context efficiency. */
```

#### How Pool entrypoint injection works

The lifecycle API allocates an already-running Pod from the Pool, so it does not replace that Pod's `command`, `args`, or `env`. When a create request supplies an `entrypoint` or environment variables, the server records them in `BatchSandbox.spec.taskTemplate`. The controller then sends the task to the allocated Pod's IP on port `5758`.

The Pool template must provide all parts of that execution path:

- Install and run task-executor, listening on `0.0.0.0:5758`. Set its `-log-dir` explicitly so the troubleshooting path is deterministic; the example writes `/tmp/task-executor.log`.
- Install execd and `bootstrap.sh` into the shared volume before the Pod starts.
- Keep `bootstrap.sh` at `/opt/opensandbox/bootstrap.sh`. The server-generated task invokes that exact path. The execd binary can use another path only when the task-executor environment sets `EXECD` accordingly.
- Start execd through `bootstrap.sh` after allocation so request-specific values such as `EXECD_ACCESS_TOKEN` are available. The example above leaves task-executor as the warm Pod's foreground process for this reason.

The Pod YAML continuing to show the Pool template is therefore expected. Inspect the `BatchSandbox` resource and task-executor instead:

```shell
# Confirm that the server injected the requested process and environment.
kubectl get batchsandbox <sandbox-name> -n <namespace> \
  -o jsonpath='{.spec.taskTemplate}{"\n"}'

# Find the allocated Pod. The annotation value contains a JSON `pods` array.
kubectl get batchsandbox <sandbox-name> -n <namespace> \
  -o jsonpath='{.metadata.annotations.sandbox\.opensandbox\.io/alloc-status}{"\n"}'

# Replace <pool-pod> with the first Pod name from that array.
kubectl exec <pool-pod> -n <namespace> -- \
  sh -c 'test -x /opt/opensandbox/task-executor && test -x /opt/opensandbox/bootstrap.sh'
kubectl exec <pool-pod> -n <namespace> -- \
  tail -n 100 /tmp/task-executor.log

# Check the executor health endpoint from a second terminal while this runs.
kubectl port-forward pod/<pool-pod> -n <namespace> 5758:5758
curl http://127.0.0.1:5758/health
curl http://127.0.0.1:5758/getTasks

# The lifecycle server uses <sandbox-name>-0 as the task name. Check the task's
# captured output (adjust the path if task-executor uses a custom data directory).
kubectl exec <pool-pod> -n <namespace> -- \
  sh -c 'tail -n 100 /var/lib/sandbox/tasks/<sandbox-name>-0/stdout.log; tail -n 100 /var/lib/sandbox/tasks/<sandbox-name>-0/stderr.log'

# Check controller logs for delivery failures between the controller and port 5758.
kubectl logs -n opensandbox-system -l control-plane=controller-manager --tail=100
```

If `taskTemplate` exists but the health check cannot reach port `5758`, verify that task-executor is installed and remains running. The generated task intentionally starts `bootstrap.sh` in the background, so its wrapper can report success even when `bootstrap.sh` is missing or the requested entrypoint later fails. Do not rely on `taskFailed` or `taskLastErrorMessage` alone for these failures; inspect the task's captured `stderr.log` and `stdout.log`, then verify the execd or application process directly.

Start the k8s OpenSandbox server:

```shell
uv pip install opensandbox-server

# replace with your k8s cluster config, kubeconfig etc.
opensandbox-server init-config ~/.sandbox.toml --example k8s
curl -o ~/batchsandbox-template.yaml https://raw.githubusercontent.com/opensandbox-group/OpenSandbox/main/server/opensandbox_server/examples/example.batchsandbox-template.yaml

opensandbox-server
```

### Create and access the Code Interpreter Sandbox (pool)

```shell
# Install OpenSandbox packages
uv pip install opensandbox opensandbox-code-interpreter

# Run the example (requires SANDBOX_DOMAIN / SANDBOX_API_KEY)
uv run python examples/code-interpreter/main_use_pool.py
```

### Pool example output

```text
=== Verify Environment Variable ===
[ENV Check] TEST_ENV value: test

[ENV Result] 'test'

=== Java example ===
[Java stdout] Hello from Java!

[Java stdout] 2 + 3 = 5

[Java result] 5

=== Go example ===
[Go stdout] Hello from Go!
3 + 4 = 7


=== TypeScript example ===
[TypeScript stdout] Hello from TypeScript!

[TypeScript stdout] sum = 6
```

## References

- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/code-interpreter)
- [Code Interpreter Image Repository](https://github.com/opensandbox-group/sandbox-images)

---

### Examples Codex Cli (`docs/examples/codex-cli.md`)

---
title: Codex CLI
description: Use the official @openai/codex npm package to call OpenAI/Codex-like models in OpenSandbox.
---

# Codex/OpenAI CLI Example

Use the official `@openai/codex` npm package to call OpenAI/Codex-like models in OpenSandbox.

## Start OpenSandbox server [local]

Pre-pull the code-interpreter image (includes Node.js):

```shell
docker pull sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0

# use docker hub
# docker pull opensandbox/code-interpreter:v1.1.0
```

Start the local OpenSandbox server, logs will be visible in the terminal:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Create and Access the Codex Sandbox

```shell
# Install OpenSandbox package
uv pip install opensandbox

# Run the example (requires SANDBOX_DOMAIN / SANDBOX_API_KEY / OPENAI_API_KEY)
uv run python examples/codex-cli/main.py
```

The script installs the Codex CLI (`npm install @openai/codex@latest`) at runtime (Node.js is already in the code-interpreter image), then executes a simple request `codex exec "Compute 1+1 and return JSON with keys result and reasoning." --skip-git-repo-check`. Auth is passed via `OPENAI_API_KEY`; you can override endpoint/model with `OPENAI_BASE_URL` / `OPENAI_MODEL`.

## Multi-Turn Sessions: Resume a Previous Run

Each `codex exec` run is a session. For pipelines that need a follow-up turn (review → fix, plan → implement), resume the session instead of starting over with a fresh context.

Turn 1 — run with `--json` and capture the session id. `--json` turns stdout into a JSON Lines (JSONL) event stream; the first event, `thread.started`, carries the `thread_id`, which is the session id that `codex exec resume` accepts:

```shell
codex exec --json "Remember this for later: my favorite sandbox number is 42." --skip-git-repo-check
```

```json
{"type": "thread.started", "thread_id": "0199a213-81c0-7800-8aa1-bbab2a035a53"}
```

Turn 2 — resume that session with a follow-up prompt; the model recalls the context of the previous turns, so the reply is `42`:

```shell
codex exec resume "0199a213-81c0-7800-8aa1-bbab2a035a53" "What is my favorite sandbox number? Reply with just the number." --skip-git-repo-check
```

::: tip
`codex exec resume --last "..."` continues the most recent session from the current working directory — convenient for quick two-stage pipelines. The example script captures the explicit id instead, which stays correct when several sessions interleave.
:::

If you only need the final message for a downstream step, `-o <path>` / `--output-last-message <path>` writes it to a file, and `--output-schema <file>` validates the final response against a JSON Schema.

## Sandbox and Approval Modes

Codex applies its own sandbox policy to model-generated shell commands; these flags control it inside the OpenSandbox container:

- `--sandbox workspace-write` (or `-s workspace-write`) lets model-generated commands write inside the workspace — the usual choice for autonomous runs.
- `--sandbox read-only` keeps the run strictly read-only.
- `-a never` / `--ask-for-approval never` never pauses for human approval, so a non-interactive run cannot block on a person.
- `--dangerously-bypass-approvals-and-sandbox` (alias `--yolo`) runs every command without approvals or sandboxing. The Codex CLI documents it as safe only "inside an externally hardened environment" — in OpenSandbox terms, that means a sandbox running a hardened runtime (gVisor, Kata), not the default runc; see [Secure Container Runtimes](/guides/secure-container).

::: warning
`--full-auto` is deprecated — prefer `--sandbox workspace-write`; Codex prints a warning when the old flag is used.
:::

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Sandbox service address |
| `SANDBOX_API_KEY` | _(optional for local)_ | API key if your server requires authentication |
| `SANDBOX_IMAGE` | `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0` | Sandbox image to use |
| `OPENAI_API_KEY` | _(required)_ | Your OpenAI API key |
| `OPENAI_BASE_URL` | `https://api.openai.com/v1` | OpenAI API endpoint |
| `OPENAI_MODEL` | `gpt-4o-mini` | Model to use |

## References

- [@openai/codex](https://www.npmjs.com/package/@openai/codex) - Official OpenAI Codex CLI
- [Codex non-interactive mode](https://developers.openai.com/codex/noninteractive) - `codex exec`, JSONL output, and session resume
- [Codex CLI reference](https://developers.openai.com/codex/cli/reference) - Sandbox and approval flags
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/codex-cli)

---

### Examples Deep Agents (`docs/examples/deep-agents.md`)

---
title: Deep Agents
description: Run a Deep Agent with its file and shell tools executing inside an OpenSandbox sandbox.
---

# Deep Agents + OpenSandbox Example

Run a [Deep Agent](https://github.com/langchain-ai/deepagents) whose file and shell tools execute inside an OpenSandbox sandbox. The [`langchain-sandbox-opensandbox`](https://pypi.org/project/langchain-sandbox-opensandbox/) backend adapts the OpenSandbox Python SDK to the Deep Agents `BaseSandbox` interface, so every `ls` / `read_file` / `write_file` / `glob` / `grep` / command the agent performs is sandboxed.

The backend passes the full `langchain-tests` `SandboxIntegrationTests` conformance suite (86/86) against a live server.

## Start OpenSandbox server [local]

Start a local OpenSandbox server, logs will be visible in the terminal:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Run the example

```shell
# Install Deep Agents + the OpenSandbox backend
uv pip install deepagents langchain-sandbox-opensandbox

# Run the example (requires SANDBOX_DOMAIN / ANTHROPIC_API_KEY)
uv run python examples/deep-agents/main.py
```

The agent writes a Python script into the sandbox, runs it, and reports the output — all file and shell operations happen inside the sandbox rather than on the host. The sandbox is destroyed on exit (terminated and local resources closed).

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Sandbox service address (host and optional port) |
| `SANDBOX_PROTOCOL` | `http` | Protocol used to reach the server (`http` or `https`) |
| `SANDBOX_API_KEY` | _(optional for local)_ | API key if your server requires authentication |
| `ANTHROPIC_API_KEY` | _(required)_ | Anthropic API key for the default Deep Agents model |

## References

- [Deep Agents](https://github.com/langchain-ai/deepagents) - Agent framework
- [langchain-sandbox-opensandbox](https://pypi.org/project/langchain-sandbox-opensandbox/) - OpenSandbox backend for Deep Agents
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/deep-agents)

---

### Examples Deer Flow (`docs/examples/deer-flow.md`)

---
title: DeerFlow
description: Run DeerFlow agent turns on OpenSandbox through the OpenSandboxProvider shipped in DeerFlow 2.1.0.
---

# DeerFlow + OpenSandbox Example

[DeerFlow](https://github.com/bytedance/deer-flow) is an open-source deep-research agent framework. Since 2.1.0 it ships an OpenSandbox sandbox provider, so the `bash`, file, and search tools an agent calls run inside a sandbox on your OpenSandbox server instead of on the gateway host.

The provider lives in DeerFlow (`deerflow.community.opensandbox:OpenSandboxProvider`) and drives the OpenSandbox Python SDK. Nothing extra is needed on the OpenSandbox side: this example starts a server, points DeerFlow's `sandbox` section at it, and runs sandbox work through DeerFlow's own provider.

## Start OpenSandbox server [local]

Start a local OpenSandbox server, logs will be visible in the terminal:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

::: info Docker runtime requirement
The server uses `runtime.type = "docker"` by default, so it **must** be able to reach a running Docker daemon. Docker Desktop users should confirm with `docker version`; on Colima (macOS) export `DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock"` before starting the server.
:::

## Set up DeerFlow with the OpenSandbox provider

DeerFlow is a cloned application rather than a PyPI library — the harness and its `deerflow-extension-api` companion are uv workspace members — so the provider extra is installed from a checkout. DeerFlow requires Python 3.12 or later.

```shell
git clone https://github.com/bytedance/deer-flow.git
cd deer-flow

# optional: the full local workspace setup (backend, frontend, and pre-commit hooks)
make install

# backend dependencies plus the OpenSandbox provider
cd backend && uv sync --all-packages --extra opensandbox && cd ..
```

The provider is imported only when `sandbox.use` selects it.

::: warning Re-add the extra after a plain sync
`opensandbox` is an optional extra, and a plain `uv sync --locked` — which is what `make install` runs for the backend — prunes it from the environment. If a later `make install` or `uv sync` removes it, run `cd backend && uv sync --all-packages --extra opensandbox` again. The docker-compose deployment is unaffected: it installs the extra at image build time from `UV_EXTRAS=opensandbox` in `.env`.
:::

## Run the example

The example drives DeerFlow's provider directly, so it needs no model configuration and no gateway process. It writes a minimal `config.yaml` containing only the `sandbox` section and points `DEER_FLOW_CONFIG_PATH` at it.

Run it with the DeerFlow backend environment, from your OpenSandbox checkout:

```shell
uv run --project ../deer-flow/backend python examples/deer-flow/main.py
```

Any interpreter that can `import deerflow` works — for example a shell where the DeerFlow checkout's `backend/.venv` is activated.

The script creates a sandbox through `provider.acquire()`, runs a command, writes a Python script with DeerFlow's file API, executes it, then exercises `read_file`, `list_dir`, `glob`, `grep`, and the `/mnt/user-data`-restricted `download_file`. The sandbox is released to the warm pool and destroyed by `provider.shutdown()` on exit.

You should see output similar to:

```text
[config] DeerFlow config: /tmp/opensandbox-deerflow-xxxx/config.yaml
[create] DeerFlow sandbox c37d0a7b8a926499 bound to remote b5419ac9-f80b-4263-b403-9bd74ee54c43
[command] Linux-6.6.87.2-microsoft-standard-WSL2-x86_64-with-glibc2.39
[file] Wrote /mnt/user-data/workspace/fib.py
[command] [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
[file] Read back: def fibonacci(count):
[list_dir] ['/mnt/user-data/workspace', '/mnt/user-data/workspace/fib.py']
[glob] ['/mnt/user-data/workspace/fib.py']
[grep] /mnt/user-data/workspace/fib.py:1: def fibonacci(count):
[download] 170 bytes fetched from /mnt/user-data/workspace/fib.py
[cleanup] Sandbox destroyed
```

The sandbox id, remote id, and platform string differ on each run.

::: tip
DeerFlow logs `No models are configured ...` while loading this generated config. That notice is expected on the provider-only path — the script never invokes a model.
:::

## Point the DeerFlow agent at OpenSandbox

For agent runs, put the sandbox section in the DeerFlow checkout's `config.yaml` (`make config` creates one from `config.example.yaml`):

```yaml
sandbox:
  use: deerflow.community.opensandbox:OpenSandboxProvider
  image: python:3.11
  domain: localhost:8080
  protocol: http
  ready_timeout: 120        # create/readiness deadline; covers a first-run image pull
  sandbox_timeout: 14400    # remote lifetime in seconds; 0 means explicit cleanup only
  bash_command_timeout: 600 # default deadline for commands run in the sandbox
  replicas: 3               # active + warm sandboxes per gateway process
  idle_timeout: 600         # destroy a warm sandbox after this many idle seconds; 0 disables
```

| Option | Default | Description |
|--------|---------|-------------|
| `image` | `python:3.11` | Sandbox image. Any image with a POSIX shell works. |
| `domain` | `OPEN_SANDBOX_DOMAIN`, else `localhost:8080` | OpenSandbox server address |
| `api_key` | `OPEN_SANDBOX_API_KEY` | API key, if the server requires authentication |
| `protocol` | `http` | Use `https` for any non-loopback domain |
| `request_timeout` | `30` | Management API request timeout in seconds |
| `ready_timeout` | `30` | Create and readiness deadline in seconds |
| `use_server_proxy` | `false` | Route execd and file traffic through the server |
| `sandbox_timeout` | `14400` | Server-side lifetime in seconds; `0` disables renewal |
| `bash_command_timeout` | `600` | Default command deadline in seconds |
| `replicas` | `3` | Active plus warm sandbox cap per gateway process |
| `idle_timeout` | `600` | Idle seconds before a warm sandbox is destroyed; `0` disables |
| `environment` | `{}` | Environment variables injected into sandbox commands. A value starting with `$` is resolved from the gateway process environment. |

`api_key` and `domain` may be omitted when `OPEN_SANDBOX_API_KEY` and `OPEN_SANDBOX_DOMAIN` are exported, which is how the example passes them. Set `use_server_proxy: true` when the gateway can reach the OpenSandbox management service but cannot reach sandbox `execd` endpoints directly.

Then run one headless agent turn. The prompt makes the agent call its `bash` tool, which now executes inside OpenSandbox:

```shell
deerflow --print "Run python3 -c 'print(2 ** 16)' and report the output"
```

`deerflow` opens the terminal workbench when run interactively, while `--print` and `--json` run one headless turn. The gateway and web UI start with `make dev`, and the docker-compose deployment reaches the provider through the `UV_EXTRAS=opensandbox` setting in `.env`.

## Behavior notes

- The provider creates one sandbox per effective `(user_id, thread_id)` scope and parks it in an in-process warm pool after each turn. Only the same scope can reclaim it, and a reclaim runs a health check first.
- Create returns only after the SDK readiness check and DeerFlow's `/mnt/user-data/{workspace,uploads,outputs}` bootstrap succeed. A bootstrap failure destroys the newly created remote sandbox.
- Every operation renews the sandbox's server-side lifetime; commands that carry no explicit timeout use `bash_command_timeout`. Operations on one sandbox are serialized so a short renewal cannot shorten the horizon of an in-flight long command.
- `execute_command` forwards per-call environment variables and timeouts, and preserves stdout, stderr, and non-zero exit status in the returned text. File reads and writes use OpenSandbox's native filesystem API.
- `list_dir`, `glob`, and `grep` run portable `find`/`grep` commands inside the sandbox. All paths must be absolute and traversal-free; artifact downloads are restricted to `/mnt/user-data`.
- A command-path HTTP 404 or 410, an unhealthy session, or a broken transport evicts the dead client so the next acquire cold-starts a replacement.
- `replicas` is a per-gateway-process soft cap: DeerFlow does not coordinate sandbox ownership between processes yet, so one gateway process per OpenSandbox-backed deployment keeps the cap meaningful.

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Server address (host and optional port) |
| `SANDBOX_PROTOCOL` | `http` | Protocol used to reach the server (`http` or `https`) |
| `SANDBOX_API_KEY` | _(optional for local)_ | API key if your server requires authentication |
| `SANDBOX_IMAGE` | `python:3.11` | Sandbox image used by this example |
| `OPEN_SANDBOX_DOMAIN` | _(unset)_ | Server address when the DeerFlow config omits `domain` |
| `OPEN_SANDBOX_API_KEY` | _(unset)_ | API key when the DeerFlow config omits `api_key` |

## All-in-one Docker image (server + DeerFlow Web UI)

`examples/deer-flow/` ships an all-in-one lab image bundling both sides of the integration: the OpenSandbox server (docker runtime) and the DeerFlow backend with the OpenSandboxProvider extra preinstalled — plus the **pre-built Web UI**, served by the bundled nginx on `:2026`. Sandbox containers run on the **host** Docker daemon: the image mounts `docker.sock` at run time and drives the host daemon through it — the same topology as `server/docker-compose.example.yaml`.

Build and run:

```shell
cd examples/deer-flow
docker build -t deerflow-opensandbox .
docker run -d --name deerflow-opensandbox \
  -p 2026:2026 \
  -p 8080:8080 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  --add-host host.docker.internal:host-gateway \
  deerflow-opensandbox
```

`--add-host host.docker.internal:host-gateway` is required on Linux so the server can reach the host-published sandbox ports; Docker Desktop resolves that name already, and the flag is harmless there.

Two entry points:

- **DeerFlow Web UI**: <http://localhost:2026> — the teaching path. Complete the first-run setup in the page, configure a model under Settings → Models (API key stays in the Web UI), then chat: the agent's `bash` and file tools execute inside an OpenSandbox sandbox.
- **OpenSandbox server API**: <http://localhost:8080> — for `curl` checks and the `osb` CLI.

Verify from a shell on the host:

```shell
curl -s http://127.0.0.1:8080/health        # -> {"status":"healthy"}
docker ps | grep -i sandbox                 # sandbox containers appear as you chat
```

Headless / model-free paths inside the container (`docker exec -it deerflow-opensandbox bash`):

```shell
# 1) model-free: drive OpenSandboxProvider directly (no API key needed)
cd /opt/OpenSandbox
uv run --project /opt/deer-flow/backend python examples/deer-flow/main.py

# 2) full agent turn via CLI (requires a configured model)
deerflow --print "Run python3 -c 'print(2 ** 16)' and report the output"
```

::: tip
On first boot the entrypoint generates `/opt/deer-flow/config.yaml` from `config.example.yaml` and switches the sandbox section to `OpenSandboxProvider` via `enable-opensandbox` (on PATH, idempotent). If `make setup` or any wizard later overwrites the config, rerun `enable-opensandbox` to switch back.
:::

::: warning
The image ships a fixed teaching API key (`deerflow-lab-key`, set as `server.api_key` and `OPEN_SANDBOX_API_KEY`) — required since the server refuses to start with an empty key in non-interactive mode. Fine for a classroom laptop; for any shared deployment mount your own config and override the env var.
:::

Notes:

- The Web UI stack starts in production mode (background `serve.sh --prod --daemon --skip-install --skip-frontend-build`), reusing the frontend build baked at image build time; the frontend build requires Node.js + pnpm, both installed in the image.
- `UV_EXTRAS=opensandbox` is set in the image so DeerFlow's own start-time `uv sync --locked` (in `scripts/serve.sh`) keeps the `opensandbox` extra instead of pruning it.
- Sandbox images default to the lab image itself (`opensandbox/deerflow-example:latest` on the ACR mirror — a python base with a rich preinstalled environment); any image with a POSIX shell works, e.g. Docker Hub `python:3.11`. Images are pulled by the **host** Docker; the provider's `ready_timeout` is set to 600s to cover the first pull of the larger default.
- `DEERFLOW_REPO` / `DEERFLOW_REF` / `OPENSANDBOX_REPO` build args let network-restricted environments point the clones at mirrors; pip/uv honor `PIP_INDEX_URL` / `UV_INDEX_URL`, and pnpm uses the configured registry.

## References

- [DeerFlow](https://github.com/bytedance/deer-flow) - Open-source deep-research agent framework
- [DeerFlow OpenSandbox provider](https://github.com/bytedance/deer-flow/blob/main/backend/packages/harness/deerflow/community/opensandbox/README.md) - Provider documentation
- [deer-flow PR #4877](https://github.com/bytedance/deer-flow/pull/4877) - OpenSandbox provider contribution
- [OpenSandbox Python SDK](https://pypi.org/project/opensandbox/)
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/deer-flow)

---

### Examples Desktop (`docs/examples/desktop.md`)

---
title: Desktop (VNC)
description: Launch Xvfb + x11vnc + fluxbox in OpenSandbox to provide a VNC-accessible desktop environment.
---

# Desktop (VNC) Example

Launch Xvfb + x11vnc + fluxbox in OpenSandbox to provide a VNC-accessible desktop environment.

## Build the Desktop Sandbox Image

The Dockerfile in the example directory builds a sandbox image with desktop and VNC components pre-installed:

```shell
cd examples/desktop
docker build -t opensandbox/desktop:latest .
```

This image includes:
- Xvfb (virtual framebuffer X server)
- x11vnc (VNC server)
- XFCE desktop (panel, file manager, terminal)
- Non-root user (desktop) for security

## Start OpenSandbox server [local]

Pre-pull the desktop image:

```shell
docker pull opensandbox/desktop:latest
```

Start the local OpenSandbox server:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Create and Access the Desktop Sandbox

```shell
# Install OpenSandbox package
uv pip install opensandbox

uv run python examples/desktop/main.py
```

The script starts the desktop stack (Xvfb + XFCE + x11vnc) and also launches noVNC/websockify. It prints:
- VNC endpoint (`endpoint.endpoint`) for native VNC clients when direct endpoint mode is enabled, password from `VNC_PASSWORD` (default: `opensandbox`)
- noVNC URL for browsers (`/vnc.html?host=...&port=...&path=...`)

The sandbox stays alive for 5 minutes by default; interrupt sooner with Ctrl+C. Uses the prebuilt desktop image by default.

### Embed noVNC in an HTTPS page

Browsers block an `http://` noVNC iframe inside an HTTPS page. Terminate TLS at a reverse proxy in front of the OpenSandbox server, then make the example use that HTTPS origin and the server proxy:

```shell
export SANDBOX_DOMAIN=sandbox.example.com
export SANDBOX_PROTOCOL=https
export SANDBOX_USE_SERVER_PROXY=true
export VNC_PASSWORD=opensandbox

uv run python examples/desktop/main.py
```

The generated URL uses the same HTTPS origin for both the noVNC page and its WebSocket, for example:

```text
https://sandbox.example.com/v1/sandboxes/<sandbox-id>/proxy/6080/vnc.html?host=sandbox.example.com&port=443&path=v1/sandboxes/<sandbox-id>/proxy/6080
```

The TLS reverse proxy must forward normal HTTP requests and WebSocket upgrades to the OpenSandbox server. For Nginx, the relevant settings are:

```nginx
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl;
    server_name sandbox.example.com;
    # Configure ssl_certificate and ssl_certificate_key here.

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}
```

This direct browser URL works when the OpenSandbox proxy route does not require browser-supplied authentication headers, including the default single-tenant mode. In multi-tenant mode, the proxy route requires `OPEN-SANDBOX-API-KEY` on both the initial noVNC HTTP request and the WebSocket upgrade. The SDK adds this header to its own requests, but a browser navigation or WebSocket cannot add it.

For multi-tenant browser access, put a trusted, browser-authenticated reverse proxy in front of OpenSandbox. It must authorize access to the requested sandbox, map the browser identity to the correct tenant, and inject that tenant's `OPEN-SANDBOX-API-KEY` header for both HTTP and WebSocket traffic. Do not put the API key in the noVNC URL or expose a reverse proxy that adds one shared key to unauthenticated traffic.

::: warning
Do not expose a direct `http://<sandbox-host>:<mapped-port>` URL in an HTTPS iframe. That endpoint has no TLS termination and the browser will reject it as mixed content. This SDK configuration uses one protocol for the management API and sandbox service requests, so the example requires `SANDBOX_USE_SERVER_PROXY=true` whenever the management API uses HTTPS. This keeps lifecycle, execd, noVNC page, and WebSocket traffic on the HTTPS server origin instead of incorrectly sending HTTPS to a direct sandbox endpoint. Server proxy mode does not support the raw TCP protocol used by native VNC clients, so the example omits the native VNC endpoint in this mode.
:::

   

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Sandbox service address; may include an `http://` or `https://` scheme |
| `SANDBOX_PROTOCOL` | Domain scheme, otherwise `http` | Protocol used when `SANDBOX_DOMAIN` has no scheme (`http` or `https`) |
| `SANDBOX_USE_SERVER_PROXY` | `false` | Route sandbox service, noVNC HTTP, and WebSocket traffic through the OpenSandbox server; required with HTTPS |
| `SANDBOX_API_KEY` | _(optional for local)_ | Example-specific API key; takes precedence over `OPEN_SANDBOX_API_KEY` |
| `OPEN_SANDBOX_API_KEY` | _(optional for local)_ | SDK-standard API key fallback when `SANDBOX_API_KEY` is unset |
| `SANDBOX_IMAGE` | `opensandbox/desktop:latest` | Sandbox image to use |
| `VNC_PASSWORD` | `opensandbox` | Password for VNC access |

## References

- [noVNC](https://github.com/novnc/noVNC)
- [x11vnc](https://github.com/LibVNC/x11vnc)
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/desktop)

---

### Examples Docker Ossfs Volume Mount (`docs/examples/docker-ossfs-volume-mount.md`)

---
title: Docker OSSFS Volume
description: Mount Alibaba Cloud OSS into sandboxes on Docker runtime using the OSSFS volume model.
---

# Docker OSSFS Volume Mount Example

This example demonstrates how to use the SDK `ossfs` volume model to mount Alibaba Cloud OSS into sandboxes on Docker runtime.

## What this example covers

1. **Basic read-write mount** on an OSSFS backend.
2. **Cross-sandbox sharing** on the same OSSFS backend path.
3. **Two mounts, different OSS prefixes via `subPath`**.

## Prerequisites

### 1. Start OpenSandbox server (Docker runtime)

Make sure your server host has:

- Linux host OS (OSSFS backend is not supported when OpenSandbox Server runs on Windows)
- `ossfs` installed
- FUSE support enabled
- writable local mount root for OSSFS (default `storage.ossfs_mount_root=/mnt/ossfs`)

`storage.ossfs_mount_root` is **optional** if you use the default `/mnt/ossfs`. Even with on-demand mounting, the runtime still needs a deterministic host-side base directory to place dynamic mounts (`<mount_root>/<bucket>/<subPath?>`).

Optional config example:

```toml
[runtime]
type = "docker"

[storage]
ossfs_mount_root = "/mnt/ossfs"
```

Then start the server:

```bash
opensandbox-server
```

### 2. Install Python SDK

```bash
uv pip install opensandbox
```

If your PyPI version does not include OSSFS volume models yet, install from source:

```bash
pip install -e sdks/sandbox/python
```

### 3. Prepare OSS credentials and target path

```bash
export SANDBOX_DOMAIN=localhost:8080
export SANDBOX_API_KEY=your-api-key
export SANDBOX_IMAGE=ubuntu

export OSS_BUCKET=your-bucket
export OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
export OSS_ACCESS_KEY_ID=your-ak
export OSS_ACCESS_KEY_SECRET=your-sk
```

## Run

```bash
uv run python examples/docker-ossfs-volume-mount/main.py
```

## Minimal SDK usage snippet

```python
from opensandbox import Sandbox
from opensandbox.models.sandboxes import OSSFS, Volume

sandbox = await Sandbox.create(
    image="ubuntu",
    volumes=[
        Volume(
            name="oss-data",
            ossfs=OSSFS(
                bucket="your-bucket",
                endpoint="oss-cn-hangzhou.aliyuncs.com",
                # version="2.0",   # optional, default is "2.0"
                accessKeyId="your-ak",
                accessKeySecret="your-sk",
            ),
            mountPath="/mnt/data",
            subPath="train",      # optional
            readOnly=False,       # optional
        )
    ],
)
```

## Notes

::: info Implementation details
- Current implementation supports **inline credentials only** (`accessKeyId`/`accessKeySecret`).
- Mounting is **on-demand** in Docker runtime (mount-or-reuse), not pre-mounted for all buckets.
- `ossfs.version` exists in API/SDK with enum `"1.0" | "2.0"`, and defaults to `"2.0"` when omitted.
- Docker runtime now applies **version-specific mount argument encoding**:
  - `1.0`: mounts via `ossfs ... -o <option>`.
  - `2.0`: mounts via `ossfs2 mount ... -c <config-file>` where options are written as `--<option>` config lines.
- `options` values must be **raw payloads** without leading `-` (for example: `allow_other`, `umask=0022`).
:::

## References

- [Sandbox Lifecycle API Spec](https://github.com/opensandbox-group/OpenSandbox/blob/main/specs/sandbox-lifecycle.yml)
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/docker-ossfs-volume-mount)

---

### Examples Docker Pvc Volume Mount (`docs/examples/docker-pvc-volume-mount.md`)

---
title: Docker PVC Volume
description: Mount Docker named volumes into sandbox containers using the OpenSandbox pvc backend.
---

# Docker PVC (Named Volume) Mount Example

This example demonstrates how to mount Docker named volumes into sandbox containers using the OpenSandbox `pvc` backend. In Docker runtime, `pvc.claimName` maps to a Docker named volume -- providing a more convenient and secure alternative to host-path bind mounts for sharing data across sandboxes.

::: info What is `pvc`?
The `pvc` backend is a runtime-neutral abstraction. In Kubernetes it maps to a PersistentVolumeClaim; in Docker it maps to a named volume. The same API request works on both runtimes.
:::

## Why Named Volumes over Host Paths?

| | Host path (`host` backend) | Named volume (`pvc` backend) |
|---|---|---|
| **Security** | Exposes host filesystem paths | Docker manages storage location; no host path exposed |
| **Setup** | Requires `allowed_host_paths` allowlist | No allowlist needed |
| **Cross-sandbox sharing** | All containers must agree on a host path | Reference the same volume name |
| **Portability** | Tied to host directory structure | Works on any Docker host |
| **Lifecycle** | User manages host directories | `docker volume create/rm` |

## Scenarios

| # | Scenario | Description |
|---|----------|-------------|
| 1 | **Read-write mount** | Mount a named volume for bidirectional file I/O |
| 2 | **Read-only mount** | Mount a named volume that sandboxes cannot modify |
| 3 | **Cross-sandbox sharing** | Two sandboxes share data through the same named volume |
| 4 | **SubPath mount** | Mount only a subdirectory of a named volume (consistent with K8s PVC subPath) |

## Prerequisites

### 1. Start OpenSandbox Server

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

### 2. Create a Docker Named Volume

```shell
# Create the named volume
docker volume create opensandbox-pvc-demo

# Seed it with a marker file via a temporary container
docker run --rm -v opensandbox-pvc-demo:/data alpine \
  sh -c "echo 'hello-from-named-volume' > /data/marker.txt"
```

### 3. Install Python SDK

```shell
uv pip install opensandbox
```

### 4. Pull the Sandbox Image

```shell
docker pull ubuntu:latest
```

## Run

```shell
uv run python examples/docker-pvc-volume-mount/main.py
```

The script automatically creates the named volume and seeds it with test data. You can also specify a custom volume name or image:

```shell
SANDBOX_IMAGE=ubuntu SANDBOX_DOMAIN=localhost:8080 uv run python examples/docker-pvc-volume-mount/main.py
```

## Expected Output

```text
OpenSandbox server : localhost:8080
Sandbox image      : ubuntu
Docker volume      : opensandbox-pvc-demo
  Ensuring Docker named volume 'opensandbox-pvc-demo' exists...
  Created volume 'opensandbox-pvc-demo' with marker.txt

============================================================
Scenario 1: Read-Write PVC (Named Volume) Mount
============================================================
  ...

============================================================
Scenario 2: Read-Only PVC (Named Volume) Mount
============================================================
  ...

============================================================
Scenario 3: Cross-Sandbox Sharing via PVC (Named Volume)
============================================================
  ...

============================================================
Scenario 4: SubPath PVC (Named Volume) Mount
============================================================
  ...

============================================================
All scenarios completed successfully!
============================================================
```

## SDK Usage Quick Reference

### Python (async)

```python
from opensandbox import Sandbox
from opensandbox.models.sandboxes import PVC, Volume

sandbox = await Sandbox.create(
    image="ubuntu",
    volumes=[
        Volume(
            name="my-data",
            pvc=PVC(claimName="my-named-volume"),
            mountPath="/mnt/data",
            readOnly=False,       # optional, default is False
            subPath="datasets/train",  # optional, mount a subdirectory
        ),
    ],
)
```

### Python (sync)

```python
from opensandbox import SandboxSync
from opensandbox.models.sandboxes import PVC, Volume

sandbox = SandboxSync.create(
    image="ubuntu",
    volumes=[
        Volume(
            name="my-data",
            pvc=PVC(claimName="my-named-volume"),
            mountPath="/mnt/data",
            subPath="datasets/train",  # optional
        ),
    ],
)
```

### JavaScript / TypeScript

```typescript
import { Sandbox } from "@alibaba-group/opensandbox";

const sandbox = await Sandbox.create({
  image: "ubuntu",
  volumes: [
    {
      name: "my-data",
      pvc: { claimName: "my-named-volume" },
      mountPath: "/mnt/data",
      readOnly: false,
      subPath: "datasets/train",  // optional
    },
  ],
});
```

### Java / Kotlin

```java
Volume volume = Volume.builder()
    .name("my-data")
    .pvc(PVC.of("my-named-volume"))
    .mountPath("/mnt/data")
    .readOnly(false)
    .subPath("datasets/train")  // optional
    .build();

Sandbox sandbox = Sandbox.builder()
    .image("ubuntu")
    .volume(volume)
    .build();
```

## Cleanup

```shell
docker volume rm opensandbox-pvc-demo
```

## References

- [Sandbox Lifecycle API Spec](https://github.com/opensandbox-group/OpenSandbox/blob/main/specs/sandbox-lifecycle.yml) -- OpenAPI schema for volume definitions
- [Host Volume Mount Example](/examples/host-volume-mount) -- Host path bind mount example (alternative approach)
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/docker-pvc-volume-mount)

---

### Examples Gemini Cli (`docs/examples/gemini-cli.md`)

---
title: Gemini CLI
description: Call Google Gemini via the @google/gemini-cli npm package in OpenSandbox.
---

# Gemini CLI Example

Call Google Gemini via the `@google/gemini-cli` npm package in OpenSandbox.

## Start OpenSandbox server [local]

Pre-pull the code-interpreter image (includes Node.js):

```shell
docker pull sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0

# use docker hub
# docker pull opensandbox/code-interpreter:v1.1.0
```

Start the local OpenSandbox server, logs will be visible in the terminal:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Create and Access the Gemini Sandbox

```shell
# Install OpenSandbox package
uv pip install opensandbox

# Run the example (requires SANDBOX_DOMAIN / SANDBOX_API_KEY / GEMINI_API_KEY)
uv run python examples/gemini-cli/main.py
```

The script installs the Gemini CLI (`npm install @google/gemini-cli@latest`) at runtime (Node.js is already in the code-interpreter image), then sends a simple request `gemini "Compute 1+1=?."`. Auth is passed via `GEMINI_API_KEY`; you can override endpoint/model with `GEMINI_BASE_URL` / `GEMINI_MODEL`.

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Sandbox service address |
| `SANDBOX_API_KEY` | _(optional for local)_ | API key if your server requires authentication |
| `SANDBOX_IMAGE` | `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0` | Sandbox image to use |
| `GEMINI_API_KEY` | _(required)_ | Your Google Gemini API key |
| `GEMINI_BASE_URL` | _(optional)_ | Gemini API endpoint (e.g., proxy) |
| `GEMINI_MODEL` | `gemini-2.5-flash` | Model to use |

## References

- [@google/gemini-cli](https://www.npmjs.com/package/@google/gemini-cli) - Gemini CLI
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/gemini-cli)

---

### Examples Github Copilot Cli Sandbox (`docs/examples/github-copilot-cli-sandbox.md`)

---
title: GitHub Copilot CLI
description: Run GitHub Copilot CLI inside a local OpenSandbox container.
---

# GitHub Copilot CLI Example

Run the official GitHub Copilot CLI inside an OpenSandbox container. The example downloads the official CLI release and executes a prompt non-interactively with `copilot -sp`.

The example follows the same local workflow as the OpenCode example. The Python client only uses the OpenSandbox SDK; the local OpenSandbox Server creates and manages the container through its Docker runtime.

The sandbox uses the Docker Hub image `opensandbox/code-interpreter:v1.1.0`; no Alibaba Cloud container registry is used.

## Start OpenSandbox server [local]

Pre-pull the code-interpreter image:

```shell
docker pull opensandbox/code-interpreter:v1.1.0
```

Install the dependencies and initialize the local Docker runtime:

```shell
uv sync
uv run opensandbox-server init-config ~/.sandbox.toml --example docker
OPENSANDBOX_INSECURE_SERVER=YES uv run opensandbox-server
```

`OPENSANDBOX_INSECURE_SERVER=YES` explicitly acknowledges that this local server has no API key. Keep it bound to localhost. For a shared deployment, configure `server.api_key` instead.

The project currently pins `opensandbox-server==0.2.3`. The upstream `1.1.0` server wheel is known to be missing its generated FastPath gRPC modules and fails during startup.

## Create and Access the GitHub Copilot Sandbox

Create a fine-grained GitHub personal access token with the **Copilot Requests** permission. Store it in the ignored local `.env` file:

```shell
GH_TOKEN=github_pat_replace_me
```

Run the example from the repository root:

```shell
uv run python examples/github-copilot-cli-sandbox/main.py
```

The script creates a Docker-backed sandbox, installs GitHub Copilot CLI with the official installation script, runs `copilot -sp` in an isolated working directory, and destroys the container afterward.

The Microsoft npm proxy does not currently provide `@github/copilot`, so this example uses the official installer, which downloads and verifies a GitHub Release artifact.

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `GH_TOKEN` | _(required)_ | Fine-grained PAT with **Copilot Requests** permission |
| `GITHUB_TOKEN` | _(optional)_ | Used when `GH_TOKEN` is not set |
| `SANDBOX_DOMAIN` | `localhost:8080` | Local sandbox service address |
| `SANDBOX_API_KEY` | _(optional for local)_ | API key if configured |
| `SANDBOX_IMAGE` | `opensandbox/code-interpreter:v1.1.0` | Docker Hub sandbox image |
| `COPILOT_PROMPT` | Arithmetic example | Prompt passed to `copilot -sp` |
| `COPILOT_VERSION` | Latest stable version | Optional official CLI release to install |

::: warning
Never commit a GitHub token. If a token is exposed in terminal output, documentation, source code, or chat, revoke it immediately and create a new one.
:::

## References

- [GitHub Copilot CLI](https://github.com/github/copilot-cli)
- [GitHub Copilot CLI quickstart](https://docs.github.com/en/copilot/get-started/cli-quickstart)
- [Source code](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/github-copilot)

---

### Examples Google Adk (`docs/examples/google-adk.md`)

---
title: Google ADK
description: Integrate Google Agent Development Kit (ADK) with OpenSandbox for tool-calling agents.
---

# Google ADK + OpenSandbox Example

Integrate Google Agent Development Kit (ADK) with OpenSandbox. The ADK agent drives tool calls that execute inside a sandbox.

## Start OpenSandbox server [local]

Pre-pull the code-interpreter image (includes Python):

```shell
docker pull sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0

# use docker hub
# docker pull opensandbox/code-interpreter:v1.1.0
```

Start the local OpenSandbox server, logs will be visible in the terminal:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Run the example

```shell
# Install OpenSandbox + Google ADK deps
uv pip install opensandbox google-adk

# Run the example (requires SANDBOX_DOMAIN / SANDBOX_API_KEY / GOOGLE_API_KEY)
uv run python examples/google-adk/main.py
```

The script uses ADK to create an agent with OpenSandbox tools (`write_file`, `read_file`, `run_in_sandbox`). It runs a few prompts, prints tool events, and cleans up the sandbox.

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Sandbox service address |
| `SANDBOX_API_KEY` | _(optional for local)_ | API key if your server requires authentication |
| `SANDBOX_IMAGE` | `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0` | Sandbox image to use |
| `GOOGLE_API_KEY` | _(required)_ | Gemini API key |
| `GOOGLE_ADK_MODEL` | `gemini-2.5-flash` | Gemini model name |

## References

- [Google ADK](https://google.github.io/adk-docs/) - Agent Development Kit
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/google-adk)

---

### Examples Harbor Evaluation (`docs/examples/harbor-evaluation.md`)

---
title: Harbor Evaluation
description: Run a Harbor agent evaluation on OpenSandbox, provisioning one sandbox per trial.
---

# Harbor Evaluation on OpenSandbox

Run a [Harbor](https://github.com/harbor-framework/harbor) agent evaluation on OpenSandbox infrastructure. Harbor provisions one OpenSandbox container per trial, runs the agent inside it, executes the task's verifier, and collects the reward plus logs and artifacts.

Harbor gained native OpenSandbox support in [harbor-framework/harbor#2054](https://github.com/harbor-framework/harbor/pull/2054), which adds the `opensandbox` environment backend and the `harbor[opensandbox]` install extra.

This example ships a minimal, self-contained task (`hello-opensandbox`) plus a job config that selects the `opensandbox` environment. The task uses a prebuilt image and an offline verifier so it runs anywhere without extra dependencies.

## Start OpenSandbox server [local]

Start a local OpenSandbox server (Docker runtime):

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Install Harbor

The OpenSandbox backend was merged into Harbor's `main` branch ([#2054](https://github.com/harbor-framework/harbor/pull/2054)) but is not yet in a published release. Until a release ships with it, install Harbor from git:

```shell
uv pip install "harbor[opensandbox] @ git+https://github.com/harbor-framework/harbor"
```

Once a released Harbor version includes the backend, the plain extra will work:

```shell
uv pip install "harbor[opensandbox]"   # after the next Harbor release
```

## Configure the connection

Harbor reads the server connection from environment variables (or from the `domain` / `api_key` kwargs in `config.yaml`):

```shell
export OPENSANDBOX_DOMAIN="localhost:8080"   # OpenSandbox server address
export OPENSANDBOX_API_KEY=""                # API key, if your server requires one
```

`OPENSANDBOX_API_KEY` may be left empty for a local / no-auth server.

## Run the evaluation

```shell
# From examples/harbor-evaluation
harbor run -c config.yaml
```

Harbor creates an OpenSandbox sandbox from the task's prebuilt image (`ubuntu:24.04`), runs the `oracle` agent (which executes the reference solution), runs the verifier, records the reward, and tears the sandbox down.

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `OPENSANDBOX_DOMAIN` | `localhost:8080` | OpenSandbox server address |
| `OPENSANDBOX_API_KEY` | _(optional)_ | API key if your server requires authentication |

## Outputs

Results are written under the `jobs_dir` from `config.yaml` (`jobs/opensandbox/<job-id>/<trial-id>/`), including `agent/` logs, `verifier/` output (`reward.txt`, `ctrf.json`), collected `artifacts/`, and `results.json`. A successful run produces `reward = 1.0`.

## Use your own task

The included `hello-opensandbox` task is checked into this repository only to make the example self-contained and runnable without publishing or downloading anything first. For real evaluations, prefer tasks from an external Harbor task registry and override the example task from the command line:

```shell
harbor run -c config.yaml --task <org>/<task>@latest
```

During local task development, you can also point Harbor at a task directory:

```shell
harbor run -c config.yaml --path /path/to/task
```

## Evaluate a real agent

Swap the `oracle` agent for a real agent + model:

```shell
harbor run -c config.yaml -a claude-code -m "anthropic/claude-sonnet-4-5"
```

## OpenSandbox-specific notes

- **Prebuilt images only** — every task must set `[environment].docker_image` in `task.toml`; the OpenSandbox backend does not build Dockerfiles. Use `image_auth` kwargs for private registries.
- **Working directory** — set `[environment].workdir` (this task uses `/app`) so the agent's relative paths resolve where the verifier expects them.
- **Resources** — `cpus` and `memory_mb` map to hard sandbox limits; `storage_mb` is ignored; GPUs are supported via `gpus` / `gpu_types`.
- **Artifacts** — files written under `/logs/artifacts` are downloaded into the trial's `artifacts/` directory.
- **Multi-container** — `docker-compose.yaml` tasks are not supported; use single-container tasks.

## References

- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/harbor-evaluation)
- [Harbor](https://github.com/harbor-framework/harbor)

---

### Examples Host Volume Mount (`docs/examples/host-volume-mount.md`)

---
title: Host Volume Mount
description: Mount host directories into sandbox containers using the OpenSandbox Volume API for bidirectional file sharing.
---

# Host Volume Mount Example

This example demonstrates how to mount host directories into sandbox containers using the OpenSandbox Volume API. Host volume mounts enable bidirectional file sharing between the host machine and sandbox environments -- ideal for sharing datasets, model checkpoints, configuration files, or collecting sandbox outputs.

## Scenarios

| # | Scenario | Description |
|---|----------|-------------|
| 1 | **Read-write mount** | Mount a host directory for bidirectional file exchange |
| 2 | **Read-only mount** | Provide shared data that sandboxes cannot modify |
| 3 | **SubPath mount** | Mount a specific subdirectory from the host path |

## Prerequisites

### 1. Start OpenSandbox Server

```shell
git clone git@github.com:opensandbox-group/OpenSandbox.git
cd OpenSandbox/server
cp opensandbox_server/examples/example.config.toml ~/.sandbox.toml
uv sync && uv run python -m opensandbox_server.main
```

### 2. Configure Allowed Host Paths

For security, the server restricts which host paths can be mounted. Add a `[storage]` section to `~/.sandbox.toml`:

```toml
[storage]
# Allowlist of host path prefixes permitted for bind mounts.
# Only paths under these prefixes can be mounted into sandboxes.
# If empty, all host paths are allowed (not recommended for production).
allowed_host_paths = ["/tmp/opensandbox-data", "/data/shared"]
```

::: warning Security note
In production, always set explicit `allowed_host_paths` to prevent sandboxes from accessing sensitive host directories. An empty list allows all paths, which is convenient for local development but not safe for shared environments.
:::

### 3. Create Host Directories

```shell
# Create a directory to share with sandboxes
mkdir -p /tmp/opensandbox-data
echo "hello-from-host" > /tmp/opensandbox-data/marker.txt

# Create a subdirectory for the subpath demo
mkdir -p /tmp/opensandbox-data/datasets/train
echo -e "id,value\n1,100\n2,200\n3,300" > /tmp/opensandbox-data/datasets/train/data.csv
```

### 4. Install SDK from Source

Volume support requires the latest SDK built from source (not yet available in the released package):

```shell
# From the project root (recommended: use uv)
uv pip install -e sdks/sandbox/python

# Or use pip inside a virtual environment
# python3 -m venv .venv && source .venv/bin/activate
# pip install -e sdks/sandbox/python
```

### 5. Pull the Sandbox Image

```shell
docker pull ubuntu:latest
```

## Run

```shell
HOST_VOLUME_PATH=/tmp/opensandbox-data uv run python examples/host-volume-mount/main.py
```

## Expected Output

```text
Using HOST_VOLUME_PATH: /tmp/opensandbox-data

OpenSandbox server : localhost:8080
Sandbox image      : ubuntu
Host volume path   : /tmp/opensandbox-data

============================================================
Scenario 1: Read-Write Host Volume Mount
============================================================
  Host path : /tmp/opensandbox-data
  Mount path: /mnt/shared

  [1] Listing files visible from inside the sandbox:
  ...

  [2] Writing a file from inside the sandbox:
  -> Written: /mnt/shared/sandbox-greeting.txt

  [3] Reading back the file:
  Hello from sandbox!

  Scenario 1 completed.

============================================================
Scenario 2: Read-Only Host Volume Mount
============================================================
  ...

============================================================
Scenario 3: SubPath Host Volume Mount
============================================================
  ...

============================================================
All scenarios completed successfully!
============================================================
```

## SDK Usage Quick Reference

### Python (async)

```python
from opensandbox import Sandbox
from opensandbox.models.sandboxes import Host, Volume

sandbox = await Sandbox.create(
    image="ubuntu",
    volumes=[
        Volume(
            name="my-data",
            host=Host(path="/data/shared"),
            mountPath="/mnt/data",
            readOnly=False,       # optional, default is False
            subPath="subdir",     # optional, mount a subdirectory
        ),
    ],
)
```

### Python (sync)

```python
from opensandbox import SandboxSync
from opensandbox.models.sandboxes import Host, Volume

sandbox = SandboxSync.create(
    image="ubuntu",
    volumes=[
        Volume(
            name="my-data",
            host=Host(path="/data/shared"),
            mountPath="/mnt/data",
        ),
    ],
)
```

### JavaScript / TypeScript

```typescript
import { Sandbox } from "@alibaba-group/opensandbox";

const sandbox = await Sandbox.create({
  image: "ubuntu",
  volumes: [
    {
      name: "my-data",
      host: { path: "/data/shared" },
      mountPath: "/mnt/data",
      readOnly: false,
    },
  ],
});
```

### Java / Kotlin

```java
Volume volume = Volume.builder()
    .name("my-data")
    .host(Host.of("/data/shared"))
    .mountPath("/mnt/data")
    .readOnly(false)
    .build();

Sandbox sandbox = Sandbox.builder()
    .image("ubuntu")
    .volume(volume)
    .build();
```

## References

- [Sandbox Lifecycle API Spec](https://github.com/opensandbox-group/OpenSandbox/blob/main/specs/sandbox-lifecycle.yml) -- OpenAPI schema for volume definitions
- [Server Configuration](https://github.com/opensandbox-group/OpenSandbox/blob/main/server/opensandbox_server/examples/example.config.toml) -- `[storage]` section for `allowed_host_paths`
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/host-volume-mount)

---

### Examples Kimi Cli (`docs/examples/kimi-cli.md`)

---
title: Kimi CLI
description: Run Kimi Code CLI (Moonshot AI) inside an OpenSandbox container.
---

# Kimi CLI Example

Run [Kimi Code CLI](https://github.com/MoonshotAI/kimi-cli) (Moonshot AI) inside an OpenSandbox container.

## Start OpenSandbox server [local]

Pre-pull the code-interpreter image (includes Python 3.12+):

```shell
docker pull sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0

# use docker hub
# docker pull opensandbox/code-interpreter:v1.1.0
```

Then start the local OpenSandbox server, stdout logs will be visible in the terminal:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Create and Access the Kimi Sandbox

```shell
# Install OpenSandbox package
uv pip install opensandbox

# Run the example (requires SANDBOX_DOMAIN / SANDBOX_API_KEY / KIMI_API_KEY)
uv run python examples/kimi-cli/main.py
```

The script installs Kimi Code CLI (`pip install kimi-cli`) at runtime (Python 3.12+ is already in the code-interpreter image), then sends a simple request `kimi -p "Compute 1+1=?."`. Auth is passed via `KIMI_API_KEY`, and you can override endpoint/model with `KIMI_BASE_URL` / `KIMI_MODEL_NAME`.

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Sandbox service address |
| `SANDBOX_API_KEY` | _(optional for local)_ | API key if your server requires authentication |
| `SANDBOX_IMAGE` | `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0` | Sandbox image to use |
| `KIMI_API_KEY` | _(required)_ | Your Moonshot AI / Kimi API key |
| `KIMI_BASE_URL` | _(optional)_ | Kimi API endpoint (defaults to Kimi's official endpoint) |
| `KIMI_MODEL_NAME` | `kimi-k2.5` | Model to use |

## References

- [Kimi Code CLI](https://github.com/MoonshotAI/kimi-cli) - Official Kimi Code CLI repository
- [Moonshot AI Platform](https://platform.moonshot.ai/) - API key management and documentation
- [Kimi CLI Documentation](https://moonshotai.github.io/kimi-cli/en/) - Full CLI documentation
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/kimi-cli)

---

### Examples Kubernetes Pvc Volume Mount (`docs/examples/kubernetes-pvc-volume-mount.md`)

---
title: Kubernetes PVC
description: Mount Kubernetes PersistentVolumeClaims into OpenSandbox containers for persistent storage.
---

# Kubernetes PVC Volume Mount

This example shows how to back a sandbox with a Kubernetes [PersistentVolumeClaim](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) (PVC). Data written to a PVC outlives the sandbox process, so files are still there when a follow-up sandbox mounts the same claim.

OpenSandbox supports two modes for sourcing the PVC. Pick based on who owns the claim's lifecycle:

| Mode | `createIfNotExists` | `deleteOnSandboxTermination` | Who owns the PVC | When to use |
|------|---------------------|------------------------------|------------------|-------------|
| **Bring your own** | `false` | _ignored_ | You (provisioned out-of-band) | Long-lived shared storage; model caches; baseline datasets that multiple sandboxes reuse |
| **Server-managed, persistent** | `true` | `false` (default) | You (after first create) | Provision on first use, keep across sandbox lifecycles |
| **Server-managed, ephemeral** | `true` | `true` | Server | Scratch storage scoped to a single sandbox; auto-cleaned when the sandbox terminates |

Both modes mount the resulting PVC the same way; the difference is only in provisioning and cleanup. See [PVC lifecycle](#pvc-lifecycle) for the cleanup mechanics in detail.

## Prerequisites

### CSI Driver

Kubernetes PVCs need a [Container Storage Interface (CSI)](https://kubernetes-csi.github.io/docs/drivers.html) driver to provision and attach storage. Install one that matches your storage backend. For example, the [Alibaba Cloud CSI Driver](https://github.com/kubernetes-sigs/alibaba-cloud-csi-driver) covers:

- **Cloud Disk (EBS)** -- block storage; high-performance single-node read-write
- **NAS** -- shared file storage; multi-node read-write (`ReadWriteMany`)
- **OSS** -- object storage; large-scale shared read
- **CPFS** -- high-performance parallel file system
- **LVM** -- local volume management

### OpenSandbox Server

The server must run on the Kubernetes runtime with the BatchSandbox workload provider. The stock Helm chart grants the RBAC needed by both BYO mounts and server-managed provisioning (`get`/`create`/`list`/`delete`/`patch` on `persistentvolumeclaims`).

### Python SDK

```shell
uv pip install opensandbox
```

## Mode 1: Bring your own PVC

Use this when the PVC is part of your platform setup -- e.g. a shared NAS that several sandboxes reuse, or a pre-warmed disk holding model weights.

### 1. Create the PVC

```yaml
# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
  namespace: opensandbox
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: <your-storage-class>
  resources:
    requests:
      storage: 10Gi
```

```shell
kubectl apply -f pvc.yaml
kubectl get pvc my-pvc -n opensandbox        # should be Bound
```

### 2. Mount it from a sandbox

```python
from opensandbox import Sandbox
from opensandbox.models.sandboxes import PVC, Volume

sandbox = await Sandbox.create(
    image="python:3.11",
    volumes=[
        Volume(
            name="data-volume",
            pvc=PVC(
                claimName="my-pvc",
                createIfNotExists=False,      # never auto-provision a BYO claim
            ),
            mountPath="/mnt/data",
            readOnly=False,
        ),
    ],
)

result = await sandbox.commands.run("ls -la /mnt/data")
print("\n".join(msg.text for msg in result.logs.stdout))
```

The PVC is never deleted by OpenSandbox: when the sandbox terminates, the claim stays bound and the data is available for the next sandbox that mounts it.

### Run the end-to-end example

```shell
export OPEN_SANDBOX_API_KEY=your-api-key
export OPEN_SANDBOX_BASE_URL=http://localhost:8080
export SANDBOX_PVC_NAME=my-pvc

python examples/kubernetes-pvc-volume-mount/main.py
```



The script creates a sandbox, writes a marker file under `/mnt/data`, kills it, then creates a second sandbox bound to the same PVC and confirms the marker is still there.

## Pool mode: pre-mount a shared PVC

Pool pods are created before a sandbox is allocated, so storage shared by a pool must be part of the Pool pod template. Create the PVC first, then mount it in every warm pod through `Pool.spec.template`:

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shared-workspace-pvc
  namespace: opensandbox
spec:
  accessModes: [ReadWriteMany]
  storageClassName: <your-rwx-storage-class>
  resources:
    requests:
      storage: 100Gi
---
apiVersion: sandbox.opensandbox.io/v1alpha1
kind: Pool
metadata:
  name: shared-workspace-pool
  namespace: opensandbox
spec:
  template:
    spec:
      containers:
        - name: sandbox-container
          image: python:3.11
          command: ["sleep", "3600"]
          volumeMounts:
            - name: shared-workspace
              mountPath: /workspace
      volumes:
        - name: shared-workspace
          persistentVolumeClaim:
            claimName: shared-workspace-pvc
  capacitySpec:
    bufferMax: 10
    bufferMin: 2
    poolMax: 20
    poolMin: 5
```

Apply the Pool manifest to the cluster before allocating sandboxes from it. Sandboxes then select the preconfigured Pool with `extensions.poolRef` and do not pass a per-sandbox `volumes` list.

::: warning Static pool storage only
The PVC must already exist, and its access mode and storage backend must allow all scheduled warm pods to mount it. OpenSandbox does not create, mutate, or delete PVCs referenced by a Pool template. A sandbox creation request cannot add `volumes` while also using `extensions.poolRef`, because the selected warm pod already exists.
:::

## Mode 2: Server-managed PVC

Use this when the sandbox should own its storage. The server creates the PVC on demand the first time the claim name is referenced. You control whether the claim survives sandbox termination through `deleteOnSandboxTermination`.

### Provision and persist (default)

The classic "first sandbox provisions, later sandboxes reuse" pattern -- e.g. a warm cache that survives crashes and restarts but never needs manual `kubectl apply`.

```python
sandbox = await Sandbox.create(
    image="python:3.11",
    volumes=[
        Volume(
            name="cache",
            pvc=PVC(
                claimName="agent-cache",
                createIfNotExists=True,                   # auto-provision on first use
                deleteOnSandboxTermination=False,         # default: keep the PVC
                storageClass="alibaba-cloud-disk-ssd",    # optional; defaults to cluster default
                storage="20Gi",                           # optional; defaults to server config
                accessModes=["ReadWriteOnce"],            # optional; defaults to ReadWriteOnce
            ),
            mountPath="/mnt/cache",
        ),
    ],
)
```

The first call provisions `agent-cache`; subsequent calls with the same `claimName` mount the existing PVC and skip provisioning. The PVC remains until you delete it with `kubectl`.

### Provision and clean up

Ephemeral scratch storage scoped to one sandbox -- the server reclaims the PVC when the sandbox terminates (including TTL expiry).

```python
sandbox = await Sandbox.create(
    image="python:3.11",
    timeout=600,                                   # 10-minute sandbox
    volumes=[
        Volume(
            name="scratch",
            pvc=PVC(
                claimName=f"scratch-{run_id}",   # unique per run; opted-in PVCs are owned exclusively
                createIfNotExists=True,
                deleteOnSandboxTermination=True,
                storage="5Gi",
            ),
            mountPath="/mnt/scratch",
        ),
    ],
)
```

::: tip Cleanup scope
The server only deletes PVCs it provisioned with this opt-in. Pre-existing PVCs and PVCs provisioned with `deleteOnSandboxTermination=false` are never touched. An opted-in PVC is exclusively owned by the sandbox that created it — a second sandbox attempting to mount the same `claimName` is rejected with `409 CONFLICT`. Use unique `claimName`s per sandbox, or use a non-opted-in / pre-existing PVC if you need to share storage.
:::

## PVC lifecycle

The cleanup behavior follows the mode used at create time:

| Source | Cleanup on sandbox termination |
|--------|-------------------------------|
| Pre-existing PVC (BYO) | Never touched by the server. |
| Auto-created, `deleteOnSandboxTermination=false` (default) | PVC persists; caller owns cleanup. |
| Auto-created, `deleteOnSandboxTermination=true` | Server deletes the PVC. |

For opted-in PVCs, the server labels them with `opensandbox.io/volume-managed-by=server` and `opensandbox.io/id=<sandbox-id>`, then runs cleanup through two layered paths:

1. **`ownerReferences`** -- the PVC is patched to point at the sandbox's workload custom resource right after creation. Kubernetes garbage collection cascade-deletes the PVC whenever the CR is removed, including controller-driven TTL expiry that never reaches the `DELETE /sandboxes/{id}` API.
2. **Label-selector sweep** -- on `DELETE /sandboxes/{id}`, the server lists PVCs by those labels and deletes them best-effort. This catches cases where the `ownerReferences` patch failed (e.g. RBAC) and ensures the PVC is gone as soon as the API returns.

Both paths only match server-labeled PVCs, so BYO and opted-out claims are never reclaimed. The underlying PV follows its `StorageClass.reclaimPolicy` once the PVC is deleted.

## Important notes

::: warning
- Per-sandbox `volumes` cannot be combined with `extensions.poolRef`. For Pool mode, pre-mount an existing shared PVC in `Pool.spec.template` as described above.
- Multiple sandboxes can mount the same PVC if the access mode allows (e.g. `ReadWriteMany`) — but only when the PVC is **not** opted into auto-cleanup. PVCs created with `deleteOnSandboxTermination=true` are owned exclusively by the creating sandbox; the server rejects attempts by other sandboxes to mount them with `409 CONFLICT`.
- All mounts of the same `claimName` in a single request must agree on `createIfNotExists` and `deleteOnSandboxTermination`; mismatches are rejected with `400 INVALID_PARAMETER`.
:::

## References

- [Kubernetes CSI Drivers](https://kubernetes-csi.github.io/docs/drivers.html)
- [Alibaba Cloud CSI Driver](https://github.com/kubernetes-sigs/alibaba-cloud-csi-driver)
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/kubernetes-pvc-volume-mount)

---

### Examples Langgraph (`docs/examples/langgraph.md`)

---
title: LangGraph
description: Integrate LangGraph with OpenSandbox using a graph-driven control flow for agent orchestration.
---

# LangGraph Agent + OpenSandbox Example

Integrate LangGraph with OpenSandbox using a graph-driven control flow. The example uses explicit state machine nodes to create, prepare, run, inspect, and clean up a sandbox, plus a decision node to retry with a fallback command if the run step fails.

## Start OpenSandbox server [local]

Pre-pull the code-interpreter image (includes Python):

```shell
docker pull sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0

# use docker hub
# docker pull opensandbox/code-interpreter:v1.1.0
```

Start the local OpenSandbox server, logs will be visible in the terminal:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Run the example

```shell
# Install OpenSandbox + LangGraph deps
uv pip install opensandbox langgraph langchain-anthropic

# Run the example (requires SANDBOX_DOMAIN / SANDBOX_API_KEY / ANTHROPIC_API_KEY)
uv run python examples/langgraph/main.py
```

The workflow writes files, executes a job, retries with a fallback command on failure (default `python` vs `python3`), then summarizes results with Claude and cleans up the sandbox instance.



## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Sandbox service address |
| `SANDBOX_API_KEY` | _(optional for local)_ | API key if your server requires authentication |
| `SANDBOX_IMAGE` | `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0` | Sandbox image to use |
| `ANTHROPIC_API_KEY` | _(required if `ANTHROPIC_AUTH_TOKEN` is unset)_ | Your Anthropic API key |
| `ANTHROPIC_AUTH_TOKEN` | _(alternate)_ | Alternate Anthropic auth token (uses `Authorization` header) |
| `ANTHROPIC_BASE_URL` | _(optional)_ | Anthropic API endpoint override |
| `ANTHROPIC_MODEL` | `claude-3-5-sonnet-latest` | Model to use |

::: warning
`ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` should not be set together.
:::

## References

- [LangGraph](https://langchain-ai.github.io/langgraph/) - Agent workflow framework
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/langgraph)

---

### Examples Microsoft Agent Framework (`docs/examples/microsoft-agent-framework.md`)

---
title: Microsoft Agent Framework
description: Run a general-purpose Microsoft Agent Framework agent with OpenSandbox.
---

# Microsoft Agent Framework Example

Run a general-purpose Microsoft Agent Framework agent alongside an OpenSandbox workflow. The example uses Agent Framework's functional workflow API to create a sandbox, prepare and execute a Python job, retry with a fallback command when needed, ask the agent an English question, and destroy the sandbox.

The Python client uses the OpenSandbox SDK. The local OpenSandbox Server creates and manages the container through its Docker runtime.

The sandbox uses the Docker Hub image `opensandbox/code-interpreter:v1.1.0`; no Alibaba Cloud container registry is required.

## Start OpenSandbox server [local]

Pre-pull the code-interpreter image (includes Python 3.12+):

```shell
docker pull opensandbox/code-interpreter:v1.1.0
```

Create a Python 3.10+ environment and install the packages declared by the example project:

```shell
uv venv
source .venv/bin/activate

uv pip install \
  --default-index https://packagefeedproxy.microsoft.io/pypi/simple \
  "agent-framework-core==1.19.0" \
  "agent-framework-openai==1.14.4" \
  "opensandbox==1.1.0" \
  "opensandbox-server==0.2.3" \
  "python-dotenv>=1.0,<2"
```

The selective Agent Framework packages provide the workflow, agent, and OpenAI/Azure OpenAI client APIs used by this example without installing unrelated integrations. The Microsoft package proxy matches the package source configured by the example project's `pyproject.toml`.

Initialize and start the local Docker runtime:

```shell
uv run opensandbox-server init-config ~/.sandbox.toml --example docker
OPENSANDBOX_INSECURE_SERVER=YES uv run opensandbox-server
```

`OPENSANDBOX_INSECURE_SERVER=YES` explicitly acknowledges that this local server has no API key. Keep it bound to localhost. For a shared deployment, configure `server.api_key` instead.

The project pins `opensandbox-server==0.2.3`. The upstream `1.1.0` server wheel is missing its generated FastPath gRPC modules and fails during startup.

## Configure the Agent

Copy the environment template:

```shell
cp examples/microsoft-agent-framework/.env.example .env
```

Configure either OpenAI:

```shell
OPENAI_API_KEY=replace_me
OPENAI_MODEL=gpt-4o-mini
```

Or Azure OpenAI:

```shell
AZURE_OPENAI_API_KEY=replace_me
AZURE_OPENAI_ENDPOINT=https://<your-resource>.openai.azure.com/
AZURE_OPENAI_MODEL=<your-deployment-name>
```

When both configurations exist, `OPENAI_API_KEY` takes precedence. Remove it to use the Azure OpenAI environment fallback.

## Run the Example

Run from the repository root:

```shell
uv run python examples/microsoft-agent-framework/main.py
```

The default English prompt is:

```text
What model are you using?
```

Set another question in `.env`, or override it for one invocation:

```shell
AGENT_QUESTION="Explain what OpenSandbox does." \
  uv run python examples/microsoft-agent-framework/main.py
```

The workflow writes and executes a small Python job in the sandbox, then passes the question, configured model name, and sandbox result to a general-purpose Agent Framework agent. The sandbox is stopped and closed even when execution or the agent call fails.

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Local sandbox service address |
| `SANDBOX_API_KEY` | _(optional for local)_ | API key if configured |
| `SANDBOX_IMAGE` | `opensandbox/code-interpreter:v1.1.0` | Docker Hub sandbox image |
| `SANDBOX_COMMAND` | `python3 /tmp/math.py` | Initial sandbox command |
| `SANDBOX_FALLBACK_COMMAND` | `python /tmp/math.py` | Retry command |
| `AGENT_QUESTION` | `What model are you using?` | English question sent to the agent |
| `OPENAI_API_KEY` | _(required for OpenAI)_ | OpenAI API key |
| `OPENAI_MODEL` | `gpt-4o-mini` in `.env.example` | OpenAI model |
| `AZURE_OPENAI_API_KEY` | _(required for Azure API-key auth)_ | Azure OpenAI API key |
| `AZURE_OPENAI_ENDPOINT` | _(required for Azure OpenAI)_ | Azure OpenAI endpoint |
| `AZURE_OPENAI_MODEL` | _(required for Azure OpenAI)_ | Azure deployment name |

## References

- [Microsoft Agent Framework Python samples](https://github.com/microsoft/agent-framework/tree/main/python/samples)
- [Functional workflow with agents](https://github.com/microsoft/agent-framework/blob/main/python/samples/01-get-started/05_functional_workflow_with_agents.py)
- [OpenSandbox](https://github.com/opensandbox-group/OpenSandbox)
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/microsoft-agent-framework)

---

### Examples Openclaw (`docs/examples/openclaw.md`)

---
title: OpenClaw
description: Launch an OpenClaw Gateway inside an OpenSandbox instance and expose its HTTP endpoint.
---

# OpenClaw Gateway Example

Launch an [OpenClaw](https://github.com/openclaw/openclaw) Gateway inside an OpenSandbox instance and expose its HTTP endpoint. The script polls the gateway until it returns HTTP 200, then prints the reachable endpoint.

## Quick Start

```shell
# Install dependencies
uv pip install opensandbox requests

# Run with default settings
uv run python examples/openclaw/main.py
```

## Configuration Options

| Variable | Default | Description |
|----------|---------|-------------|
| `OPENCLAW_SERVER` | `http://localhost:8080` | OpenSandbox server address |
| `OPENCLAW_TOKEN` | `dummy-token-for-sandbox` | Gateway authentication token |
| `OPENCLAW_IMAGE` | `ghcr.io/openclaw/openclaw:latest` | Container image |
| `OPENCLAW_TIMEOUT` | `3600` | Sandbox timeout in seconds |

## Network Policy

By default, the sandbox denies all network access except `pypi.org` (for package installation). You can customize this in `main.py`:

```python
network_policy=NetworkPolicy(
    defaultAction="deny",
    egress=[
        NetworkRule(action="allow", target="pypi.org"),
        NetworkRule(action="allow", target="pypi.python.org"),
        # Add more allowed targets
    ],
)
```

## Environment Variables for OpenClaw

Pass environment variables to the OpenClaw Gateway inside the sandbox:

```python
env={
    "OPENCLAW_GATEWAY_TOKEN": token,
    "OPENCLAW_MODEL": "claude-sonnet-4-20250514",
    # Add more env vars as needed
},
```

## Start OpenSandbox server [local]

You can find the latest OpenClaw container image [here](https://github.com/openclaw/openclaw/pkgs/container/openclaw).

::: info Docker runtime requirement
The server uses `runtime.type = "docker"` by default, so it **must** be able to reach a running Docker daemon.

- **Docker Desktop**: ensure Docker Desktop is running, then verify with `docker version`.
- **Colima (macOS)**: start it first (`colima start`) and export the socket before starting the server:

```shell
export DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock"
```
:::

Pre-pull the OpenClaw image:

```shell
docker pull ghcr.io/openclaw/openclaw:latest
```

Start the OpenSandbox server (logs will stay in the terminal):

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Create and Access the OpenClaw Sandbox

This example is hard-coded for a quick start:
- OpenSandbox server: `http://localhost:8080`
- Image: `ghcr.io/openclaw/openclaw:latest`
- Gateway port: `18789`
- Timeout: `3600s`
- Token: `OPENCLAW_GATEWAY_TOKEN` (default: `dummy-token-for-sandbox`)

Install dependencies from the project root:

```shell
uv pip install opensandbox requests
```

Run the example (set a real token if you need authenticated access):

```shell
export OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)"
uv run python examples/openclaw/main.py
```

You should see output similar to:

```text
Creating openclaw sandbox with image=ghcr.io/openclaw/openclaw:latest on OpenSandbox server http://localhost:8080...
[check] sandbox ready after 7.1s
Openclaw started finished. Please refer to 127.0.0.1:56123
```

## Advanced: Custom Gateway Port

To use a custom port, modify the `entrypoint` in `main.py`:

```python
entrypoint=["node dist/index.js gateway --bind=lan --port 19999 --allow-unconfigured --verbose"],
```

Then update the port in the `get_endpoint()` call:

```python
endpoint = sandbox.get_endpoint(19999)
```

## References

- [OpenClaw](https://github.com/openclaw/openclaw)
- [OpenSandbox Python SDK](https://pypi.org/project/opensandbox/)
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/openclaw)

---

### Examples Opencode (`docs/examples/opencode.md`)

---
title: OpenCode
description: Run the OpenCode coding agent inside an OpenSandbox container.
---

# OpenCode Example

Run [OpenCode](https://opencode.ai/) inside an OpenSandbox container. The example installs the official CLI and executes a prompt non-interactively with `opencode run`.

## Start OpenSandbox server [local]

Pre-pull the code-interpreter image (includes Node.js):

```shell
docker pull sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0

# use Docker Hub
# docker pull opensandbox/code-interpreter:v1.1.0
```

Then start the local OpenSandbox server. Logs will be visible in the terminal:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Create and Access the OpenCode Sandbox

```shell
# Install the OpenSandbox package
uv pip install opensandbox

# Run the example. The default free model does not require an API key.
uv run python examples/opencode/main.py
```

The script installs OpenCode (`npm install opencode-ai@latest`) at runtime, creates an isolated working directory, and runs `opencode run` with a simple prompt. The default model is currently available without authentication. Set `OPENCODE_API_KEY` and `OPENCODE_MODEL` to use an authenticated model from the OpenCode provider instead.

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Sandbox service address |
| `SANDBOX_API_KEY` | _(optional for local)_ | API key if your server requires authentication |
| `SANDBOX_IMAGE` | `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0` | Sandbox image to use |
| `OPENCODE_MODEL` | `opencode/deepseek-v4-flash-free` | Model ID in `provider/model` format |
| `OPENCODE_API_KEY` | _(optional)_ | API key for authenticated models from the OpenCode provider |

::: info
The default free model is offered by OpenCode for a limited time. Do not send private code, credentials, or other confidential data to a free model; review the provider's data-use terms before using it. Override `OPENCODE_MODEL` when you need a different model. Other providers may require additional environment variables or configuration.
:::

## References

- [OpenCode CLI](https://opencode.ai/docs/cli/) - Installation and non-interactive `run` command
- [OpenCode Providers](https://opencode.ai/docs/providers) - Provider authentication and configuration
- [OpenCode Zen](https://opencode.ai/docs/zen/) - Model availability and data-use notes
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/opencode)

---

### Examples Playwright (`docs/examples/playwright.md`)

---
title: Playwright
description: Access web pages in headless mode using Playwright + Chromium in OpenSandbox.
---

# Playwright Example

Access web pages in headless mode using Playwright + Chromium in OpenSandbox to scrape title/body snippets.

## Build the Playwright Sandbox Image

The Dockerfile in the example directory builds a sandbox image with Playwright and Chromium pre-installed:

```shell
cd examples/playwright
docker build -t opensandbox/playwright:latest .
```

This image includes:
- Playwright Python package
- Chromium browser binaries
- Node.js and npm (for Playwright MCP)
- Non-root user (playwright) for security

## Start OpenSandbox server [local]

Pre-pull the Playwright image:

```shell
docker pull sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/playwright:latest
```

Start the local OpenSandbox server:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Create and Access the Playwright Sandbox

```shell
# Install OpenSandbox package
uv pip install opensandbox

uv run python examples/playwright/main.py
```

The script launches Chromium in headless mode to access the target URL, prints title/body snippets, and saves a full-page screenshot to `/home/playwright/screenshot.png` inside the sandbox. It also downloads the screenshot to the local working directory as `./screenshot.png`. Uses the prebuilt Playwright image by default.



## References

- [Playwright](https://playwright.dev/)
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/playwright)

---

### Examples Qwen Code (`docs/examples/qwen-code.md`)

---
title: Qwen Code
description: Run Qwen Code inside an OpenSandbox container through an OpenAI-compatible endpoint.
---

# Qwen Code Example

Run [Qwen Code](https://github.com/QwenLM/qwen-code) inside an OpenSandbox container through an OpenAI-compatible endpoint.

## Start OpenSandbox server [local]

Pre-pull the code-interpreter image (includes Node.js):

```shell
docker pull sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0

# use docker hub
# docker pull opensandbox/code-interpreter:v1.1.0
```

Then start the local OpenSandbox server, stdout logs will be visible in the terminal:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Create and Access the Qwen Sandbox

```shell
# Install OpenSandbox package
uv pip install opensandbox

# Export provider settings
export API_KEY=your-api-key
export BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
export MODEL_NAME=qwen3-coder-plus

# Run the example
uv run python examples/qwen-code/main.py
```

The script installs Qwen Code (`npm install @qwen-code/qwen-code@latest`) at runtime, writes a project-local `.qwen/settings.json` inside the sandbox, and runs `qwen -p "Compute 1+1 and reply with only the final number."` in headless mode. The API key is injected only through the `API_KEY` environment variable and is not written into the repository.

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Sandbox service address |
| `SANDBOX_API_KEY` | _(optional for local)_ | API key if your server requires authentication |
| `SANDBOX_IMAGE` | `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0` | Sandbox image to use |
| `API_KEY` | _(required)_ | API key for the OpenAI-compatible provider used by Qwen Code |
| `BASE_URL` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI-compatible base URL |
| `MODEL_NAME` | `qwen3-coder-plus` | Model name for Qwen Code |

## References

- [Qwen Code](https://github.com/QwenLM/qwen-code) - Official repository
- [Qwen Code Authentication](https://qwenlm.github.io/qwen-code-docs/en/users/configuration/auth/) - Provider configuration reference
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/qwen-code)

---

### Examples Rclone Volume Mount (`docs/examples/rclone-volume-mount.md`)

---
title: rclone Volume Mount
description: Mount rclone-backed Docker volumes or Kubernetes PVCs into OpenSandbox using the existing volume API.
---

# rclone Volume Mount

Use an externally provisioned rclone volume to expose remote storage to a sandbox. OpenSandbox attaches the volume through its existing `pvc` backend; the Docker volume plugin or Kubernetes CSI driver manages the remote configuration, credentials, FUSE process, cache, and unmount lifecycle.

This integration does not add a `rclone` backend or `driver_opts` field to the OpenSandbox API. Configure the storage driver before creating a sandbox.

## Docker setup

Use a Linux Docker host with FUSE support, the rclone CLI, and an OpenSandbox server configured for the Docker runtime. Run the following setup on the same Docker daemon used by the server. The sandbox image does not need rclone or additional FUSE privileges.

### Configure the remote and install the plugin

Create the plugin directories and configure a remote named `storage`. Choose your backend and credentials in the interactive rclone configuration wizard:

```shell
sudo install -d -m 700 /var/lib/docker-plugins/rclone/config
sudo install -d -m 700 /var/lib/docker-plugins/rclone/cache
sudo rclone config --config /var/lib/docker-plugins/rclone/config/rclone.conf
sudo chmod 600 /var/lib/docker-plugins/rclone/config/rclone.conf

# This example uses the amd64 plugin; select the image for your host architecture.
docker plugin install rclone/docker-volume-rclone:amd64 --alias rclone
docker plugin ls
```

Review the requested plugin permissions when Docker prompts. The plugin runs outside the sandbox and needs host-level access for FUSE. Follow the [rclone Docker plugin documentation](https://rclone.org/docker/) for host prerequisites, architecture choices, and plugin upgrades.

Keep storage credentials in the operator-managed configuration. Do not put passwords or tokens in sandbox environment variables or commit them in examples.

### Create a named volume

Use a dedicated, writable test directory on the remote. Replace `storage:opensandbox-demo` with the path appropriate for your backend (for example, an S3 bucket and prefix or an SFTP directory):

```shell
docker volume create --driver rclone \
  --opt remote=storage:opensandbox-demo \
  --opt allow-other=true \
  --opt vfs-cache-mode=writes \
  opensandbox-rclone-demo

docker volume inspect --format '{{.Driver}}' opensandbox-rclone-demo
docker run --rm --mount type=volume,src=opensandbox-rclone-demo,dst=/data \
  alpine:3.21 ls -la /data
```

Confirm that the inspected driver is `rclone` (or the plugin name you installed), and that the temporary container can access the expected directory. A named volume alone does not verify remote connectivity.

::: warning Required Docker settings
- Set `createIfNotExists=False`. Otherwise a missing or misspelled volume name can cause the server to create an ordinary Docker volume without rclone options.
- Omit `subPath`: Docker `subPath` handling currently requires the `local` driver. Restrict the remote directory using the plugin's `remote` option instead.
- Provision the volume on the server's Docker daemon, even when the SDK runs on another machine.
:::

## Mount from the SDK

The same request works with a pre-created Docker named volume or Kubernetes PVC:

```python
from opensandbox import Sandbox
from opensandbox.models.sandboxes import PVC, Volume

sandbox = await Sandbox.create(
    image="python:3.11",
    volumes=[
        Volume(
            name="remote-storage",
            pvc=PVC(
                claimName="opensandbox-rclone-demo",
                createIfNotExists=False,
                deleteOnSandboxTermination=False,
            ),
            mountPath="/mnt/remote",
            readOnly=True,
        ),
    ],
)

async with sandbox:
    try:
        result = await sandbox.commands.run("ls -la /mnt/remote")
        if result.error or result.exit_code not in (None, 0):
            raise RuntimeError("Could not read the remote volume")
        print("".join(message.text for message in result.logs.stdout))
    finally:
        await sandbox.kill()
```

## Run the verification example

From the repository root, install the Python SDK and run:

```shell
uv pip install -e sdks/sandbox/python
export OPEN_SANDBOX_DOMAIN=localhost:8080
export OPEN_SANDBOX_API_KEY=your-api-key
export SANDBOX_VOLUME_NAME=opensandbox-rclone-demo
python examples/rclone-volume-mount/main.py
```

The example uses `python:3.11` by default; `SANDBOX_IMAGE` can select another Linux image with `python3`. It writes a uniquely named marker, checks the contents, deletes the first sandbox, and reads the marker from a second sandbox with a read-only mount. It also verifies that writing through that mount fails with a read-only-filesystem error. Command failures and content mismatches fail the example instead of printing a success message.

Both sandboxes are cleaned up, but the external volume and marker are retained. The script prints the marker filename and expected content. For Docker, verify the marker independently against the remote, substituting that filename:

```shell
sudo rclone --config /var/lib/docker-plugins/rclone/config/rclone.conf \
  cat storage:opensandbox-demo/MARKER_FILENAME
```

Reading the marker through a second sandbox demonstrates reuse of the mounted volume, not necessarily remote durability: the driver may still serve cached data. Allow pending uploads to finish and independently verify the remote before removing the test file or plugin. See rclone's [VFS file caching documentation](https://rclone.org/commands/rclone_mount/#vfs-file-caching).

## Kubernetes setup

Have the cluster operator provision an rclone-backed PV/PVC with a CSI driver supported by your platform. The operator must configure the driver's credentials or Secret references, remote path, cache, and mount options according to that driver's documentation; these settings are not fields in the OpenSandbox API.

Use the [bring-your-own PVC workflow](/examples/kubernetes-pvc-volume-mount#mode-1-bring-your-own-pvc):

1. Create the PVC in the sandbox workload namespace and confirm it is usable with the selected driver. A `WaitForFirstConsumer` claim can remain Pending until a workload is scheduled.
2. Set `SANDBOX_VOLUME_NAME` to the PVC name and point the SDK at the Kubernetes OpenSandbox server.
3. Run the same verification example. Check the marker independently on the remote using the operator's credentials and verify driver cleanup.

For Pool mode, mount the PVC in the Pool pod template in advance. Per-sandbox `volumes` cannot be combined with `extensions.poolRef`; the example above is for on-demand sandboxes. See [static pool storage](/examples/kubernetes-pvc-volume-mount#pool-mode-pre-mount-a-shared-pvc).

## Operational boundaries and cleanup

- Pre-existing volumes are not deleted by OpenSandbox when a sandbox terminates. The operator owns their configuration, credentials, and cleanup.
- Use a dedicated remote path and appropriately scoped credentials for each tenant. A volume name is not an authorization boundary.
- Backend semantics differ: do not assume local-filesystem locking, atomic rename, or immediate visibility of concurrent writes. Validate your workload against the selected backend and cache settings.
- Inspect plugin/CSI logs for mount failures, expired credentials, or unfinished uploads. Sandbox readiness alone does not prove the remote is writable.

After independently confirming the marker on the remote, remove only the marker created by this run. For Docker, replace `MARKER_FILENAME` below:

```shell
sudo rclone --config /var/lib/docker-plugins/rclone/config/rclone.conf \
  deletefile storage:opensandbox-demo/MARKER_FILENAME
docker volume rm opensandbox-rclone-demo
```

Remove the Docker volume only after all containers using it have stopped and pending uploads have completed. For Kubernetes, follow the CSI driver's cleanup procedure and check the PV reclaim policy before deleting the PVC.

## References

- [rclone Docker Volume Plugin](https://rclone.org/docker/)
- [Docker named volume example](/examples/docker-pvc-volume-mount)
- [Kubernetes PVC example](/examples/kubernetes-pvc-volume-mount)
- [Example source](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/rclone-volume-mount)

---

### Examples Vscode (`docs/examples/vscode.md`)

---
title: VS Code
description: Launch code-server (VS Code Web) in OpenSandbox to provide browser-based IDE access.
---

# VS Code Example

Launch code-server (VS Code Web) in OpenSandbox to provide browser access.

## Build the VS Code Sandbox Image

The Dockerfile in the example directory builds a sandbox image with code-server pre-installed:

```shell
cd examples/vscode
docker build -t opensandbox/vscode:latest .
```

This image includes:
- code-server (VS Code Web) pre-installed
- Non-root user (vscode) for security
- Workspace directory at `/workspace`

## Start OpenSandbox server [local]

Pre-pull the VS Code image:

```shell
docker pull sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/vscode:latest
```

Start the local OpenSandbox server:

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Create and Access the VS Code Sandbox

```shell
# Install OpenSandbox package
uv pip install opensandbox

uv run python examples/vscode/main.py
```

The script starts code-server (with authentication disabled), binds it to the specified port and outputs the accessible address. Uses the prebuilt VS Code image by default.

 

## References

- [code-server (VS Code Web)](https://github.com/coder/code-server)
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/vscode)

---

### Examples Windows (`docs/examples/windows.md`)

---
title: Windows
description: Run a Windows guest in an OpenSandbox sandbox via KVM/QEMU using the dockur/windows image.
---

# Windows Sandbox Example

Run a Windows guest in an OpenSandbox sandbox via KVM/QEMU using the [`dockur/windows`](https://github.com/dockur/windows) image.

## How it works

OpenSandbox creates a Linux container running KVM/QEMU, which boots a Windows guest OS inside it. The Windows profile (`platform.os=windows`) automatically configures the required devices, capabilities, OEM scripts, and port mappings -- you only need to specify `platform` and `resource` in the SDK call.

## Prerequisites

- OpenSandbox server running (e.g. `http://localhost:8080`)
- Host with `/dev/kvm` and `/dev/net/tun` present
- Server `storage.allowed_host_paths` configured for any host bind mounts

## Start OpenSandbox server [local]

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server
```

## Run the example

```shell
uv pip install opensandbox
cd examples/windows
python main.py
```

The script will:

1. Create a Windows sandbox with `dockurr/windows:latest` and Windows 11
2. Wait until the sandbox is healthy (first boot can take several minutes)
3. Print the execd, RDP (3389), and web console (8006) endpoints
4. Execute a test command and print the output

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Sandbox service address |
| `SANDBOX_API_KEY` | _(optional for local)_ | API key if your server requires authentication |

## Customization

### Resource limits

The Windows profile enforces minimum resources: **cpu >= 2, memory >= 4G, disk >= 64G**. The example uses 4 CPU, 8G RAM, and 64G disk. You can adjust these in the `main.py` `resource` dict.

### Persistent storage

Bind a host directory to `/storage` for a persistent system disk (add to the `SandboxSync.create` call):

```python
from opensandbox.models.sandboxes import Host, Volume

volumes = [
    Volume(
        name="win-storage",
        host=Host(path="/data/opensandbox/windows-storage"),
        mount_path="/storage",
        read_only=False,
    ),
]
```

### Local ISO

Bind a Windows install ISO to `/boot.iso` to avoid repeated downloads:

```python
volumes = [
    Volume(
        name="win-iso",
        host=Host(path="/data/iso/Win11_23H2.iso"),
        mount_path="/boot.iso",
        read_only=True,
    ),
]
```

### Windows guest configuration

Pass [dockur/windows environment variables](https://github.com/dockur/windows) through the `env` parameter:

```python
env = {
    "VERSION": "11l",
    "USERNAME": "Docker",
    "PASSWORD": "your-secure-password",
    "LANGUAGE": "Chinese",
    "REGION": "zh-CN",
    "KEYBOARD": "zh-CN",
}
```

::: warning
Do not manually set `CPU_CORES`, `RAM_SIZE`, or `DISK_SIZE` -- they are derived from `resourceLimits` automatically.
:::

## Exposed ports

| Port | Service |
|------|---------|
| 44772 | execd (sandbox execution API) |
| 8080 | HTTP service |
| 3389 | RDP (native Remote Desktop) |
| 8006 | Web console (noVNC) |

## Troubleshooting

- **`Unsupported platform.os 'windows'`**: Server build has no Windows profile; upgrade OpenSandbox server.
- **`INVALID_PARAMETER` for resourceLimits**: Ensure cpu >= 2, memory >= 4G, disk >= 64G.
- **Stays Pending a long time**: First Windows install is slow; check host resources and `/storage` space, increase `ready_timeout`.
- **Status Running but endpoint unreachable**: Verify endpoint resolution returns a valid address; check `USER_PORTS` if you need additional ports forwarded.

### ENI CNI network issue (Alibaba Cloud ACK)

On clusters using ENI-based CNIs (e.g. Alibaba Cloud ACK Terway in ENI mode), dockur/windows fails at startup with:

```
> ERROR: This container does not support host mode networking!
```

or:

```
> ERROR: Status 1 while: ethtool -i "$VM_NET_DEV"
```

**Root cause**: The image's `network.sh` uses `ethtool -i` to check the network interface. ENI interfaces have real PCI bus-info, which triggers a false "host mode" detection. Standard veth-based CNIs (Calico, Flannel, Cilium) do NOT have this problem.

**Solution**: Use the provided `main_fix_net.py` example, which patches the script at runtime and sets `NETWORK=slirp` for QEMU user-mode NAT:

```shell
cd examples/windows
python main_fix_net.py
```

See [`main_fix_net.py`](https://github.com/opensandbox-group/OpenSandbox/blob/main/examples/windows/main_fix_net.py) for the full implementation.

**How it works**:

1. `sed` replaces three lines in `/run/network.sh` with empty variable assignments (`result=""`, `nic=""`, `bus=""`), preventing the ethtool check from aborting the script.
2. `NETWORK=slirp` tells the script to use QEMU's SLIRP networking (user-mode NAT), which doesn't require a real NIC.
3. `exec /usr/bin/tini -s /run/entry.sh` launches the original image entrypoint after patching.

This approach keeps the Pod's independent IP and requires no image rebuild or `hostNetwork`.

## Windows Sandbox from pool

Use a pre-warmed K8s pool for faster Windows sandbox startup.

### 1. Create the pool

Apply the pool manifest (the image, resources, device mounts, and OEM scripts are pre-configured):

```shell
cd examples/windows
kubectl apply -f pool-win-example.yaml
```

### 2. Start the OpenSandbox server [k8s]

```shell
uv pip install opensandbox-server
opensandbox-server init-config ~/.sandbox.toml --example k8s
opensandbox-server
```

### 3. Run the pool example

```shell
uv pip install opensandbox
cd examples/windows
python main_use_pool.py
```

The script acquires a sandbox from `pool-win-example`, prints endpoints, and runs a command.

### Environment variables (pool)

| Variable | Default | Description |
|----------|---------|-------------|
| `SANDBOX_DOMAIN` | `localhost:8080` | Sandbox service address |
| `SANDBOX_API_KEY` | _(optional)_ | API key if your server requires authentication |

## References

- [Windows sandbox guide](/guides/windows-sandbox)
- [dockur/windows](https://github.com/dockur/windows)
- [Source code on GitHub](https://github.com/opensandbox-group/OpenSandbox/tree/main/examples/windows)

---

### Getting Started Configuration (`docs/getting-started/configuration.md`)

---
title: Configuration
description: Server configuration reference for OpenSandbox, covering TOML config, runtimes, networking, and authentication.
---

# Configuration

The OpenSandbox server reads a **TOML** configuration file. Default path: `~/.sandbox.toml`. Override with the `SANDBOX_CONFIG_PATH` environment variable or the `--config` flag.

## Generate a Config File

```bash
# Docker runtime (default)
opensandbox-server init-config ~/.sandbox.toml --example docker

# Kubernetes runtime
opensandbox-server init-config ~/.sandbox.toml --example k8s

# Schema-only skeleton (no defaults)
opensandbox-server init-config ~/.sandbox.toml

# Overwrite existing config
opensandbox-server init-config ~/.sandbox.toml --example docker --force
```

## Run the Server

```bash
opensandbox-server
# or specify a config path
opensandbox-server --config /path/to/sandbox.toml
```

The server listens on the `server.host` / `server.port` values from your TOML config.

## Authentication

Authentication is enforced when `server.api_key` is set. All API endpoints (except `/health`, `/version`, `/docs`, `/redoc`, and `/openapi.json`) require the `OPEN-SANDBOX-API-KEY` header:

```bash
curl -H "OPEN-SANDBOX-API-KEY: your-secret-api-key" http://localhost:8080/v1/sandboxes
```

::: warning
If `server.api_key` is empty, the server runs without authentication. In non-interactive environments (Docker/Kubernetes/CI), set `OPENSANDBOX_INSECURE_SERVER=YES` to acknowledge the risk. **Always set an API key in production.**
:::

## Key Configuration Areas

| Section | Description |
|---------|-------------|
| `[server]` | Host, port, API key, and general server settings |
| `[runtime]` | Runtime selection and execd image configuration |
| `[otel]` | OpenTelemetry export for SDK create-latency metrics |
| `[proxy]` | Server-side sandbox proxy target selection; use host mappings when the server cannot route to sandbox IPs |
| `[docker]` | Docker runtime networking, host address, and image-pull settings |
| `[kubernetes]` | Kubernetes runtime: `workload_provider`, `batchsandbox_template_file` |
| `[egress]` | Egress sidecar for `networkPolicy` enforcement |
| `[ingress]` | Ingress gateway configuration |
| `[secure_runtime]` | Secure container runtime (gVisor, Kata, Firecracker) |
| `[store]` | Persistence backend (default: SQLite at `~/.opensandbox/opensandbox.db`) |
| `[renew_intent]` | Auto-renew on access (experimental) |
| `[agent_sandbox]` | Agent sandbox settings for Kubernetes |

For the full configuration reference with all keys and defaults, see the [server configuration.md](https://github.com/opensandbox-group/OpenSandbox/blob/main/server/configuration.md).

## Sandbox Expiration

`server.max_sandbox_timeout_seconds` limits the `timeout` supplied when creating a sandbox. If configured, it must be at least `60` seconds; omit it to disable the creation-time upper bound. It does not limit renewal requests or the total lifetime of a sandbox.

`POST /v1/sandboxes/{sandboxId}/renew-expiration` sets an absolute `expiresAt`. Despite the endpoint name, it can shorten, preserve, or extend the current expiration, provided the requested timestamp is still in the future. A timestamp beyond `now + max_sandbox_timeout_seconds` is also accepted.

Duration-based SDK `renew()` methods calculate the target as `now + duration`, not `current expiresAt + duration`. For example, renewing for five minutes when thirty minutes remain moves expiration earlier. Callers that require extension only must account for the current expiration before choosing a target.

## Kubernetes Creation Wait

For the BatchSandbox and agent-sandbox providers, the server waits for the workload status to become `Running` or `Allocated` before returning a successful create response. This does not check whether your application inside the sandbox is ready.

Creation requests share the existing resource watches. A workload change wakes its waiting request immediately, which evaluates the object carried by the event without another API read. The initial check and periodic fallback still use normal cache/API reads. Events overlapping a read or describing deletion trigger a fresh check. Providers without subscription support continue polling.

| `[kubernetes]` setting | Default | Meaning |
|------------------------|---------|---------|
| `sandbox_create_timeout_seconds` | `60` | Overall creation wait limit |
| `pool_acquisition_timeout_seconds` | `30` | Cumulative wait limit while Pool capacity is exhausted |
| `sandbox_create_poll_interval_seconds` | `1.0` | Fallback status-check interval when no notification arrives |

The default fallback interval remains unchanged. Increasing it (for example to `5.0`) reduces repeated checks while nothing changes, but can delay detection when watch notifications are unavailable. Overall and Pool capacity deadlines still apply independently of this interval. Unavailable cache reads continue to fall back to the Kubernetes API.

## API Documentation

Once the server is running, interactive API docs are available at:

- **Swagger UI**: `http://localhost:8080/docs`
- **ReDoc**: `http://localhost:8080/redoc`

## Environment Variables

| Variable | Description |
|----------|-------------|
| `SANDBOX_CONFIG_PATH` | Override the config file path |
| `DOCKER_HOST` | Custom Docker daemon address |
| `OPENSANDBOX_INSECURE_SERVER` | Set to `YES` to run without API key in non-interactive mode |

## Related

- [Secure Container Runtime](/guides/secure-container) — gVisor, Kata, and Firecracker configuration
- [Credential Vault](/guides/credential-vault) — Secure credential injection
- [Network Isolation on Kubernetes](/guides/network-isolation) — Egress policy design
- [Kubernetes Deployment](/deployment/) — Kubernetes-specific setup

---

### Getting Started Installation (`docs/getting-started/installation.md`)

---
title: Installation
description: Install OpenSandbox server, SDKs, CLI, and MCP server across all supported languages and platforms.
---

# Installation

## Server

The OpenSandbox server is a FastAPI-based service that manages sandbox lifecycles. It supports Docker and Kubernetes runtimes.

::: code-group

```bash [pip]
pip install opensandbox-server
```

```bash [uv]
uv pip install opensandbox-server
```

:::

**Requirements:**
- Python 3.10+
- Docker Engine 20.10+ (Docker runtime) or Kubernetes 1.21.1+ (Kubernetes runtime)
- Linux, macOS, or Windows with WSL2

See [Configuration](/getting-started/configuration) for server setup.

## SDKs

### Sandbox SDKs

The core SDKs for sandbox lifecycle management, command execution, and file operations.

::: code-group

```bash [Python]
pip install opensandbox
```

```bash [JavaScript/TypeScript]
npm install @alibaba-group/opensandbox
```

```kotlin [Kotlin/Java (Gradle)]
dependencies {
    implementation(platform("com.alibaba.opensandbox:sandbox-bom:{latest_version}"))
    implementation("com.alibaba.opensandbox:sandbox")
}
```

```xml [Kotlin/Java (Maven)]
<dependency>
    <groupId>com.alibaba.opensandbox</groupId>
    <artifactId>sandbox</artifactId>
    <version>{latest_version}</version>
</dependency>
```

```bash [Go]
go get github.com/alibaba/OpenSandbox/sdks/sandbox/go
```

```bash [C#/.NET]
dotnet add package Alibaba.OpenSandbox
```

:::

For detailed SDK usage, see the [SDK documentation](/sdks/).

Choose a published SDK version for your application. Replace `{latest_version}` in the JVM examples with that version; use the BOM when combining JVM modules such as `sandbox` and `sandbox-pool-redis`. See the [release index](https://github.com/opensandbox-group/OpenSandbox/releases).

Feature availability differs by language. See the [capability matrix](/sdks/#capability-coverage), [Client Pool](/guides/client-pool), and [SDK Tracing](/sdks/observability#pool-warmup-tracing).

## CLI

The `osb` CLI provides terminal-based sandbox management.

::: code-group

```bash [pip]
pip install opensandbox-cli
```

```bash [uv]
uv tool install opensandbox-cli
```

:::

See the [CLI reference](/cli/) for the full command set.

## MCP Server

The MCP server exposes sandbox operations to MCP-capable clients like Claude Code and Cursor.

::: code-group

```bash [pip]
pip install opensandbox-mcp
```

```bash [uv]
uv pip install opensandbox-mcp
```

:::

```bash
opensandbox-mcp --domain localhost:8080 --protocol http
```

Minimal stdio config for MCP clients:

```json
{
  "mcpServers": {
    "opensandbox": {
      "command": "opensandbox-mcp",
      "args": ["--domain", "localhost:8080", "--protocol", "http"]
    }
  }
}
```

See the [MCP documentation](/sdks/mcp) for client-specific setup.

---

### Guides Auto Pause Wake (`docs/guides/auto-pause-wake.md`)

---
title: Auto-Pause & Wake-on-Access
description: Let idle Fast Sandbox sandboxes pause themselves after X seconds of inactivity and wake on the next request — opt-in via lifecycle.idlePolicy, with bounded request parking and SDK auto-retry.
---

# Auto-Pause & Wake-on-Access Guide

This guide shows how to enable and operate **auto-pause** and **wake-on-access** for Fast Sandbox (`fsb-`) sandboxes: a sandbox that opts in is paused automatically after **X** seconds without ingress traffic (checkpointed, memory and CPU released), and the next request to it **wakes** it — the gateway parks the request for up to **Y** seconds while the sandbox restores, then forwards normally instead of failing with a permanent `503`.

For the design rationale and internal mechanics, see [Auto-Pause and Wake-on-Access architecture](/architecture/network/auto-pause-wake-on-access).

## How it works in one minute

```text
active    every request through the ingress refreshes activity:{id} in Redis (async, coalesced)
idle      server sweeper (every 15s) checks gates → PauseSandbox → checkpoint → Paused
wake      next request → gateway resumes + parks the request ≤ Y → forwards when Ready
          (budget exhausted → 503 + Retry-After + OpenSandbox-Wake: expired; restore continues)
```

Three parties must agree before anything happens — the server, the ingress, and the sandbox itself. All three default to **off**, and a sandbox that never declares an `idlePolicy` keeps today's behavior exactly: never auto-paused, never auto-woken.

## Prerequisites

- **OpenSandbox full stack v1.2.0 or later.** The lifecycle server, ingress gateway, and SDKs must all be at v1.2.0+; earlier stacks do not accept `lifecycle.idlePolicy` or the wake-timeout contract.
- **Fast Sandbox backend only.** Docker and Kubernetes pod sandboxes are not covered (their manual pause/resume is separate — see [Pause & Resume](/guides/pause-resume)).
- **Traffic through the ingress gateway.** The ingress is the only activity source. Sandboxes reached through the server proxy path (`/sandboxes/{id}/proxy/{port}`, the SDK `useServerProxy` mode) neither record activity nor wake — do not opt those workloads in.
- **Redis reachable from both the ingress and the server** (the same deployment as renew-intents). Wake itself does not use Redis — a Redis outage can only delay pauses, never break wake.

## Step 1 — Enable the sweeper on the server

In the server's `config.toml`:

```toml
[automation.idle_pause]
enabled = true                    # master switch: enables the idle sweeper
sweep_interval_seconds = 15       # detection cadence
activity_ttl_seconds = 1800       # must stay ≥ the largest accepted X (see constraints)

[automation.idle_pause.redis]
dsn = "redis://127.0.0.1:6379/0"  # required when enabled = true
lock_ttl_seconds = 30
```

See [`[automation.idle_pause]` in the server configuration reference](https://github.com/opensandbox-group/OpenSandbox/blob/main/server/configuration.md) for all keys.

## Step 2 — Enable the ingress

```text
--activity-enabled=true           # record activity while proxying
--wake-enabled=true               # trigger resume + park requests
```

Relevant flags and defaults:

| Flag | Default | Purpose |
|---|---|---|
| `--activity-enabled` | `false` | Record per-sandbox activity in Redis |
| `--activity-redis-dsn` | `redis://127.0.0.1:6379/0` | Redis for activity writes |
| `--activity-min-interval` | `5s` | Per-sandbox write coalescing |
| `--activity-ttl-seconds` | `1800` | Key TTL — keep equal to the server's `activity_ttl_seconds` |
| `--wake-enabled` | `false` | Resume + request parking |
| `--wake-park-budget` | `5s` | Park budget **Y** (operator-owned latency policy) |
| `--wake-park-max` | `1024` | Parking-lot capacity (held requests) |
| `--wake-restore-max` | `256` | Concurrent restore permits (must be ≤ `--wake-park-max`) |
| `--wake-retry-interval` | `50ms` | First poll interval |
| `--wake-retry-factor` | `1.3` | Poll backoff factor |
| `--wake-retry-jitter` | `0.1` | Poll backoff jitter |
| `--wake-max-flight-lifetime` | `5m` | Cap on one flight's establish+poll phase |

## Step 3 — Opt in sandboxes via `lifecycle.idlePolicy`

Set the policy at create time:

```json
{
  "templateId": "tpl_2f0c...",
  "timeout": 86400,
  "lifecycle": {
    "idlePolicy": {
      "idleTimeoutSeconds": 300,
      "onIdle": "pause",
      "wakeOnAccess": true
    }
  }
}
```

Official SDKs expose typed parameters. Python (`create_from_template` is the fast-sandbox path — `idle_policy` rides alongside the template):

```python
from datetime import timedelta

from opensandbox.models import IdlePolicy
from opensandbox.sandbox import Sandbox

sandbox = await Sandbox.create_from_template(
    "tpl_2f0c...",
    timeout=timedelta(hours=24),
    idle_policy=IdlePolicy(
        idle_timeout_seconds=300,
        on_idle="pause",
        wake_on_access=True,
    ),
)
```

The policy is desired state: validated updates take effect on the next read, without re-creating the sandbox. It is stored as the `sandbox.opensandbox.io/idle-policy` Sandbox CR annotation and hidden from public metadata.

### `idlePolicy` reference

| Field | Type | Default | Meaning |
|---|---|---|---|
| `idleTimeoutSeconds` | int 30–86400 | — | Idle threshold **X**. Required together with `onIdle`. Create is rejected with `400` if `X > activity_ttl_seconds`. |
| `onIdle` | enum | — | Only `pause` today. The field exists so a future action (e.g. `terminate`) stays additive. |
| `wakeOnAccess` | bool | `true` | Within a declared policy, `false` opts out of wake: traffic never resumes the sandbox (permanent `503`), only `POST /sandboxes/{id}/resume` does. |
| `activeProbe` | object | — | App-defined activity probe — see below. **Auto-pause only:** requires `idleTimeoutSeconds` (a wake-only policy is never probed, so the create is rejected with `400`); all three sub-fields required when present. |

**Wake-only policy**: omit `idleTimeoutSeconds` and `onIdle`, keep `wakeOnAccess: true`. The sandbox is never auto-paused, but a (manually) paused sandbox wakes on traffic.

**Omission invariant**: no `lifecycle` or no `idlePolicy` ⇒ never auto-paused and never auto-woken, regardless of server/ingress flags.

### The activity probe

Ingress observation cannot see work that generates no inbound traffic — a request that started a 20-minute in-sandbox computation, a background poller. `activeProbe` lets the application veto such a pause (it is auto-pause only: it must be declared together with `idleTimeoutSeconds`):

```json
"activeProbe": {
  "port": 8080,
  "path": "/active",
  "timeoutSeconds": 2
}
```

When the sweeper is otherwise ready to pause, it issues `GET {path}` on `{port}` through the ingress gateway (sentinel-carried; it never resets the idle clock). Your endpoint answers:

```text
200 {"active": true}    → busy: counts as activity, pause dropped for this sweep (re-probed ~X later)
200 {"active": false}   → idle: pause proceeds; optional "message" lands in the pause audit log
anything else / timeout → no record, candidate dropped this sweep (fail-safe: only an explicit
                          "not active" lets a pause through)
```

::: tip
Use the probe when your workload has meaningful silent work. Without it, such a sandbox **will** be paused; the next request recovers state after restore, but the restore cost is real.
:::

## Wake behavior and SDK retry

A request arriving at a paused, opted-in, `wakeOnAccess: true` sandbox is parked while the gateway resumes it (same-node restore is typically tens of milliseconds; the wake P95 target is < 500 ms):

- **Ready within Y** → forwarded like any other request. For the client, the park is just added latency — no status or body bytes are written during the wait, so keep LB/Envoy idle timeouts and client response-header timeouts above Y.
- **Y exhausted** → the gateway answers:

```text
HTTP/1.1 503 Service Unavailable
Retry-After: 1
OpenSandbox-Wake: expired

{"error": {"code": "sandbox_wake_timeout",
           "message": "restore still in progress; request was never forwarded and is safe to retry",
           "sandboxId": "sb_..."}}
```

The `OpenSandbox-Wake: expired` header is gateway-owned (stripped from every upstream response), so its presence proves the request never reached the sandbox — that is what makes automatic retry safe for any method, including command/code-execution starts, provided the body is replayable. Official SDKs apply this contract to the marked response only:

- Wait per `Retry-After`, resend the original request. On by default, disable-able per client.
- Total wait bound: a wake-retry deadline (default 60 s) and a max-attempts cap (default 5) spanning all wake cycles; retries never reset the deadline.
- Success continues the original call; exhausted budget raises a typed wake-timeout error carrying the attempt count and waited time. A bare `503` (no marker) is never auto-retried for non-idempotent methods.

Python example:

```python
from datetime import timedelta

from opensandbox.config import ConnectionConfig
from opensandbox.transport import RetryPolicy

config = ConnectionConfig(
    retry_policy=RetryPolicy(
        wake_retry_enabled=False,             # disable auto wake-retry
        wake_overall_deadline=timedelta(seconds=120),
        wake_max_attempts=10,
    )
)
```

Exhaustion raises `SandboxWakeTimeoutException`.

## Composition with other features

- **Renew-on-access ([OSEP-0009](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0009-auto-renew-sandbox-on-ingress-access.md))** is independent — one request can feed both (see [Renew-on-Access](/guides/renew-on-access)). Recommended: enable both, so active sandboxes keep extending their expiry while idle ones get reclaimed.
- **Expiry is absolute**: pausing neither stops nor extends `expires_at`. A paused sandbox whose expiry passes becomes `Terminated`; wake then fails fast with `404`.
- **Manual pause/resume** keeps working. Wake treats manually and automatically paused sandboxes identically (unless `wakeOnAccess: false`).

## Constraints checklist

- `X ≤ activity_ttl_seconds` (server + ingress values must match); X floor is 30 s — lower values are rejected to keep the checkpoint cycle worth its cost.
- Everything in front of the ingress (LB idle timeouts, client response-header timeouts) must exceed **Y**, otherwise middle layers cut parked connections before the gateway can answer `503 + Retry-After`.
- Pause timing = X + at most one sweep interval of detection lag.
- Only new HTTP requests count: open WebSocket/SSE streams are not activity — frames (including WS ping/pong) are relayed without being recorded, so application-level WS pings cannot keep a sandbox alive. After X seconds without a fresh request the sandbox pauses and the connection breaks. Keep a background HTTP request cadence shorter than X, declare an `activeProbe`, or do not opt in.
- Scheduled workloads: a paused sandbox cannot wake itself. Arrange an external scheduler to call `POST /sandboxes/{id}/resume` before each run.
- Checkpoint cost is proportional to guest memory; every paused sandbox holds a full checkpoint in the artifact store until resumed or deleted. Size artifact-store capacity for peak concurrent paused sandboxes.

## Operating the feature

- **Rollout order**: deploy with all flags off → enable activity + sweeper for a canary tenant → enable wake on a canary ingress → opt in real workloads via `idlePolicy`.
- **Rollback**: disable the flags. Sandboxes left `Paused` stay resumable through the manual API — resume or delete stranded ones after disabling wake, since traffic will no longer wake them.
- **Key metrics** (see [Telemetry](/guides/telemetry)):
  - `opensandbox.server.idlep.pause.requests` (`outcome` = accepted/gated/failed) — pause pipeline health.
  - `opensandbox.server.idlep.probe.outcome` — sustained `error` ⇒ broken probe endpoint on an opted-in workload.
  - `ingress.activity.writes_dropped` — Redis pressure on the producer side.
  - `ingress.park.shed` — parking-lot overflows; sustained growth ⇒ raise `--wake-park-max` or investigate restore latency.
  - `ingress.park.wait.duration` and `ingress.wake.restore.active` — wake health; same-node wake P95 target < 500 ms.
  - Flap ratio (pauses followed by a quick wake — the primary tuning signal for X) is not emitted as an instrument yet: derive it per sandbox by joining `pause.requests{outcome="accepted"}` with subsequent wake flights. Sustained flapping means traffic period ≈ X — raise X above the workload period.

## Troubleshooting

| Symptom | Likely cause | What to do |
|---|---|---|
| Sandbox never pauses | No `idlePolicy` annotation, traffic through server-proxy mode, or Redis unreachable | Check the sandbox was created with `idlePolicy`; route through the ingress; check server logs for skipped sweeps |
| Sandbox pauses while working | Silent background work invisible to the ingress | Declare `activeProbe` and answer `{"active": true}` while busy |
| Paused-and-expired sandbox returns 404 | Absolute expiry passed while paused | Expected — pausing does not extend expiry; compose with renew-on-access |
| Clients see plain 503 (no marker) on a paused sandbox | `wakeOnAccess: false`, no declared policy, or `--wake-enabled=false` | Check the three activation parties |
| Wake latency above P95 target | Cross-node restore (artifact fetch + full materialization) | Size Y from the cluster's actual resume distribution; checkpoint-affinity scheduling is future work |
| Pauses/wakes flapping | Traffic period ≈ X | Raise X; track pauses followed by a quick wake per sandbox (the flap ratio is derived, not an emitted instrument — see [Monitoring](#operating-the-feature)) |

## Reference

- [Architecture: Auto-Pause and Wake-on-Access](/architecture/network/auto-pause-wake-on-access)
- [OSEP-0024](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0024-auto-pause-and-wake-on-access-for-fast-sandbox.md) — full proposal, alternatives, test plan
- [Fast Sandbox: Pause, Resume, and Snapshots](/architecture/fast-sandbox/checkpoints)

---

### Guides Client Pool (`docs/guides/client-pool.md`)

---
title: Client Pool
description: How the SDK-side sandbox pool works, how to configure it, and a minimal example for each supported SDK.
---

# Client Pool

The OpenSandbox SDKs ship an experimental **client-side sandbox pool** that keeps a small buffer of ready sandboxes warm on the server so that `acquire()` returns quickly instead of paying the full sandbox creation latency on the hot path.

Available in Python (async and sync), JavaScript/TypeScript, Kotlin/Java, and Go. C# does not currently ship a client pool.

::: warning Experimental
The client pool API is marked experimental and may change between minor releases. Pin your SDK version if you rely on it in production.
:::

## What it actually pools

The pool does **not** pool SDK `Sandbox` objects. It pools the **IDs of pre-warmed, ready sandboxes** running on the OpenSandbox server.

Use a pool for latency-sensitive workers that repeatedly need a fresh sandbox. The pool keeps remote sandboxes ready; it is separate from an HTTP connection pool. Borrowed sandboxes still consume server resources and must be explicitly cleaned up.



Two flows happen concurrently:

- **Warmup (leader-only).** A background reconcile loop runs on every node. Whichever node holds the primary lock computes the idle deficit and replenishes it. Python, JavaScript, and Kotlin use a nominal one-second cadence and admit at most `warmup_create_qps` new creates per completed tick. Kotlin schedules at a fixed rate; JavaScript skips a tick if the previous reconcile is still running; Python waits one second after the previous tick completes. Go uses a configurable `reconcile_interval` and caps each tick with `warmup_concurrency`. A successful warmup is published independently to the idle buffer with a TTL of `idle_timeout`.
- **Acquire (any node).** `acquire()` pops an idle ID from the store, connects a `Sandbox` client to it, optionally runs a health check and a `renew()` to the caller-supplied timeout, and hands it to the caller. Non-leader nodes can acquire freely; only replenish and shrink are gated by the leader lock.

Idle membership carries sandbox IDs and their expiry, never HTTP connections or client-side `Sandbox` objects. The state store also coordinates the leader lease, shared idle target and TTL, and namespace-destroy fence. A Redis-backed store makes that coordination and idle membership visible across processes and pods.

The warmup path — the leader-only replenish flow above — is worth zooming in on because it is the only part of the pool that is gated by a distributed lock:



### Lifecycle model

During graceful operation, each pool instance moves through `NOT_STARTED → STARTING → RUNNING → DRAINING → STOPPED`. A failed start or non-graceful shutdown can transition directly to `STOPPED`. If awaiting Python's `SandboxPoolAsync.start()` is cancelled, it waits for startup-owned resource cleanup, even if the caller cancels repeatedly during cleanup. Successful cleanup returns the pool to `STOPPED` before propagating `CancelledError`, so the same instance can then be started again. If cleanup itself fails after a further caller cancellation, `CancelledError` takes precedence and retains the cleanup error as its cause. Without a further cancellation, the cleanup error propagates normally. In either case the lifecycle state is `STOPPED`, allowing another `start()` attempt instead of silently returning in `STARTING`. Resource cleanup may still be incomplete when closing fails. Health is tracked separately as `HEALTHY | DEGRADED | DRAINING | STOPPED`; after `degraded_threshold` consecutive warmup or reconcile failures the pool enters `DEGRADED`. Go applies exponential replenish backoff while degraded. Python, JavaScript, and Kotlin continue attempting reconciliation at their nominal one-second cadence: `warmup_create_qps` is their pressure control, and `snapshot().backoff_active` / `snapshot().backoffActive` is retained only for compatibility and is always `false`. Callers do not need to observe these states directly — `snapshot()` exposes them for diagnostics.

Python, JavaScript, and Kotlin built-in warmup creates make a single lifecycle request. Python and Kotlin disable the connection-level retry policy for HTTP 429, other retryable statuses, and transport recovery; JavaScript's lifecycle transport does not add automatic retries. There is no pool-level `Retry-After` throttle. A custom `PooledSandboxCreator` receives the warmup connection configuration through `PooledSandboxCreateContext.connection_config` or `createConnectionConfig` and must honor it, together with `skipHealthCheck`, to preserve the staged behavior. A failed create is recorded and a later tick may admit replacement work. Normal standalone creation and `AcquirePolicy.DIRECT_CREATE` retain their usual transport behavior.



### There is no `release()`

Sandboxes are ephemeral. Once you have called `acquire()`, the sandbox is yours until you `destroy()` / `kill()` it. `max_idle` bounds the **warm buffer**, not the number of sandboxes borrowed by application code and not the number of sandboxes produced by `DIRECT_CREATE` fallback.

In Python's `SandboxPoolAsync`, ownership transfers when `acquire()` returns. If the call fails or is cancelled after connecting to or creating a sandbox, the pool attempts to kill that sandbox and closes its local resources before propagating the error. Cancellation during renewal or the final pool-state checks, including repeated cancellation during cleanup, does not skip that cleanup. Once acquisition succeeds, the caller remains responsible for disposal.

## Empty-buffer behavior: `AcquirePolicy`

All four pool SDKs expose these policies. Acquire consumes a candidate; it does not wait for future warmup work to fill an empty buffer.

| Policy | Idle candidates attempted | When candidates are exhausted |
| --- | --- | --- |
| `FAIL_FAST` | At most one | Return a pool-empty/acquire error |
| `DIRECT_CREATE` (default) | At most one | Create a fresh sandbox |
| `RETRY_NEXT_IDLE` | Up to `max_acquire_retries` | Return a pool-empty/acquire error |
| `RETRY_NEXT_IDLE_THEN_CREATE` | Up to `max_acquire_retries` | Create a fresh sandbox |

`max_acquire_retries` / `maxAcquireRetries` / `MaxAcquireRetries` defaults to `3`. It bounds the **total candidate attempts**, not three additional retries after the first. Each candidate can consume the acquire-readiness budget; direct creation adds its own startup latency.

`start()` begins background replenishment and does not wait for a full buffer. Use `DIRECT_CREATE` during startup, or observe the idle count before using `FAIL_FAST`. A pool is not a concurrency limit: direct creation and already borrowed sandboxes can exceed `max_idle`.

## Configuration

The SDKs share the pool concepts, but their scheduling surfaces differ. This table is the canonical reference; refer to the per-language builder or constructor for exact camelCase / snake_case naming.

| Parameter | Python / JavaScript / Kotlin default | Go default | Meaning |
| --- | --- | --- | --- |
| `pool_name` | required | required | Logical namespace shared by all nodes of one distributed pool |
| `owner_id` | auto (`pool-owner-<uuid>`) | auto (`pool-owner-<host/pid>`) | Identity of this process for primary-lock ownership; **must be unique per node** |
| `max_idle` | required (≥ 0) | required (≥ 0) | Target size and cap of the idle buffer |
| `state_store` | required in Python/Kotlin; JavaScript defaults to in-memory | builder defaults to in-memory | `InMemoryPoolStateStore` or Redis-backed store |
| `connection_config` | required | required | Used for lifecycle and execd calls |
| `creation_spec` | required in Python/Kotlin; required in JavaScript only without a creator | required only when `sandbox_creator` is unset | Template for warmed sandboxes; exact fields vary by language |
| `sandbox_creator` | `null` | `null` | Optional callback that overrides default creation. Python and Kotlin still require `creation_spec`; JavaScript and Go allow a creator-only pool. |
| `warmup_create_qps` | `10` | not available | Maximum warmup creates admitted on each completed reconcile tick |
| `warmup_concurrency` | `128` | `max(1, ceil(max_idle * 0.2))` | Python / JavaScript / Kotlin: concurrent post-create work; it does not control create admission. Go: create cap per tick and worker concurrency. |
| `primary_lock_ttl` | `60 s` | `60 s` | Leader lease TTL |
| `reconcile_interval` | nominal `1 s`, not exposed | `30 s`, configurable | Reconcile cadence; long Python/JavaScript ticks reduce the effective rate |
| `degraded_threshold` | `3` | `3` | Consecutive failures before `DEGRADED`; only Go pauses replenish with backoff |
| `acquire_ready_timeout` | `30 s` | `30 s` | Max wait for the returned sandbox to become ready |
| `acquire_health_check_polling_interval` | `200 ms` | `200 ms` | Ready-poll interval during acquire |
| `acquire_health_check` | `null` | `null` | Custom readiness predicate for acquire |
| `acquire_skip_health_check` | `false`; JavaScript also has a per-acquire override | per-acquire option | Skip the readiness check on acquire |
| `acquire_min_remaining_ttl` | `min(60 s, idle_timeout / 2)` | `min(60 s, idle_timeout / 2)` | Discard idles closer to expiry than this on acquire |
| `warmup_ready_timeout` | `30 s` | `30 s` | Max readiness-check window for a warmed sandbox |
| `warmup_health_check_initial_delay` | `0 s` | not available | Delay between successful create and the first readiness check |
| `warmup_health_check_polling_interval` | `500 ms` | `200 ms` | Ready-poll interval during warmup; Python / JavaScript / Kotlin also use it for post-prepare checks |
| `warmup_health_check` | `null` | `null` | Custom warmup readiness predicate |
| `warmup_sandbox_preparer` | `null` | `null` | Runs once after readiness and before publishing to the idle buffer |
| `warmup_post_prepare_health_check` | `null` | not available | Optional validation after the preparer; retries do not rerun the preparer |
| `warmup_post_prepare_health_check_timeout` | `30 s` | not available | Retry window for post-prepare validation |
| `warmup_skip_health_check` | `false` | `false` | Skip the pre-prepare readiness stage during warmup |
| `idle_timeout` | `24 h` | `24 h` | Server-side TTL for pool-created sandboxes |
| `drain_timeout` | `30 s` | `30 s` | Max wait for in-flight ops during graceful shutdown |

### JavaScript settings and creator coverage

JavaScript uses the Python/JVM scheduling defaults above: `warmupCreateQps: 10`, `warmupConcurrency: 128`, a nominal one-second reconcile cadence, 500 ms warmup polling, and 30-second readiness windows. Its store defaults to `InMemoryPoolStateStore`; `creationSpec` can be omitted when `sandboxCreator` is supplied. Time settings include units in their names, such as `idleTimeoutSeconds` and `warmupHealthCheckInitialDelayMillis`.

Default pool creators expose fewer options than standalone sandbox creation:

| Creation field | Python | JavaScript | Kotlin/Java | Go |
| --- | --- | --- | --- | --- |
| Snapshot restore | No | Yes | No | Yes |
| Resource configuration | Yes | Yes | Yes | Yes |
| Lifecycle hooks | No | Yes | No | No |
| Credential Proxy | Yes | Yes | Yes | Yes |

For Python pools, pass `credential_proxy=CredentialProxyConfig(enabled=True)`
in `PoolCreationSpec` alongside `network_policy`. Both `SandboxPoolSync` and
`SandboxPoolAsync` apply it during warmup and direct creation. Omitting the field
keeps the standalone creation default. See [Credential Vault](/guides/credential-vault)
for the required network policy and credential configuration.

All four standalone sandbox creation APIs accept these fields. Use a custom `sandbox_creator` / `sandboxCreator` when the default pool spec is insufficient, and honor the supplied connection config, readiness controls, and timeout.

JavaScript and Go also accept minimum remaining TTL and skip-health-check overrides on each acquire. Python and Kotlin expose these as pool-level configuration.

### Staged warmup

Staged warmup separates creation admission from post-create work:



1. On each reconcile tick, the leader admits at most `min(max_idle - idle - warming, warmup_create_qps)` creates. The lifecycle create request is attempted once and returns a client without running its normal inline readiness loop. The create execution limit is internal and resolves to `ceil(warmup_create_qps * 1.5)`, so slow creates cannot grow active create work without bound. A custom creator must honor the supplied warmup connection config and `skipHealthCheck` from its `PooledSandboxCreateContext`.
2. The created sandbox enters a delayed stage queue. The first readiness check runs after `warmup_health_check_initial_delay`; failures retry every `warmup_health_check_polling_interval` until `warmup_ready_timeout`, including one final check at the deadline.
3. `warmup_sandbox_preparer` runs once. If configured, `warmup_post_prepare_health_check` then retries at the same polling interval until `warmup_post_prepare_health_check_timeout`; retries never rerun the preparer.
4. A healthy sandbox is renewed and committed to the idle buffer. `warmup_concurrency` bounds active post-create work. Delays between unsuccessful readiness polls do not permanently occupy the entire capacity.

There is no Python, JavaScript, or Kotlin `reconcile_interval` setting and no replenish backoff. For Python/Kotlin configurations that used the older setting, remove `reconcile_interval=...` / `reconcileInterval(...)`, choose `warmup_create_qps` / `warmupCreateQps(...)` for create admission, and use `warmup_concurrency` / `warmupConcurrency(...)` only for post-create capacity.

`warmup_create_qps` is an admission ceiling, not a completion-rate guarantee. Create latency, readiness, preparation, state-store latency, event-loop or executor capacity, and a delayed/skipped reconcile tick can all reduce observed warmup throughput.

### Choosing a state store

- **`InMemoryPoolStateStore`** — single process only. Suitable for development, tests, and single-instance workers. Not process-wide for gunicorn/uvicorn workers, Celery, or Kubernetes replicas.
- **Redis-backed store** (`RedisPoolStateStore`, `AsyncRedisPoolStateStore`, `sandbox-pool-redis` on the JVM, `poolredis` in Go, `@alibaba-group/opensandbox/pool-redis` in JavaScript) — required for multi-process or multi-pod deployments. All nodes in one logical pool must share the same `pool_name` and Redis `key_prefix`, and each process must use a **unique** `owner_id`.



### Rules that apply to every deployment

- `max_idle` bounds the warm buffer only. It does not cap borrowed sandboxes or `DIRECT_CREATE` fallbacks.
- All nodes sharing one pool must use the same creation and warmup definition. If that definition changes, roll out under a **new** `pool_name` (or Redis `key_prefix`) and retire the old one (see "Retiring an old pool namespace" below). Do not attempt to refill a changed template into the same `pool_name`: `release_all_idle()` does not fence other nodes, does not lower `max_idle`, and does not stop any current leader (which may still be running the old code) from immediately re-publishing old-template sandbox IDs into the shared buffer during a rolling deploy.
- `resize(max_idle)` and `release_all_idle()` can be called from any node.
- Configure `primary_lock_ttl` above `warmup_ready_timeout` plus expected preparer time and operational headroom. The SDKs renew leadership independently and fence every commit, but a lost lease causes in-flight warmups to be discarded instead of published.

## Minimal usage

### Python (sync)

```python
from datetime import timedelta

from opensandbox import (
    AcquirePolicy,
    InMemoryPoolStateStore,
    PoolCreationSpec,
    SandboxPoolSync,
)
from opensandbox.config import ConnectionConfigSync

pool = SandboxPoolSync(
    pool_name="demo-pool",
    owner_id="worker-1",
    max_idle=2,
    state_store=InMemoryPoolStateStore(),
    connection_config=ConnectionConfigSync(domain="api.opensandbox.io"),
    creation_spec=PoolCreationSpec(image="ubuntu:22.04"),
    warmup_create_qps=10,
    warmup_concurrency=128,
)

pool.start()
try:
    sandbox = pool.acquire(
        sandbox_timeout=timedelta(minutes=30),
        policy=AcquirePolicy.DIRECT_CREATE,
    )
    try:
        result = sandbox.commands.run("echo pool-ok")
        print(result.logs.stdout[0].text)
    finally:
        sandbox.destroy()
finally:
    pool.shutdown(graceful=True)
```

### Python (asyncio)

`SandboxPoolAsync` has the same surface plus an `async with` context manager:

```python
from datetime import timedelta

from opensandbox import (
    AcquirePolicy,
    InMemoryAsyncPoolStateStore,
    PoolCreationSpec,
    SandboxPoolAsync,
)
from opensandbox.config import ConnectionConfig

async with SandboxPoolAsync(
    pool_name="demo-pool",
    owner_id="worker-1",
    max_idle=2,
    state_store=InMemoryAsyncPoolStateStore(),
    connection_config=ConnectionConfig(domain="api.opensandbox.io"),
    creation_spec=PoolCreationSpec(image="ubuntu:22.04"),
) as pool:
    sandbox = await pool.acquire(
        sandbox_timeout=timedelta(minutes=30),
        policy=AcquirePolicy.DIRECT_CREATE,
    )
    try:
        result = await sandbox.commands.run("echo pool-ok")
    finally:
        await sandbox.destroy()
```

### Kotlin / Java

```java
SandboxPool pool = SandboxPool.builder()
    .poolName("demo-pool")
    .ownerId("worker-1")
    .maxIdle(3)
    .stateStore(new InMemoryPoolStateStore())
    .connectionConfig(config)
    .creationSpec(PoolCreationSpec.builder()
        .image("ubuntu:22.04")
        .entrypoint(List.of("tail", "-f", "/dev/null"))
        .build())
    .warmupReadyTimeout(Duration.ofSeconds(45))
    .build();

pool.start();
try {
    Sandbox sb = pool.acquire(Duration.ofMinutes(10), AcquirePolicy.DIRECT_CREATE);
    try {
        sb.commands().run("echo pool-ok");
    } finally {
        try {
            sb.kill();
        } finally {
            sb.close();
        }
    }
} finally {
    pool.shutdown(true);
}
```

### Go

Inside a function returning `error`, with `ctx` and the usual `context`, `fmt`, `time`, and `opensandbox` imports:

```go
pool, err := opensandbox.NewSandboxPoolBuilder().
    PoolName("demo-pool").
    OwnerID("worker-1").
    MaxIdle(3).
    ConnectionConfig(opensandbox.ConnectionConfig{Domain: "api.opensandbox.io"}).
    CreationSpec(opensandbox.PoolCreationSpec{Image: "ubuntu:22.04"}).
    StateStore(opensandbox.NewInMemoryPoolStateStore()).
    Build()
if err != nil {
    return err
}
if err := pool.Start(ctx); err != nil {
    return err
}
defer pool.Shutdown(context.Background(), true)

policy := opensandbox.AcquirePolicyDirectCreate
sb, err := pool.Acquire(ctx, opensandbox.AcquireOptions{
    SandboxTimeout: 10 * time.Minute,
    Policy:         &policy,
})
if err != nil {
    return err
}
defer sb.Close()
defer sb.Kill(context.Background())

result, err := sb.RunCommand(ctx, "echo pool-ok", nil)
if err != nil {
    return err
}
fmt.Println(result.Logs.Stdout)
return nil
```

### JavaScript / TypeScript

```ts
import { AcquirePolicy, SandboxPool } from "@alibaba-group/opensandbox";

const pool = SandboxPool.create({
  poolName: "demo-pool",
  maxIdle: 2,
  connectionConfig: { domain: "localhost:8080", useServerProxy: true },
  creationSpec: { image: "python:3.12" },
  warmupCreateQps: 10,
  warmupConcurrency: 128,
});

await pool.start();
try {
  const sandbox = await pool.acquire({
    sandboxTimeoutSeconds: 600,
    policy: AcquirePolicy.DIRECT_CREATE,
  });
  try {
    const result = await sandbox.commands.run("echo pool-ok");
    console.log(result.logs.stdout);
  } finally {
    try {
      await sandbox.kill();
    } finally {
      await sandbox.close();
    }
  }
} finally {
  await pool.shutdown(true);
}
```

To share state across processes, install `redis`, connect it in the application, and pass this store as `stateStore`:

```ts
import { createClient } from "redis";
import { RedisPoolStateStore } from "@alibaba-group/opensandbox/pool-redis";

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const stateStore = new RedisPoolStateStore({
  client: redis,
  keyPrefix: "opensandbox:pool:prod",
});
// Pass stateStore to SandboxPool.create(...).
// After all pools/managers using it are shut down:
// await redis.quit();
```

The caller owns the Redis connection. Custom JavaScript preparers and health checks must bound their own work: cancellation can stop waiting for a callback without stopping the callback itself.

## Diagnostics and operations

Each pool SDK exposes these operations (names follow language conventions):

- `snapshot()` — pool phase, health, counters (idle size, in-flight warmups, consecutive failures, last error).
- `snapshot_idle_entries()` — the current idle sandbox IDs with expiry timestamps.
- `resize(max_idle)` — change the target buffer size at runtime. For business-hours sizing, see the [scheduled client pool example](/examples/client-pool-schedule), which calls `Resize` from an application-level controller.
- `release_all_idle()` — drain the currently visible idle buffer and best-effort kill each entry, without stopping the pool. Useful to force a fresh set of warmups after a transient upstream problem. It does **not** change `max_idle`, does **not** fence other nodes, and does **not** stop an active leader from immediately replenishing — so it is not a safe way to swap creation templates on the same `pool_name`. For that case, retire the whole namespace under a new `pool_name` (see below).

The existing cleanup methods retain their original execution behavior. For opt-in bounded parallel cleanup, use Python's `release_all_idle_parallel(max_workers=50)`, Kotlin's `releaseAllIdle(concurrency)`, JavaScript's `releaseAllIdle(concurrency)`, or Go's concrete `(*DefaultSandboxPool).ReleaseAllIdleParallel(ctx, maxWorkers)`. These methods validate a positive concurrency value and wait for every drained ID to receive a best-effort kill attempt. The Go method is intentionally outside the `SandboxPool` interface to preserve compatibility with third-party implementors.

### Tracing warmups

Python, JavaScript, and Kotlin/Java emit opt-in OpenTelemetry warmup spans. Set `enable_tracing=True` in Python or `enableTracing: true` / `.enableTracing(true)` in JavaScript/JVM connection configuration. The application supplies its OpenTelemetry provider and exporter. See [SDK Tracing](/sdks/observability#pool-warmup-tracing) for phase names and language-specific attributes.

Pool `shutdown` stops that local pool instance; it does not terminate sandboxes already handed to callers. To retire a shared namespace, use the manager below.

### Retiring an old pool namespace

Each supported pool SDK exposes a `SandboxPoolManager` with a `destroy` operation that applies the same `DESTROYING → DESTROYED` protocol:

1. Write a `DESTROYING` fence into the state store, so any still-running peer instance sees it and stops replenishing instead of racing the retirement.
2. Best-effort drain and kill every idle sandbox, bounded by the drain timeout.
3. Clear the persistent per-pool state.
4. Write a `DESTROYED` tombstone with the tombstone TTL (default 7 days) so future callers cannot silently rebind to the same `pool_name`.

Destroy is idempotent: calling it on an already-tombstoned namespace reports `DESTROYED` without draining or killing anything. If the drain or the cleanup cannot finish, the namespace stays `DESTROYING` and the call reports the destroy as incomplete; retrying is safe and picks up where it left off.

**Python / Kotlin** — `SandboxPoolManager.destroy(poolName, options)`, configured through `PoolDestroyOptions` (`strategy`, `drain_timeout`, `tombstone_ttl`).

**JavaScript** — `SandboxPoolManager.create({ stateStore, connectionConfig })`, then `await manager.destroy(poolName, options)`.

**Go** — `(*SandboxPoolManager).Destroy(ctx, poolName, options)`:

```go
manager, err := opensandbox.NewSandboxPoolManagerBuilder().
    StateStore(store).
    ConnectionConfig(connCfg).
    Build()
if err != nil {
    return err
}

result, err := manager.Destroy(ctx, "orders-v2", opensandbox.PoolDestroyOptions{})
if err != nil {
    return err
}
log.Printf("retired %s: drained=%d killed=%d",
    result.PoolName, result.DrainedIdleCount, result.KilledIdleCount)
```

`PoolDestroyOptions` mirrors the other SDKs. `Strategy` selects the algorithm and only `PoolDestroyForce` is implemented. `DrainTimeout` and `TombstoneTTL` are `*time.Duration`: leave them nil for the defaults (30s and 7 days), or set an explicit zero to drain without a deadline and to write a tombstone that never expires.

The fence is what makes retirement safe without stopping every writer first, and it is enforced on two levels. The state store refuses `PutIdle`, `SetMaxIdle` and `SetIdleEntryTTL` with a `*PoolDestroyedError` and hands out no primary lock, which stops replenishment. The pool itself also checks the fence when it starts, before every acquire, again once an acquire holds a live sandbox, and on each reconcile tick: a surviving peer stops outright on its next tick, an in-flight acquire fails rather than minting a fresh sandbox into the retired namespace through the direct-create fallthrough, and a sandbox obtained just before the fence landed is killed instead of handed out. The post-acquire check matters because the idle take is deliberately left unfenced so `destroy` can drain: once an ID has been taken, `destroy` can no longer reach it, so the acquire has to dispose of it itself. Starting a fresh pool against a tombstoned `PoolName` fails for the same reason, so rebinding the name requires either waiting out the tombstone TTL or rotating to a new `PoolName`.

One deliberate exception: if the state store itself is unreachable, the destroy state is unknowable, so policies that already fall through to direct create on a store outage (`DIRECT_CREATE`, `RETRY_NEXT_IDLE_THEN_CREATE`) assume `ACTIVE` and proceed, matching the existing `try_take_idle` outage behavior. `FAIL_FAST` and `RETRY_NEXT_IDLE` surface the outage instead. That relaxation stops at a sandbox already taken from the idle buffer: there the check is fail-closed and an unreachable store means the sandbox is killed, because nothing else is tracking it any more.

## Further reading

- Python: [`/sdks/python`](/sdks/python) &mdash; `SandboxPoolSync`, `SandboxPoolAsync`, Redis store.
- JavaScript: [SDK entry point](/sdks/javascript) — `SandboxPool` and the `/pool-redis` export.
- Kotlin: [`/sdks/kotlin`](/sdks/kotlin) &mdash; `SandboxPool` builder, `sandbox-pool-redis` module.
- Go: [`/sdks/go`](/sdks/go) &mdash; `SandboxPool` interface, `RedisPoolStateStore`, distributed deployment notes.

---

### Guides Credential Vault (`docs/guides/credential-vault.md`)

---
title: Credential Vault
description: Secure credential injection for sandbox outbound requests without exposing secrets to workloads.
---

# Credential Vault

Credential Vault is OpenSandbox's outbound credential broker for sandboxed agents and developer tools. Real credentials are written to the egress sidecar by the host-side SDK, while the sandbox process only receives fake or empty credential values. When tools such as Claude Code, Git, curl, package managers, or model API clients make allowed outbound HTTPS requests, the sidecar matches the request against Credential Vault bindings and injects the required authentication headers on the way out. This lets existing tools keep their normal workflows while keeping real secrets out of the sandbox environment, command line, filesystem, and logs, reducing credential exfiltration risk from prompt injection or untrusted code.

## Requirements

- `opensandbox-server` >= 0.2.0
- `egress` >= 1.1.1
- Python SDK >= 0.1.11
- JavaScript/TypeScript SDK >= 0.1.9
- Kotlin SDK >= 1.0.13
- Go SDK >= 1.0.3
- C# SDK >= 0.1.3
- Server config sets `[egress].image`.
- Server config sets `[egress].mode = "dns+nft"`. Credential Vault refuses to activate in DNS-only mode because direct-IP connections can bypass DNS policy.
- Sandbox create request includes an outbound network policy.
- The outbound network policy should use `defaultAction="deny"` and explicitly allow every host referenced by a credential binding. Default-allow remains temporarily supported for backward compatibility but emits a security warning.
- Sandbox create request enables Credential Proxy.
- Sandbox pods are not running with an additional transparent service-mesh sidecar (for example Istio/Envoy injection) in the same network namespace. Credential Vault currently assumes the OpenSandbox egress sidecar is the only transparent outbound interception layer in the pod.
- The sandbox image has the tools you want to run. For Claude Code, use an image with Node.js and npm, such as the OpenSandbox code-interpreter image.

::: warning Migration notice
Credential Proxy still requires server `[egress].mode = "dns+nft"`; deployments that cannot provide nft enforcement cannot safely enable credential injection. Default-allow policies remain accepted during the compatibility period, but emit a security warning and should migrate to `defaultAction="deny"` before enforcement is tightened in a future release.
:::

## How It Works



Credential Vault is implemented by the egress sidecar. A sandbox must be created with both an outbound `network_policy` / `networkPolicy` and Credential Proxy enabled. The lifecycle API field name is `credentialProxy.enabled`; SDKs expose that field using their language-specific naming conventions.

At a high level:

1. The lifecycle server attaches the egress sidecar to the sandbox.
2. The SDK writes credentials and bindings to the sidecar Credential Vault API.
3. The sandbox process runs with fake or empty credential environment variables.
4. When the sandbox makes an HTTPS request, transparent MITM in the sidecar inspects the request metadata.
5. If exactly one binding matches the request scheme, host, port, method, and path, the sidecar injects the configured auth header and scoped placeholder substitutions.
6. Secret values are redacted from vault responses and response headers.

When the active-vault check succeeds, requests that do not match any credential binding are forwarded unchanged. Credential path-safety checks apply only after a binding matches and the request would otherwise receive credentials. The active-vault check itself runs before binding selection for every intercepted flow, including requests to hosts that would not match a binding.

The active vault used by the MITM process is served over a local Unix domain socket inside the sidecar. The sandbox workload cannot fetch this active state over the normal server proxy path.

Each new flow conditionally reads that private endpoint with the cached opaque snapshot tag in `If-None-Match`. A `304 Not Modified` response reuses the same immutable in-process snapshot without rendering or transferring secret values; a `200 OK` response carries a fully validated replacement snapshot and a new opaque `ETag`. Successful create and patch responses are acknowledged only after the store has changed that tag, so the next flow immediately observes the new credentials without a cache-TTL delay.

The opaque tag is separate from the public vault revision. Deleting a vault and creating it again can reset the public revision to `1`, but the recreated vault receives a different tag and cannot reuse plaintext from the deleted snapshot. A `404 Not Found` from the private endpoint is the normal "no active vault" result: the MITM process clears its cached snapshot and continues without credential injection.

::: warning Runtime availability dependency
The private active-vault socket is a hard availability dependency for all traffic intercepted by Credential Proxy, not only traffic that ultimately matches a credential binding. A timeout, refused connection, unexpected EOF, `5xx` response, malformed snapshot, or invalid `ETag` clears the affected plaintext cache and fails the request closed before any upstream forwarding. Small requests whose bodies are safely buffered receive a local `503 Service Unavailable` response. Requests that are already streaming, may cross the streaming threshold, or have an unknown body length are terminated instead, because mitmproxy cannot safely synthesize a local response after streaming has started. Operators should monitor the sidecar and private socket as part of the egress data plane's availability.
:::

## Persistence Across Pause and Resume

::: warning In-memory state
Credential Vault entries are process-local memory in the egress sidecar; they are not part of the sandbox root filesystem or the `BatchSandbox` Pod template. Kubernetes pause deletes the Pod after snapshotting, so the fresh egress sidecar created by resume starts with an empty vault. Credential injection does not resume until a trusted client creates the credentials and bindings again.

Keep the original vault request or an equivalent secret-manager reference in a trusted control plane outside the sandbox. After the sandbox returns to `Running`, call the Credential Vault create API again before allowing work that depends on those credentials. Do not persist real credential values in sandbox metadata, environment variables, snapshots, or logs.

Docker pause/unpause retains the existing container processes, but any egress sidecar replacement or restart also creates a new in-memory vault and requires the same re-injection procedure.
:::

::: info Fast-sandbox profile: server-routed vault
For template-backed (fsb) sandboxes the vault is egress-held: the server pushes complete vault revisions to the egress vault over the FastPath handler relay and the relay response is the synchronous egress-validated ack. The same SDK entry points (`sandbox.credential_vault` / `credentialVault` / `CredentialVault`) work for template-backed sandboxes through the server-routed API — no sidecar endpoint is resolved. The vault is **not** carried at create time: create the sandbox first, then call the vault API.

Everything else is unchanged for this profile: values are write-only and stripped from every read, `PATCH` is atomic (composed server-side over the relay; a `412` means another writer won the revision — re-read and retry with the fresh `expectedRevision`), and the vault persists egress-locally (memory plus one plaintext v1 file per sandbox under a Pod-scoped `emptyDir`), so an egress container restart inside the live Pod recovers locally when the Fastlet replays the bindings, and a Kubernetes pause/resume carries the vault files in the snapshot bundle (export strictly before teardown, re-stamped import at restore). The vault volume is part of the credential trust surface (mount it with `medium: Memory` to keep plaintext off node disk). See [fast-sandbox networking](/architecture/fast-sandbox/networking#credential-delivery).
:::

## Service Mesh Compatibility

Credential Vault depends on the egress sidecar's transparent redirect and MITM path. If the sandbox pod is also injected with a transparent service-mesh sidecar such as Istio/Envoy, both layers will try to intercept outbound traffic in the same network namespace. OpenSandbox does not currently support that combination for Credential Vault.

Use one of these operator patterns instead:

- disable mesh sidecar injection for sandbox pods that need Credential Vault
- keep mesh injection enabled, but do not enable `credentialProxy` / Credential Vault for those pods
- move outbound policy and credential handling to a platform mechanism outside the sandbox pod if mesh injection is mandatory

For the underlying egress-sidecar limitation, see [Egress](/architecture/network/egress#service-mesh-compatibility).

Credential bindings are intentionally precise. A default-deny egress policy is required. Use a narrow path match, for example `/v1/*` for Anthropic API calls.

## Auth Types

Each binding uses an `auth` rule to describe how the referenced credential is rendered into the outbound request:

- `bearer`: injects `Authorization: Bearer <credential>`.
- `basic`: injects `Authorization: Basic <credential>`. The credential value must already be base64-encoded `username:password`.
- `apiKey`: injects the credential value into the configured header name.
- `customHeaders`: injects multiple configured headers, each backed by its own credential.
- `passthrough`: does not inject an auth header. Use it with `substitutions` when the upstream API requires a credential in a path, query string, or body placeholder instead of a header.

Simple examples:

```python
auth={"type": "bearer", "credential": "github-token"}
```

```http
Authorization: Bearer <github-token>
```

```python
auth={"type": "basic", "credential": "registry-basic"}
```

```http
Authorization: Basic <base64(username:password)>
```

```python
auth={"type": "apiKey", "name": "x-api-key", "credential": "anthropic-api-key"}
```

```http
x-api-key: <anthropic-api-key>
```

```python
auth={
    "type": "customHeaders",
    "headers": [
        {"name": "X-Client-Id", "credential": "client-id"},
        {"name": "X-Client-Secret", "credential": "client-secret"},
    ],
}
```

```http
X-Client-Id: <client-id>
X-Client-Secret: <client-secret>
```

### Scoped Placeholder Substitutions

Some upstream APIs require credentials in a request URL or body instead of a dedicated auth header. Credential Vault can handle those APIs without placing the real credential in the sandbox process. All auth types accept an optional `substitutions` list. Each substitution names a credential, a literal placeholder, and the request surfaces where replacement is allowed:

```python
auth={
    "type": "passthrough",
    "substitutions": [
        {
            "credential": "client-secret",
            "placeholder": "__client_secret__",
            "in": ["body", "query"],
        }
    ],
}
```

Use `type="passthrough"` when the binding only performs substitutions and should not inject an auth header. You can also combine substitutions with `bearer`, `basic`, `apiKey`, or `customHeaders` when the same upstream request needs both header injection and placeholder replacement.

Substitution is disabled by default and is exact, literal, and case-sensitive. Only the configured surfaces are rewritten:

| Surface | Behavior |
| --- | --- |
| `path` | Replaces placeholders in the request path and URL-encodes the credential value. If the rewritten path contains ambiguous path segments, encoded separators, or traversal-like content, the sidecar rejects the request instead of forwarding a secret-bearing URL outside the matched scope. |
| `query` | Replaces placeholders in the query string and URL-encodes the credential value. |
| `header` | Replaces placeholders in the original request headers, excluding hop-by-hop and security-sensitive headers such as `Host`, `Content-Length`, and forwarding headers. Substitutions run before Credential Vault injects auth headers, so the sidecar does not rewrite the credential headers it creates. |
| `body` | Replaces placeholders in UTF-8 request bodies. For `application/json`, the replacement is encoded as JSON string contents, so put the placeholder inside a quoted JSON string. For `application/x-www-form-urlencoded`, the replacement is form-encoded. Compressed and multipart bodies are skipped. |

The sidecar applies all replacements for a surface against the original request text in one pass. Inserted credential values are not scanned again for later placeholders, which prevents one secret from accidentally rewriting another secret. When a body is rewritten, the sidecar updates `Content-Length` and removes `Transfer-Encoding` because the forwarded request now has a fixed-size buffered body.

The placeholder, raw credential value, URL-encoded value, form-encoded value, and JSON-escaped values are added to the active redaction set. A binding with substitutions that matched the request but did not find any placeholder emits a substitution-miss log without exposing credential values.

Example configuration:

```python
await sandbox.credential_vault.create(
    credentials=[
        Credential(name="tenant-id", source={"value": "tenant 42"}),
        Credential(name="api-key", source={"value": "query secret+value"}),
        Credential(name="client-secret", source={"value": 'body "secret" value'}),
    ],
    bindings=[
        CredentialBinding(
            name="token-request",
            match={
                "schemes": ["https"],
                "hosts": ["api.example.com"],
                "methods": ["POST"],
                "paths": ["/tenants/__tenant_id__/token"],
            },
            auth={
                "type": "passthrough",
                "substitutions": [
                    {
                        "credential": "tenant-id",
                        "placeholder": "__tenant_id__",
                        "in": ["path"],
                    },
                    {
                        "credential": "api-key",
                        "placeholder": "__api_key__",
                        "in": ["query"],
                    },
                    {
                        "credential": "client-secret",
                        "placeholder": "__client_secret__",
                        "in": ["body"],
                    },
                ],
            },
        )
    ],
)
```

The sandbox can use placeholders instead of real secrets:

```bash
curl -X POST \
  "https://api.example.com/tenants/__tenant_id__/token?api_key=__api_key__" \
  -H "content-type: application/json" \
  --data '{"client_secret":"__client_secret__"}'
```

The upstream receives the rewritten request:

```http
POST /tenants/tenant%2042/token?api_key=query%20secret%2Bvalue HTTP/1.1
content-type: application/json

{"client_secret":"body \"secret\" value"}
```

## Egress Sidecar Configuration

| Environment variable | Default | Description |
| --- | --- | --- |
| `OPENSANDBOX_EGRESS_CREDENTIAL_VAULT_REQUIRE_TLS` | off | When enabled (`true`/`1`/`on`), credential vault write operations (create, patch, delete) require TLS, loopback transport, or `X-Forwarded-Proto: https` from a configured trusted proxy. When disabled (default), any authenticated request is accepted regardless of transport. Enable this in deployments where the egress sidecar is directly reachable from untrusted networks without a TLS-terminating reverse proxy. |
| `OPENSANDBOX_EGRESS_CREDENTIAL_VAULT_TRUSTED_PROXY_CIDRS` | empty | Comma-separated IP addresses or CIDRs allowed to assert `X-Forwarded-Proto: https`. Forwarded transport headers from all other peers are ignored. Configure this when TLS terminates at a reverse proxy before the egress sidecar. |
| `OPENSANDBOX_EGRESS_CREDENTIAL_VAULT_REQUIRE_SCOPED_MATCH` | off | When enabled, every credential binding must explicitly provide non-empty `match.methods` and `match.paths`, and paths cannot contain the host-wide `/*` pattern. Vault create and patch requests that omit either field are rejected instead of receiving compatibility defaults. Enable this for control planes that translate higher-level API tool definitions into Credential Vault bindings. |

The scoped-match setting is a fail-closed integration guard. It does not infer methods or paths from another platform's API tool definition. The caller that creates the vault must still copy the intended methods and paths into each binding and verify the sanitized binding readback.


## SDK Quick Reference

All sandbox SDKs use the same wire contract. The main differences are naming and language style:

| SDK | Enable proxy on sandbox create | Vault entry point | Create / patch methods |
| --- | --- | --- | --- |
| Python | `credential_proxy=CredentialProxyConfig(enabled=True)` | `sandbox.credential_vault` | `create(...)`, `patch(...)` |
| Go | `CredentialProxy: &opensandbox.CredentialProxyConfig{Enabled: true}` | `sandbox.CredentialVault(ctx)` or sandbox helpers | `CreateCredentialVault(ctx, req)`, `PatchCredentialVault(ctx, req)` |
| JavaScript/TypeScript | `credentialProxy: { enabled: true }` | `sandbox.credentialVault` | `create(request)`, `patch(request)` |
| Kotlin/JVM | `.credentialProxyEnabled(true)` or `.credentialProxy { enabled(true) }` | `sandbox.credentialVault()` | `create(request)`, `patch(request)` |
| C#/.NET | `CredentialProxy = new CredentialProxyConfig { Enabled = true }` | `sandbox.CredentialVault` or sandbox helpers | `CreateCredentialVaultAsync(...)`, `PatchCredentialVaultAsync(...)` |

The vault APIs return sanitized metadata. Plaintext credential values are write-only and are not returned by `get`, `list`, or patch responses.

The same entry points and methods work for template-backed (fsb) sandboxes: the SDK routes the vault through the lifecycle server, which relays revisions to the egress-held vault. No sidecar endpoint is resolved for them, and the `credentialProxy` create-time flag is ignored on that profile (the vault is set after creation, through the API).

## Claude Code With Anthropic

This example installs Claude Code in the sandbox and calls the official Anthropic API endpoint. The real API key is read on the host and written to Credential Vault. The sandbox only sees a fake `ANTHROPIC_API_KEY`.

Before running the script:

```bash
export ANTHROPIC_API_KEY="sk-ant-..."
# Optional: export ANTHROPIC_MODEL="<a Claude Code supported Anthropic model>"
```

Run:

```
/* Detailed source-code truncated for AI context efficiency. */
```

The Claude Code process reads the fake key from `ANTHROPIC_API_KEY`, but the outbound HTTPS request to `api.anthropic.com/v1/*` receives the real `x-api-key` header from Credential Vault. If your environment uses a private npm mirror, replace `registry.npmjs.org` in the network policy and the `npm install` command with that mirror host.

## Git And Curl With Vault-Injected Credentials

Credential Vault can also protect credentials used by command-line tools such as `git` and `curl`. Keep the command free of real secrets and bind the request shape to the credential in Vault instead.

For a private Git repository, store a base64-encoded `username:token` value and bind it with `basic` auth:

```python
Credential(name="git-basic", source={"value": "<base64(username:token)>"})

CredentialBinding(
    name="git-basic",
    match={
        "schemes": ["https"],
        "hosts": ["git.example.com"],
        "paths": ["/org/private-repo.git*"],
    },
    auth={"type": "basic", "credential": "git-basic"},
)
```

Then run the normal URL without embedding credentials:

```bash
GIT_TERMINAL_PROMPT=0 git clone https://git.example.com/org/private-repo.git
```

For an API request that expects a token header, bind the path and method to an `apiKey` auth rule:

```python
Credential(name="api-token", source={"value": "<token>"})

CredentialBinding(
    name="api-token",
    match={
        "schemes": ["https"],
        "hosts": ["api.example.com"],
        "methods": ["GET"],
        "paths": ["/v1/projects/123/variables"],
    },
    auth={"type": "apiKey", "name": "PRIVATE-TOKEN", "credential": "api-token"},
)
```

The sandbox command stays secret-free:

```bash
curl -fsS https://api.example.com/v1/projects/123/variables
```

## Binding Guidance

- Use `defaultAction="deny"` and only allow the service hosts required by the tool. Default-allow policies are deprecated because they may allow credential destination bypass and will emit a security warning.
- Scope bindings by path whenever possible, for example `/v1/*`.
- Avoid overlapping bindings at the same precedence; ambiguous matches are rejected.
- Rejected requests (ambiguous paths, encoded separators crossing a binding boundary) return `403` when the request body is small and fully known. Requests with bodies above the mitmproxy streaming threshold (~1 MiB) or with unknown length (chunked) cannot be answered with a `403` while the body is being streamed, so the sidecar drops the connection instead — either way the request is never forwarded upstream.
- Do not put real secrets in sandbox `env`, command arguments, files, or metadata.
- Keep fake environment variables when a CLI refuses to start without a key; the vault-injected header is what authenticates the outbound request.

## Path Ambiguity Guard

The sidecar inspects the request path only for requests that would receive injected credentials. A request is rejected with `403` when its path contains dot-segments (`..`) or backslashes (`\`, `%5c`) at any percent-decoding depth, or when percent-decoding does not converge within the guard's iteration bound — these are traversal or path-confusion primitives that legitimate clients do not send.

Encoded slashes (`%2f`, including nested forms like `%252f`) are allowed when every decoding depth of the path matches the same credential binding. This keeps legitimate encoded wire formats working — npm scoped registry paths (`/@scope%2fname`) and artifact-store download URLs whose coordinates are double-encoded (for example `pkg%252F1.0`, as produced by some internal pypi proxies) — while a path whose decoding would cross a binding boundary is still rejected before any credential is injected. Requests outside every binding scope pass through untouched.

## Migrating From `ports`

The `match.ports` field is deprecated. Port is now derived from scheme (`https`→443, `http`→80). Only ports 80 and 443 are supported; non-standard values are rejected with a validation error.

If you have existing bindings that use `ports` to narrow scope, migrate them to the equivalent `schemes` restriction:

| Before | After |
|--------|-------|
| `schemes: ["http", "https"], ports: [443]` | `schemes: ["https"]` |
| `schemes: ["http", "https"], ports: [80]` | `schemes: ["http"]` |
| `schemes: ["https"], ports: [443]` | `schemes: ["https"]` (remove `ports`) |

## Experimental Live Credential-Bound Admission (OSEP-0023)

::: warning Experimental boundary
The live credential-bound loop is an internal sidecar experiment, not a supported deployment mode. It is documented here to bound what the current implementation actually does; the gate remains off by default and no public configuration surface is added.
:::

When the experimental revision runtime is enabled, the sidecar installs an immutable decision snapshot into the mitmproxy child over a private authenticated IPC channel and admits traffic per connection and per request:

- **Dynamic decisions use the installed snapshot.** Unbound HTTPS passes through opaquely; bound hosts are decrypted only with a registry admission. Unknown identity denies. For a known identity, ECH, no-SNI and validated static TLS/443 bypass retain their early precedence, including during bootstrap. Other unknown, bootstrapping or mismatched state denies.
- **Static TLS/443 bypass is operator-owned.** Set `OPENSANDBOX_EGRESS_MITMPROXY_PASSTHROUGH_HOSTS` to a JSON array of exact FQDNs or leftmost `*.` selectors, for example `["status.example.com","*.logs.example.com"]`. A wildcard covers all proper subdomains, including multiple labels, but not its apex. Go normalizes IDNA, case, surrounding whitespace and one root dot, then freezes a sorted unique list. Unset and `[]` mean empty; explicit empty text is an error. JSON input and canonical output each have a 64 KiB limit; at most 256 input entries are allowed before deduplication. IPs, URLs, ports and regexes are rejected.
- **Migrate native bypass options before enabling live admission.** Both effective mitmproxy `ignore_hosts` and `allow_hosts` must remain empty. Static selectors run in the typed TLS hook at actual destination port 443, not in native regex options. HTTP/80 behavior and network policy are unchanged. Non-empty selectors cannot be combined with a chained upstream proxy. Explicit use of this variable outside live mode is rejected. Legacy regex behavior remains unchanged when the variable is unset. Existing `tcp_hosts` may still bypass HTTP credential handling; this change does not establish a general addon/options isolation boundary.
- **TLS overlap is rejected before publication.** Any semantic intersection between a static selector and an HTTPS/443 binding rejects the complete Vault or Policy candidate before IPC prepare, policy persistence or nft application. Methods and paths do not exempt an overlap. HTTP-only bindings remain eligible. The child independently rejects overlap before staging. The list cannot be changed by a Vault update and adds no snapshot fields; changing it requires replacing the sidecar process.
- **Sticky recovery, no same-sidecar restart.** Any terminal outcome with unknown external effects (a mutation whose remote result cannot be confirmed, a session cleanup failure, an unexpected child exit) latches a permanent recovery-required state: readiness drops, outstanding bootstrap tickets are invalidated, nft writes are quiesced, and quarantine is attempted. Packet isolation is established only when quarantine is confirmed. The same sidecar cannot bootstrap a fresh revision session afterwards; a whole-sidecar restart is not a verified safe recovery procedure.
- **Request admission and cutover.** A vault mutation is applied to the live receiver first and published publicly only after exact confirmation. A rendered snapshot exceeding the IPC size limit is rejected with HTTP 400 before transmission; the current vault and sidecar remain available. In-flight requests finish on their admitted revision; new requests on a connection whose host was removed are fenced immediately: HTTP/1 receives 403; valid HTTP/2 requests refused at `requestheaders` receive `REFUSED_STREAM` without reaching the origin. The retired transport is force-closed at the drain deadline. Deleting the vault affects new requests and new connections; a retired connection can never be revived by re-adding the host. Already-admitted requests may finish only inside the bounded drain window — deletion does not recall requests the remote origin already received or revoke credentials already delivered remotely.
- **Policy updates share the revision barrier.** Existing `POST`/`PUT`/`PATCH`/`DELETE /policy` operations and automatic `deny.always` / `allow.always` reloads validate the complete Policy/Vault pair. A policy that would deny any active binding is rejected before file, nft or receiver effects. A policy-only update preserves the public Vault revision, pinned credential bytes and bindings; it does not reread credential sources. Its commit order is optional user-policy file persistence, nft application, exact receiver confirmation, then Go proxy/base publication and API success. Reload skips user-policy persistence and publishes the accepted loader view only after receiver confirmation. Subsequent Vault writes and bootstraps use the committed internal Policy epoch rather than resetting it to zero.
- **Policy visibility and failure.** Disk, nft and receiver updates are not instantaneously atomic: new nft rules may affect traffic before receiver confirmation. A receiver failure after successful nft application requires sticky recovery, quarantine and exact-child cleanup, even if the receiver definitively rejected the candidate. Replaying old nft rules or restoring the file cannot prove that new connections or requests were undone. Unavailable or unconfirmed revision updates and recovery-blocked writes return a fixed, sanitized 503; the initial file/nft error may still be 500. Validation failures before effects do not themselves require recovery.
- **Policy no-ops and retries.** A DELETE with no matching target and an unchanged always-rule reload do not advance the internal Policy epoch, but cannot bypass recovery checks. Other committed writes, including equal content or a loader-detected rewrite, can advance the epoch and initiate another drain. No public epoch, Policy idempotency key or new SDK method is added. `GET /policy` reports the last Go-published user policy, not an in-progress transaction, always-rule overlay or packet-isolation status.
- **Drain accounting.** Retired transports carry a bounded retirement deadline (default 30 seconds, internal range 1–300). The drain sweep pages expired transports and closes them; a failed close is retried on the next sweep rather than extending the deadline. An HTTP 101 protocol upgrade (including WebSocket) retains its request admission until the connection disconnects, so credential rotation and policy-only changes also drain an upgraded transport even when its host remains bound. API success waits for publication and retirement-deadline registration, not for the 30-second window to elapse. Repeated updates never extend an older request's earliest deadline. A still-covered connection may admit new-revision requests, but closing it for an expired old request also interrupts those newer requests. A still-covered idle connection with no old request is not retired solely because the Policy epoch changes. Sweep scheduling and failed-close retries can delay actual closure; this is not a deadline for opaque or direct-IP connections outside the registry. HTTP/2 normal retirement delays the single `NO_ERROR` GOAWAY with its frozen admitted-stream upper bound until old streams and their downstream/upstream output obligations are terminal. Deadline expiry is abnormal closure, not graceful completion.
- **ECH is opaque pass-through.** A ClientHello carrying the `encrypted_client_hello` extension (0xfe0d), including GREASE forms, is forwarded untouched as an opaque tunnel without decryption; uninspectable ClientHello data denies. The implementation does not decrypt the inner ClientHello and does not guarantee that an ECH-enabled origin handshake succeeds.
- **HTTP/1 authority gate.** On a decrypted HTTP/1 connection every request must carry exactly one syntactically strict `Host` header equal to the intercepted SNI and the admission binding (port omitted or 443). A conflicting absolute-form target, a CONNECT authority-form inside the stream and missing/duplicate/malformed Host values are rejected before any credential can be injected.
- **HTTP/2 authority gate.** The fixed-version direct/lazy adapter validates original HTTPS pseudo-headers, authority, actual port 443 and SNI association before credential injection; invalid or missing identity fails closed.
- **`ssl_insecure` is refused in live mode.** The launcher and the addon reject an insecure-upstream configuration when the live admission bundle is active; the legacy installation-only receiver keeps its existing behavior.
- **Scope.** This mode is scoped to the internal sidecar profile: Docker DNS + nft enforcement, exact-host HTTPS on port 443, HTTP/1.1 origin-form or matching absolute-form requests, and inline credentials injected as headers. Experimental HTTP/2 admission and bounded drain wiring additionally cover only the fixed-dependency direct/lazy path; configured chained upstream proxies remain rejected for this live HTTP/2 path. Long-held responses can still cause Go retries to time out; full client migration and Linux acceptance remain follow-up work. See the [HTTP/2 integration boundary](/architecture/network/osep-0023-h2-integration). The fast-sandbox profile and recovery across a complete sidecar replacement remain out of scope. Dynamic DNS learning and Kubernetes deployments are not covered by this experiment's evidence. Policy participation is part of this implementation; its acceptance evidence is recorded separately from the earlier live-Vault checks below.

The task-level acceptance record for this experiment lives in [Live Vault Acceptance](/guides/live-vault-acceptance).

---

### Guides Egress Sse Truncation (`docs/guides/egress-sse-truncation.md`)

---
title: Egress Mitmproxy SSE Truncation
description: Why large SSE bodies can be truncated when an HTTP/1.1 upstream closes its connection with a TCP RST, how to confirm it, and how to fix it on the gateway side.
---

# Egress Mitmproxy: Large SSE Chunks Truncated at the Tail

## Symptom

Sandboxes calling an LLM API through the egress sidecar sometimes receive a **truncated SSE stream**: a `data:` event larger than ~1 MB is cut off near the end, and the SDK reports malformed JSON or a dropped connection. The lost amount varies (a few bytes to hundreds of KB).

## Root cause

Byte-level instrumentation of the full data path shows this is **standard TCP RST semantics**, not a mitmproxy or OpenSandbox bug:

1. The upstream closes the connection while data is still unread in its receive buffer (e.g. a gateway that streams a response without consuming the client's request body). The kernel sends **TCP RST** instead of a clean FIN.
2. Per [RFC 793](https://www.rfc-editor.org/rfc/rfc793), a RST discards the receiver's unread kernel receive buffer — the unread tail of the response is lost before any application code sees it.
3. mitmproxy's h11 parser then hard-errors (`incomplete chunked read`), the flow is killed and the client connection closed.

Confirmed by a controlled A/B/C experiment (same server code, payload, proxy and client; only the close behavior differs):

| Close behavior | mitmproxy TCP signature | Client result |
|---|---|---|
| A. Unread request data on close → kernel RST | `ConnectionResetError` | Truncated, 5/5 |
| B. Request consumed, close → clean FIN | clean EOF | Complete, 5/5 |
| C. Request consumed + `SO_LINGER=0` forced RST (empty receive buffer) | `ConnectionResetError` | Truncated, 4/5 |

Cell C is the smoking gun: **RST alone reproduces the truncation even with no unread data; FIN never does**. A direct slow-reading client loses the whole body against server A and nothing against server B, so the loss is purely a function of how much of the tail is still unread when the RST lands. HTTP/2 upstreams and keep-alive connections are unaffected.

## Reproduce

Scripts in [`components/egress/tests/mitm-sse-truncation/`](https://github.com/opensandbox-group/OpenSandbox/blob/main/components/egress/tests/mitm-sse-truncation/run.sh) drive a local `mitmdump` (same options as egress transparent mode) against a synthetic RST-closing SSE upstream:

```bash
cd components/egress
./tests/mitm-sse-truncation/run.sh            # reproduce the truncation (TLS, 4 MiB, RST close)
./tests/mitm-sse-truncation/run.sh --plain     # controls that must pass
./tests/mitm-sse-truncation/run.sh --delay-close 1
./tests/mitm-sse-truncation/run.sh --read-request
./tests/mitm-sse-truncation/run.sh --probe --keep-workdir   # show the error hook in the mitmdump log
```

Requires `python3`, `openssl`, `mitmdump` (pass `--mitmdump PATH` if not on `PATH`). Options: `--size`, `--iterations`, `--delay-close`, `--plain`, `--read-request`, `--probe`, `--mitmdump`, `--port`, `--upstream-port`, `--workdir`, `--keep-workdir`.

## Fix (gateway side)

There is no code fix in mitmproxy or OpenSandbox — the bytes are gone from the kernel. The fix belongs to the upstream server.

**Confirm it is a RST first** (run on/in front of the gateway while reproducing):

```bash
tcpdump -ni any 'tcp port 443 and tcp[tcpflags] & (tcp-rst) != 0'   # RST right after the last response byte, no FIN
netstat -s | grep -i reset
# from a client: "Recv failure: Connection reset by peer" / curl: (56) instead of a clean EOF
```

**Then fix the close behavior** — the rule: consume the request before closing; for SSE, prefer not closing at all:

- **nginx**: keep `lingering_close on` (default) with `lingering_time 30s` / `lingering_timeout 5s`; check this especially if `proxy_request_buffering off` was set.
- **FastAPI / uvicorn / Starlette**: read the request body before streaming (`await request.body()`), otherwise a streaming response followed by close can leave the body unread → RST.
- **Go net/http**: drain the body explicitly (`io.Copy(io.Discard, r.Body)`); Go does not drain for you. Never use `SO_LINGER` 0 (`SetLinger(0)`) on streaming responses.
- **Generic**: prefer keep-alive (end chunked bodies with `0\r\n\r\n`); if you must close, drain the receive buffer first; HTTP/2 connections avoid this whole class of failure.

If the gateway cannot be changed: route the sandbox to the gateway over HTTP/2, or front it with a proxy that consumes the request body and forwards keep-alive.

## Status

- mitmproxy upstream: [mitmproxy/mitmproxy#8364](https://github.com/mitmproxy/mitmproxy/issues/8364) — closed as not-a-bug (TCP RST semantics).
- OpenSandbox tracking: [opensandbox-group/OpenSandbox#1461](https://github.com/opensandbox-group/OpenSandbox/issues/1461) — awaiting field confirmation.

A bigger egress-side read buffer was considered and measured: it shrinks the loss window at burst rate but never changes the user-visible outcome (the trailing SSE event is lost either way), so no patch is shipped.

---

### Guides Egress Upstream Proxy (`docs/guides/egress-upstream-proxy.md`)

---
title: Egress Chained Upstream Proxy Operations
description: Configure, operate, verify, and troubleshoot chained upstream HTTP CONNECT proxies in OpenSandbox.
---

# Egress Chained Upstream Proxy Operations

OpenSandbox provides an administrator-managed chained upstream proxy capability for sandboxes running with egress network policies. This guide explains how to configure, operate, verify, and roll back an upstream forward proxy next hop across Docker and Kubernetes environments.

## Overview and prerequisites

The chained upstream proxy routes egress traffic through an external HTTP/HTTPS forward proxy via standard HTTP CONNECT tunnels. The configuration is lifecycle-server-wide and administrator-only; request environments cannot override proxy destinations, credentials, or custom CA bundles.

Prerequisites:
- Egress sidecar enabled by provisioning sandboxes with a `networkPolicy`.
- Server `[egress]` configuration set to `mode = "dns+nft"`.
- Transparent interception enabled via `credentialProxy.enabled=true` or `OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT=true`.
- For Docker: `[docker] network_mode = "bridge"`.
- For Kubernetes: non-pooled AgentSandbox or BatchSandbox workloads.

::: warning
Transparent service-mesh sidecars (such as Envoy or Istio) injected into the same pod network namespace are unsupported because both layers intercept and rewrite outbound packets. Use the external chained upstream proxy hop instead.
:::

## Docker and Kubernetes configuration examples

Configure the `[egress.upstream_proxy]` section in the lifecycle server configuration. For the complete configuration schema and field definitions, refer to the [Server Configuration Reference](https://github.com/opensandbox-group/OpenSandbox/blob/main/server/configuration.md#egress).

### Docker configuration

For a private proxy CA in Docker, `ca_cert_path` must reference an absolute POSIX path on the Docker daemon host. The file is bind-mounted read-only into the egress sidecar container only.

```toml
[egress]
image = "opensandbox/egress:<version>"
mode = "dns+nft"

[egress.upstream_proxy]
url = "https://proxy.example.com:8443"
# Optional complete Proxy-Authorization value:
# authorization = "Basic <base64>"
ca_cert_path = "/etc/ssl/certs/proxy-ca.pem"
```

### Kubernetes configuration

For a private proxy CA in Kubernetes, `ca_secret_name` references a Secret in the sandbox namespace. The Secret must contain the fixed key `ca.crt`, which is projected read-only into the egress sidecar container.

```toml
[egress]
image = "opensandbox/egress:<version>"
mode = "dns+nft"

[egress.upstream_proxy]
url = "https://proxy.example.com:8443"
# Optional complete Proxy-Authorization value:
# authorization = "Basic <base64>"
ca_secret_name = "upstream-proxy-ca"
```

## Traffic and security model

For a detailed overview of egress packet flows, see [/architecture/network/egress](/architecture/network/egress).

1. **Resolution & Routing**: Under `dns+nft`, proxy hostnames are resolved by infrastructure DNS even under a deny-all policy. Egress reachability is enforced by dedicated nftables sets scoped strictly to the mitmproxy UID, proxy IP, and proxy port, isolating proxy traffic from sandbox dynamic allow sets.
2. **Protocol & Next Hop**: Both `http://` and `https://` proxy endpoints are supported. For HTTPS endpoints, SNI and server hostname validation are enforced. Configured CA bundles augment the system trust store and apply to all mitmproxy upstream TLS verifications.
3. **CONNECT Authority**: Intercepted TLS traffic uses the SNI or Host-derived FQDN for the CONNECT authority when available, falling back to `IP:port`. Direct dials are strictly refused when chaining is enabled to prevent silent bypass.
4. **Credential Isolation**: Business credentials from Credential Vault are injected into the inner target request, while `authorization` is attached exclusively as `Proxy-Authorization` on the outer CONNECT request. Neither value is written to logs. For secret management details, see [/guides/credential-vault](/guides/credential-vault).
5. **Secret Exposure Bounds**: Proxy authorization values appear as literal environment variables inside the egress container and are visible via `docker inspect` or Kubernetes Pod specs. Restrict host and cluster inspection permissions accordingly. The authorization environment and CA mount are added only to the egress container, not the sandbox container.

## Runtime compatibility matrix

Compatibility reflects current upstream `main` only:

| Runtime / Platform | Support Status | Operational Constraints |
|---|---|---|
| Docker (`runc`) | Supported | Requires `[docker] network_mode = "bridge"`; use `ca_cert_path` only when a private CA is required. |
| Kubernetes (`runc`) | Supported | Non-pooled AgentSandbox and BatchSandbox workloads; use `ca_secret_name` only when a private CA is required. |
| Kubernetes (`Kata`) | Supported | Accepted by egress validation; validate the selected RuntimeClass, CNI, and kernel in your deployment. |
| gVisor (`secure_runtime.type = "gvisor"`) | Rejected | Sandbox creation returns HTTP 400 because gVisor netstack lacks the required iptables nat redirect. |
| Kubernetes Pool Mode | Unsupported | Rejects per-request `networkPolicy`; cannot attach per-allocation egress sidecars. |
| Fast Sandbox | Unsupported | Current fast-sandbox profile rejects `networkPolicy` when `upstream_proxy` is set. |

## Protocol and streaming behavior

| Protocol / Traffic Type | Supported | Behavior & Constraints |
|---|---|---|
| HTTP/1.1 and HTTP/2 application traffic | Yes | Mitmproxy can negotiate HTTP/2 with application targets. The upstream web-proxy hop uses HTTP/1 CONNECT; HTTP/2 CONNECT is not supported by mitmproxy 11.0.2. |
| Ports 80 & 443 | Yes | Always intercepted; Credential Vault binding applies to canonical ports 80/443. |
| Extra TCP Ports | Bounded | Intercepted when configured, but Credential Vault binding does not apply. |
| Streaming and large request bodies | Preserved with limits | Credential header injection occurs in `requestheaders` before body streaming. Body placeholder substitution is skipped for streamed bodies. |
| Server-Sent Events (SSE) | Caveat | Forced into streaming mode. Truncation can occur if origin closes with TCP RST; see [/guides/egress-sse-truncation](/guides/egress-sse-truncation). |
| TLS pass-through | No | Non-empty `ignore_hosts`, `tcp_hosts`, or `udp_hosts` makes the upstream addon fail to load; no-SNI pass-through is refused at request time. |
| UDP and QUIC | No | These flows cannot use HTTP CONNECT and are refused rather than sent directly. |
| WebSocket (WSS) | Unverified | Dedicated chained-proxy WSS compatibility is not independently verified. |
| IPv6 Egress | Incomplete | Incomplete Kubernetes dual-stack support; `egress.disable_ipv6` defaults to `true`. |

The default-off experimental OSEP-0023 revision runtime has a narrower live HTTP/2 admission boundary: only the fixed-dependency direct/lazy path is wired, and configured chained upstream proxies remain rejected for that path. Existing HTTP/1 and legacy proxy behavior in the matrix above is unchanged. See the [HTTP/2 integration boundary](/architecture/network/osep-0023-h2-integration) for drain semantics and incomplete client-migration and Linux acceptance.

## Deployment, rotation, and rollback

- **Application Bounds**: Proxy configuration changes apply only to newly created sandboxes. Existing sidecars retain their creation-time proxy settings.
- **Proxy or Authorization Rotation**: Update the lifecycle server configuration, restart the lifecycle server, then recreate affected sandboxes.
- **CA Rotation**: Update the Docker CA file or Kubernetes Secret. If its configured path or Secret name is unchanged, the lifecycle server does not need a restart, but affected sandboxes still need recreation. Recreating avoids relying on bind-mount or Secret `subPath` update behavior.
- **Rollback Procedure**: Remove the `[egress.upstream_proxy]` section, restart the lifecycle server, and recreate affected sandboxes. Existing sandboxes remain unchanged until they are recreated, and proxy failure never triggers an automatic direct fallback.

## Verification checklist

1. Confirm the lifecycle server starts with the new configuration; invalid URL, mode, and runtime-specific CA fields fail validation before sandbox creation.
2. Create a new sandbox with `networkPolicy` and transparent interception enabled.
3. Confirm the egress health endpoint returns `200 OK`; `503 mitmproxy not ready` means the interception process did not initialize.
4. Check startup logs for the `credential proxy: upstream proxy chaining enabled` message, without printing environment values.
5. From the sandbox, access an allowed HTTPS destination with normal certificate verification. Do not use `curl -k` or set `ssl_insecure`.
6. Confirm a denied destination stays denied and inspect the external proxy's telemetry for the expected FQDN CONNECT authority.
7. If Credential Vault is enabled, confirm the target receives the business credential while the forward proxy receives only `Proxy-Authorization`.

## Troubleshooting table

| Failure Category | Observed Symptoms | Root Cause & Resolution |
|---|---|---|
| Config-load error | Server fails to start; configuration parse errors | Invalid proxy URL, non-absolute Docker CA path, or mixing Docker and Kubernetes CA settings. |
| Request 400 error | Sandbox creation rejected with HTTP 400 | Transparent interception is missing, `secure_runtime.type = "gvisor"` is selected, or Pool/Fast Sandbox is combined with per-request `networkPolicy`. |
| Pod or bind setup failure | Kubernetes Pod stays pending, or Docker container creation fails | The Kubernetes Secret/key is missing, or the Docker daemon cannot read the configured host path. Correct the administrator-managed source and recreate the sandbox. |
| Sidecar startup failure | Egress container restarts or fails readiness | Custom pass-through settings are incompatible, or the mounted CA bundle is malformed or unreadable. |
| Sidecar 503 error | Egress health checks return HTTP 503 | Mitmproxy has not initialized. Inspect sidecar startup logs; runtime proxy outages affect requests but do not by themselves make the local listener unready. |
| TLS / SAN failure | Handshake failure; certificate validation errors | Upstream proxy certificate SAN does not match proxy hostname, or missing custom CA bundle. |
| Proxy auth failure | Requests through the proxy fail; proxy telemetry reports 407 | Supply the complete, valid `authorization` value in server configuration and recreate the sandbox. |
| Direct-dial error | Logs contain `upstream proxy required: direct egress dial refused` | Expected fail-closed behavior for a flow that cannot be chained; do not bypass it by disabling TLS verification. |
| SSE / stream truncation | Streaming connection drops prematurely | Origin closed connection with TCP RST while unread bytes remained; review upstream gateway keep-alive settings. |

## Known limits and references

- **No Per-Request Proxy**: Proxy settings cannot be modified per request or through environment variables.
- **No Hot Reload**: Existing sandbox sidecars cannot reload proxy configurations dynamically.
- **No Dedicated OTLP Metrics**: There is no dedicated chained-proxy metric family; rely on egress health checks, DNS/nftables metrics, and proxy logs.
- **Runtime Scope**: No production certification is claimed for every Kata implementation, custom RuntimeClass, CNI, or kernel combination.
- **References**:
  - [Egress Architecture Specification](/architecture/network/egress)
  - [Credential Vault Operations Guide](/guides/credential-vault)
  - [Server-Sent Events Truncation Guide](/guides/egress-sse-truncation)
  - [Server Configuration Reference](https://github.com/opensandbox-group/OpenSandbox/blob/main/server/configuration.md#egress)

---

### Guides Isolation Sessions (`docs/guides/isolation-sessions.md`)

---
title: Isolation Sessions
description: Per-execution bubblewrap-isolated bash sessions inside a sandbox for fast, contained code runs.
---

# Isolation Sessions

Isolation sessions run a **long-lived `bash` inside a [bubblewrap](https://github.com/containers/bubblewrap) namespace**, so one sandbox pod can host many mutually isolated task runs without spinning up a new container. Each session gets its own PID, mount, tmpfs, and env namespaces; startup is sub-millisecond.

## Table of Contents

- [Requirements](#requirements)
- [Overview](#overview)
- [How It Works](#how-it-works)
- [Quick Start](#quick-start)
- [Session Lifecycle](#session-lifecycle)
- [Workspace Modes](#workspace-modes)
- [Bind Mounts and Allowlist](#bind-mounts-and-allowlist)
- [Profiles and Defaults](#profiles-and-defaults)
- [Environment, UID, and Networking](#environment-uid-and-networking)
- [Filesystem Proxy](#filesystem-proxy)
- [Capabilities and Probing](#capabilities-and-probing)
- [Server Configuration](#server-configuration)
- [Limitations](#limitations)
- [See Also](#see-also)

---

## Requirements

Component versions needed for the features covered by this guide:

- `execd` >= 1.0.20 for base isolation session support; **>= 1.0.21 recommended** for `binds`, `List sessions`, `uid_mode: "userns"`, and the default writable allowlist (`/workspace`, `/mnt`, `/media`, `/data`)
- `opensandbox-server` >= 0.2.1 — the server injects `CAP_SYS_ADMIN`, unconfined AppArmor, seccomp, and protected-system-path settings, and the tmpfs mount required by `bwrap` when the execd image declares `bootstrap.execd.isolation`
- Python SDK >= 0.1.14 (`isolation.run_once` / `isolation.session` context manager); >= 0.1.13 for the generated isolation client only
- JavaScript / TypeScript SDK >= 0.1.10 (`isolation.runOnce` / `isolation.withSession`)
- Kotlin / Java SDK >= 1.0.16 (`isolation.runOnce()` / `isolation.withSession { ... }`); >= 1.0.15 for the generated isolation client only
- C# SDK >= 0.1.4 (`RunOnceAsync` / `WithSessionAsync`)
- Go SDK >= 1.0.4 (`IsolationRunOnce` / `IsolationWithSession`)

The `setpriv_available` / `userns_available` fields on [`/capabilities`](#capabilities-and-probing) were added after execd v1.0.21 and are only present on execd builds from `main` at the time of writing; older execd omits both fields (clients must tolerate their absence).

Host requirements (`bwrap` binary, `CAP_SYS_ADMIN`, `overlayfs`, etc.) are listed under [Server Configuration → Host Requirements](#server-configuration).

The published execd image includes the fail-closed native workload gate used during session startup. For a Linux source build, `make build` requires a C compiler plus static libc and produces `bin/opensandbox-session-gate`; run `make build-session-gate` and then `sudo make install-session-gate` before starting execd to install the helper at `/opt/opensandbox/opensandbox-session-gate`. Keep that path and its parent directory root-owned and not group- or world-writable.

---

## Overview

| Concept | Boundary | Startup | Typical Use |
|---|---|---|---|
| **Isolation session** | bubblewrap namespaces inside one sandbox | ~100 ms to create; subsequent `run` in an existing session is near-zero overhead | Many short, mutually isolated task runs reusing one long-lived session |
| Bash session (`/session`) | bash process, no extra namespaces | ms | Interactive REPL-style command sequences |
| Sandbox | container or pod | 100s of ms to seconds | Tenant, workspace, or user boundary |
| Secure runtime (gVisor / Kata) | user-space kernel or VM per sandbox | 10–500 ms | Hardware-level protection against container escape |

::: info Session creation vs. per-run cost
Creating a session takes about 100 ms because execd waits briefly after starting `bwrap` to detect an immediately-exiting child. Once the session exists, each `POST /run` reuses the same bash process, so per-run overhead is negligible. Design workloads that amortize the create cost across many `run` calls in one session.
:::

Good fits: **RL rollouts**, **batch code grading**, **multi-tool agent runs** — one sandbox per worker, many isolated tasks inside.

Not a fit: cross-language kernels (use `/code`), interactive REPLs (use `/session`), or hard trust boundaries against kernel exploits (use gVisor / Kata; see [Secure Container Runtime](/guides/secure-container)).

---

## How It Works

`execd` forks one `bwrap` child per session; `bwrap` sets up Linux namespaces, then `exec`s a long-lived `bash` inside them.



Two things the diagram doesn't show:

- `bash` is long-lived, so `export X=1` in one `run` is visible to the next `run` **in the same session** (never in another session).
- The FS proxy reads and writes the merged workspace view from **outside** the namespace, so uploads and downloads work while `bash` is busy.

### `run` request flow



Non-happy paths:

| Event | What happens |
|---|---|
| Run `timeout_seconds` elapsed | execd cancels the run context, which sends `SIGINT` to the bwrap process group and emits an `IsolatedError` SSE event; the session itself stays alive. |
| `bwrap` process exited | The next `run` returns an `IsolatedError` SSE event with `session process has exited`; only `ErrContextNotFound` (session ID unknown) becomes an HTTP-level error. `DELETE` + recreate. |
| Idle timeout reached | GC runs the same teardown as `DELETE`. |
| Client disconnects mid-SSE | The Gin request context is cancelled and execd sends `SIGINT` to the running command; the run does **not** continue in the background. |

---

## Quick Start

### curl

```bash
# Probe.
curl -s http://localhost:44772/v1/isolated/capabilities

# Create.
SESSION=$(curl -s -X POST http://localhost:44772/v1/isolated/session \
  -H "Content-Type: application/json" \
  -d '{
    "profile": "strict",
    "workspace": {"path": "/workspace", "mode": "overlay"},
    "idle_timeout_seconds": 300
  }' | jq -r .session_id)

# Run (SSE: stdout / error / complete).
curl -N -X POST "http://localhost:44772/v1/isolated/session/$SESSION/run" \
  -H "Content-Type: application/json" \
  -d '{"code": "export X=1; echo $X", "timeout_seconds": 30}'

# Second run reuses shell state.
curl -N -X POST "http://localhost:44772/v1/isolated/session/$SESSION/run" \
  -H "Content-Type: application/json" \
  -d '{"code": "echo $X"}'  # prints 1

# Delete.
curl -X DELETE "http://localhost:44772/v1/isolated/session/$SESSION"
```

### Python SDK

```python
from opensandbox import Sandbox
from opensandbox.models.isolated import (
    CreateIsolatedSessionRequest, IsolatedWorkspaceSpec, IsolatedRunOpts,
)

# Sandbox.create is an async classmethod, so await it before entering
# the async context manager.
async with (await Sandbox.create("python:3.11")) as sandbox:
    # One-shot.
    await sandbox.isolation.run_once(
        "python -c 'print(42)'", workspace="/workspace", profile="strict",
    )

    # Persistent session.
    async with sandbox.isolation.session(
        CreateIsolatedSessionRequest(
            workspace=IsolatedWorkspaceSpec(path="/workspace", mode="overlay"),
            profile="strict",
            idle_timeout_seconds=300,
        )
    ) as session:
        await session.run("export STAGE=train")
        await session.run("python train.py", opts=IsolatedRunOpts(timeout_seconds=600))
        await session.files.write_file("/workspace/data.csv", data_bytes)

    # Reattach after a client restart.
    handle = await sandbox.isolation.attach(known_session_id)
```

### JavaScript / TypeScript SDK

```ts
await sandbox.isolation.runOnce("node -e 'console.log(1)'", "/workspace", {
  profile: "strict",
});

await sandbox.isolation.withSession(
  { profile: "strict", workspace: { path: "/workspace", mode: "overlay" } },
  async (session) => { await session.run("npm test"); },
);
```

### Kotlin SDK

```kotlin
sandbox.isolation().runOnce(code = "python -c 'print(1)'", workspace = "/workspace")

sandbox.isolation().withSession(request) { session ->
    session.run("ls /workspace")
}
```

---

## Session Lifecycle

Under `/v1/isolated/` on execd. Use `X-EXECD-ACCESS-TOKEN` when execd has an access token configured.

| Method | Path | Purpose |
|---|---|---|
| `POST` | `/session` | Create; returns `{session_id, created_at}`. |
| `GET` | `/sessions` | List active sessions. |
| `GET` | `/session/{id}` | Full state; echoes creation params so a stateless client can rebuild the handle. |
| `POST` | `/session/{id}/run` | Foreground (default): SSE stream `stdout` / `error` / `complete`. Background (`"background": true`): returns `202` with a run handle. Runs on the same session are serialized. |
| `GET` | `/session/{id}/runs/{runId}` | Background run status: `running`, `exit_code`, `error`, timestamps. |
| `GET` | `/session/{id}/runs/{runId}/logs` | Background run combined output as plain text, with byte-cursor pagination. |
| `DELETE` | `/session/{id}` | Destroy. |
| `GET` | `/capabilities` | Probe. |

`idle_timeout_seconds > 0` destroys idle sessions automatically; set to `0` to disable idle GC and always `DELETE` explicitly.

---

## Background Runs

A background run starts code detached inside the session and returns immediately; the run's combined stdout/stderr and exit code are captured by execd, and the client polls them while other work continues on the session.

```bash
# Start detached; 202 returns {"session_id", "run_id", "started_at"}.
RUN=$(curl -s -X POST "http://localhost:44772/v1/isolated/session/$SESSION/run" \
  -H "Content-Type: application/json" \
  -d '{"code": "sleep 5 && echo done", "background": true}' | jq -r .run_id)

# Poll status until running=false.
curl -s "http://localhost:44772/v1/isolated/session/$SESSION/runs/$RUN"

# Read output (plain text). The response header EXECD-ISOLATED-TAIL-CURSOR
# carries the next byte offset for incremental reads.
curl -s "http://localhost:44772/v1/isolated/session/$SESSION/runs/$RUN/logs"
curl -s "http://localhost:44772/v1/isolated/session/$SESSION/runs/$RUN/logs?cursor=123"
```

Python SDK:

```python
session = await sandbox.isolation.create(CreateIsolatedSessionRequest(
    workspace=IsolatedWorkspaceSpec(path="/workspace", mode="rw"),
))

run = await session.run_background("sleep 5 && echo done")
while (status := await session.run_status(run.run_id)).running:
    await asyncio.sleep(0.2)
print(status.exit_code)
logs = await session.run_logs(run.run_id)   # .text + .cursor for pagination
```

Background run semantics:

- `timeout_seconds` is foreground-only; background runs are not time-limited.
- Idle GC is suspended while a background run is active; after it finishes, the normal idle window applies. Deleting the session kills the run.
- Background runs require a writable log location, so sessions with a read-only (`ro`) workspace reject them with `400`; `rw` and `overlay` workspaces are supported.
- Runs share the session's process group, so session-level signals (e.g. the `SIGINT` sent when a foreground run times out or is cancelled) also reach them.
- Each `logs` request returns at most 16 MiB; page through large output with the returned cursor. Per-run log retention is capped at 16 MiB — output beyond the cap is discarded when the run completes, so drain incrementally while the run is active if you need more than the first page.
- A run whose session dies mid-flight reports `running: false` with `error: "session terminated"`; run records and their logs are removed when the session is deleted or garbage-collected.

---

## Workspace Modes

| Mode | Semantics |
|---|---|
| `rw` | Bind-mount read-write; writes persist on the host. |
| `overlay` (default) | Copy-on-write via overlayfs; writes go to a per-session upper dir and vanish on `DELETE`. |
| `ro` | Bind-mount read-only; writes fail with `EROFS`. |

Overlay upper dirs live under `upper_root` (default `/var/lib/execd/isolation`).

::: warning Overlay mode is one-directional for the files API
While a session is running, files-API writes on an `overlay` workspace land in the host upper directory and are **not visible to `session.run`**: the kernel caches the overlayfs directory entries at mount time, so host-side writes bypass the overlay the namespace already holds. The `session.run` → `session.files` direction always works. For bidirectional file exchange between the two views, use workspace `mode: "rw"` or a bind mount instead of `overlay`.
:::

Because isolated-session state lives only in execd's memory, every session left under `upper_root` when execd exits is stale. Before publishing a new session, execd creates a durable cleanup record at `<upper_root>/.execd-cleanup/<session-id>`; on startup it recovers those records and removes the exact matching session directories. A record is retired only after the entire session subtree has been deleted, so partial deletion and transient filesystem failure retain a retryable identity across restarts. Residue that cannot be removed yet — e.g. an upper still referenced by a mount from the previous lifetime — stays counted toward `upper_max_bytes` and is retried by the idle collector.

Only recorded session directories are removed. Unrecorded children under `upper_root`, including allocator-shaped directory trees, are treated as operator data and are never reclaimed automatically. Directories left by execd versions that predate the cleanup registry therefore require explicit migration or manual cleanup; keep `upper_root` dedicated to execd and do not modify the reserved `.execd-cleanup` registry. In pooled / pre-provisioned sandboxes this prevents one occupant's session data from leaking to the next.

```json
{ "workspace": { "path": "/workspace", "mode": "overlay" } }
```

---

## Multiple Overlay Mounts

A session can carry **several independent overlay mounts** via the `overlays` request field (the single `workspace` remains supported as sugar for a one-element list; when both are present, `workspace` is prepended):

```json
{
  "overlays": [
    { "path": "/", "mode": "overlay" },
    { "path": "/workspace", "mode": "overlay", "persist": true },
    { "path": "/data/scratch", "mode": "overlay", "persist": false }
  ]
}
```

- Each entry has its own mount semantics, so workspace files, system-level changes (`apt install` → `/usr`, config → `/etc`), and additional project directories get **independent copy-on-write uppers**.
- Mounts are applied shallow-first; a nested overlay (e.g. `/workspace` on top of a `/` root overlay) shadows its ancestors within its own subtree. Paths must be absolute and unique.
- `persist` (overlay mode only, default `true`): `true` allocates a host upper directory under `upper_root`; `false` uses an ephemeral tmpfs upper whose writes are discarded when the session ends. `rw`/`ro` entries must not set `persist`. An ephemeral upper lives inside the namespace only, so the files API serves `persist=false` overlays from their host-side content: in-session writes under such an overlay are not observable through the files API and files-API writes into it are rejected.
- The files API routes each request to the session mount (overlay or bind) whose mount path is the longest prefix of the requested path; relative paths resolve against the first overlay. Paths outside every session mount — a `binds` destination that was not configured, a strict profile's private `/tmp`, `/etc`, ... — are rejected with an error instead of being silently resolved against the workspace.
- Background runs use the **first** overlay for their log location: it must be `rw`, or `overlay` with `persist: true`; otherwise background runs are rejected.

---

## Bind Mounts and Allowlist

- **`extra_writable`** — paths bind-mounted read-write at the same path (`source == destination`).
- **`binds`** — explicit `source` → `dest` mappings, optionally `readonly`. `source` must already exist; `dest` must already exist inside the namespace (bake it into the sandbox image).

```json
{
  "workspace": { "path": "/workspace", "mode": "rw" },
  "extra_writable": ["/data/scratch"],
  "binds": [
    { "source": "/data/in",  "dest": "/mnt/in",  "readonly": true },
    { "source": "/data/out", "dest": "/mnt/out" }
  ]
}
```

Every `source` is checked against the operator-configured `allowed_writable` allowlist **after** symlink resolution, so symlinks cannot escape it. Default allowlist: `/workspace`, `/mnt`, `/media`, `/data` (subpaths allowed). Empty allowlist rejects all extra binds.

Bind destinations are also served by the session files API: paths under `dest` resolve to the host `source` directory, so `session.files` and `session.run` see the same data in both directions (a bind is a live mount, unlike the overlay-mode upper). The bind view mirrors the namespace exactly: symlinked entries are followed (bubblewrap binds do not redirect symlink traversal) and `.wh.`-prefixed names are ordinary files, not overlay whiteouts. Writes through the files API are rejected for `readonly` binds, matching `--ro-bind` inside the namespace. When a bind shares a `dest` with an overlay or `extra_writable` path, the bind shadows it in both the namespace and the files view.

Destination constraints keep the two views in agreement; session creation rejects otherwise-valid-looking configs:

- **No duplicate `dest`** across `binds` entries (bwrap would expose only the last).
- **A `dest` must not be a strict ancestor** of another mount destination (`binds`, `overlays`, or `extra_writable`) — the later parent mount would hide that subtree in the namespace while the files API would still route into it.
- **A `dest` must not be `/run`, `/dev`, or `/proc`** (or nested under them) — the trusted base mounts are applied after every caller mount and always win.

---

## Profiles and Defaults

The `profile` field currently controls only how `/tmp` is exposed inside the namespace:

| Profile | `/tmp` |
|---|---|
| `strict` (default) | Private tmpfs (`--tmpfs /tmp`) |
| `balanced` | Shared with the sandbox (`--bind /tmp /tmp`) |

`workspace.mode` and `env_passthrough` are **independent** of the profile: when they are omitted, execd normalizes `workspace.mode` to `overlay` and `env_passthrough.mode` to `deny` regardless of which profile you pick. Set those fields explicitly if you want persistent workspace writes (`"rw"`) or host env passthrough (`"allow"`).

---

## Environment, UID, and Networking

- **`env_passthrough`** — `mode: "allow"` + `keys` whitelists host env vars; default `deny`. Per-run overrides go in `IsolatedRunRequest.envs`.
- **`uid` / `gid`** with **`uid_mode: "setpriv"`** (default, real setuid/setgid drop) or **`"userns"`** (user namespace remap). Check `setpriv_available` / `userns_available` from [`/capabilities`](#capabilities-and-probing) before requesting a mode.
- **`share_net: true`** shares the sandbox's network namespace. Sandbox-level egress and Credential Vault policies still apply.
- **`share_net: false`** creates a private network namespace. Before releasing the workload startup gate, execd opens the authenticated NetNS, obtains its real owning UserNS with `NS_GET_USERNS`, and bind-pins both below `/run/execd/namespaces/<opaque-id>/`. Any validation or pin failure aborts Session creation. Execd attempts pin cleanup on failed startup, process exit, explicit delete, idle collection, and runner shutdown; retryable failures retain Session ownership and are retried while execd remains alive. This applies to both UID modes; hardened network backends will require `uid_mode: "userns"`.

The legacy default remains unchanged in this phase: omitting `share_net` continues to share the sandbox network namespace. Namespace pinning alone does not enable Session egress or ingress.

Private Sessions are not recoverable across an execd restart. Deployments that enable them must treat execd as sandbox-critical and recreate the entire sandbox/container if execd exits; they must not launch a replacement execd inside the surviving mount namespace. Destroying the sandbox/container tears down that mount namespace and releases all pins. Execd intentionally does not scan and adopt opaque namespace-pin directories on startup because they do not carry a durable sandbox generation, so doing so could unmount another live process's resources.

---

## Filesystem Proxy

Reads and writes the session's **merged** mount view — the workspace overlays plus every `binds` destination — from outside the namespace. This is how SDKs `upload`, `download`, and `list` without spawning a shell, including against bind-mounted persistent data. The files API serves exactly those session mounts: absolute paths outside every mount (namespace-only locations such as a strict profile's private `/tmp`, or unconfigured `binds` destinations) are rejected with an error rather than resolving to a different file. All paths are per session under `/v1/isolated/session/{id}/`:

| Method | Path |
|---|---|
| `GET` | `files/info?path=...` |
| `GET` | `files/download?path=...` (supports `Range` and `offset`/`limit`) |
| `POST` | `files/upload` (multipart: `metadata` + `file`) |
| `DELETE` | `files?path=...` |
| `POST` | `files/mv` |
| `POST` | `files/permissions` |
| `POST` | `files/replace?verbose=true` |
| `GET` | `files/search?path=...&pattern=...` |
| `GET` | `directories/list?path=...&depth=N` |
| `POST` | `directories` |
| `DELETE` | `directories?path=...` |

Writes on a `ro` workspace fail; writes on `overlay` land in the upper dir and vanish on `DELETE`, and while the session runs they are not visible to `session.run` (see [Workspace Modes](#workspace-modes)); writes on a `readonly` bind fail.

---

## Capabilities and Probing

```bash
curl -s http://localhost:44772/v1/isolated/capabilities
```

```json
{
  "available": true,
  "isolator": "bwrap",
  "version": "0.9.0",
  "setpriv_available": true,
  "userns_available": false,
  "commit_supported": false,
  "diff_supported": false
}
```

- `available: false` — the trusted native workload gate is missing or untrusted, bubblewrap is missing, or the host can't create the required namespaces (missing `CAP_SYS_ADMIN`, restricted user-ns sysctl, etc.).
- `setpriv_available` / `userns_available` — whether sessions with `uid_mode: "setpriv"` or `"userns"` can be created. `setpriv_available` reflects only execd's **default** UID/GID; a session that requests a different UID/GID may still return `503 NOT_SUPPORTED` when identity switching is unavailable.
- `commit_supported` / `diff_supported` — Phase 2 stubs, currently return `503`.

---

## Server Configuration

Point execd at an optional TOML file:

| Flag | Env |
|---|---|
| `--isolation-config` | `EXECD_ISOLATION_CONFIG` |

```toml
# Parent directory for per-session overlay upper dirs.
upper_root = "/var/lib/execd/isolation"

# Allocation-time threshold for total overlay upper-directory size (bytes).
# Existing sessions can write beyond this value.
# Default: 8 GiB. Set to 0 to disable the allocation check.
upper_max_bytes = 8589934592  # 8 GiB

# Sources allowed for extra_writable / binds (symlink-resolved).
# Default: ["/workspace", "/mnt", "/media", "/data"]. Empty = reject all.
allowed_writable = ["/workspace", "/mnt", "/media", "/data"]
```

Example: `components/execd/configs/isolation.example.toml`.

`upper_max_bytes` is checked when creating an `overlay` workspace (the default mode). If a successful usage scan reports a total at or above the configured positive limit, the new session is rejected. This setting does not cap writes by existing sessions and does not apply to `rw` or `ro` workspaces.

Deleting an overlay session can restore admission once its cleanup succeeds and total usage falls below the threshold. Deletion discards that session's private upper data, so preserve any data you need before deleting it.

**Host requirements:** `bwrap` and the trusted native workload gate in the execd image; `CAP_SYS_ADMIN` (and `kernel.unprivileged_userns_clone=1` for `uid_mode: "userns"`); `overlayfs` in the kernel for `overlay` workspaces. The published image installs the gate automatically. Linux source builds must run `make build-session-gate` and then `sudo make install-session-gate` from `components/execd` before starting execd.

Note: `/capabilities` reports `available: false` when the native workload gate cannot be opened as a trusted executable or when bwrap itself cannot be started (missing binary or missing namespace capabilities). A missing `overlayfs` does **not** flip `available` — the overlay probe only influences Phase 2 `commit`/`diff` support, and default overlay-mode session creation can still fail at runtime on such hosts. If you rely on `workspace.mode: "overlay"`, verify `overlayfs` support directly on the host.

---

## Limitations

- **`diff` / `commit` are Phase 2 stubs**, currently return `503`.
- **No hardware-level guarantee.** Namespaces + seccomp only; pair with a secure runtime for kernel-exploit defense.
- **Linux only.** Non-Linux builds return `available: false`.
- **Serialized runs per session** — create multiple sessions for parallelism.
- **Bind destinations must pre-exist** in the sandbox image.

---

## See Also

- [execd](/architecture/data-plane/execd)
- [Secure Container Runtime](/guides/secure-container)
- [execd OpenAPI spec](/api/)

---

### Guides Kubernetes Resume Benchmark (`docs/guides/kubernetes-resume-benchmark.md`)

---
title: Kubernetes Resume Benchmark
description: Reproduce end-to-end rootfs resume latency measurements for the standard Kubernetes BatchSandbox backend.
---

# Kubernetes Resume Benchmark

The opt-in [runner](https://github.com/opensandbox-group/OpenSandbox/blob/main/tests/python/benchmarks/kubernetes_resume.py) measures standard Kubernetes **standalone, rootfs** pause/resume. It creates one sandbox per trial, writes a unique marker outside volume mounts, pauses it, waits for its Pods to disappear, resumes it, and checks the marker through a foreground command. It deletes the sandbox after each trial. QEMU VMState, pooled creation, and Fast Sandbox are outside this baseline.

## Prerequisites

- A lifecycle server configured for the Kubernetes BatchSandbox provider, with the controller and OCI snapshot registry configured as in [Pause and Resume](/guides/pause-resume).
- `kubectl` connected to the same cluster, with permission to read BatchSandbox, SandboxSnapshots, Pods, nodes, and cluster version in the sandbox namespace. Pass an explicit context to avoid measuring against a different cluster.
- An immutable image digest containing `/bin/sh`, `cat`, and `sleep`. The server must inject execd. Do not put the marker directory under a volume mount.
- A runner with network access to the lifecycle server and sandbox endpoints; `--server-proxy` measures the server proxy route instead of direct access.

This is an explicit benchmark, not part of the default E2E suite. Use a dedicated test cluster. No node caches are evicted by the runner.

## Run

Create an environment file containing the versions and conditions needed to repeat the experiment. For example, `environment.json`:

```json
{
  "server_revision": "<git SHA or image digest>",
  "controller_image": "<image digest>",
  "execd_image": "<image digest>",
  "image_committer_image": "<image digest>",
  "runtime_class": "runc (default)",
  "node_resources": "<CPU, RAM, disk type>",
  "registry": "<implementation, placement, network conditions>",
  "snapshot_configuration": "<non-secret controller flags>",
  "cache_evidence": "Uncontrolled; no cache intervention",
  "background_load": "<other workloads and concurrency>"
}
```

Do not include credentials. The output also records the runner revision, Python and SDK versions, Kubernetes version, node runtime versions, context, image, resource limits, proxy mode, timeouts, and polling interval.

```bash
cd tests/python
uv sync --frozen
export OPEN_SANDBOX_DOMAIN=localhost:8080
export OPEN_SANDBOX_API_KEY='<your API key>'
uv run python benchmarks/kubernetes_resume.py \
  --image '<image>@sha256:<digest>' \
  --namespace default --context '<test-context>' \
  --environment environment.json --scenario uncontrolled \
  --warmups 3 --samples 30 --output resume-uncontrolled.jsonl
```

Use `--protocol https` for a TLS server and `--server-proxy` when direct endpoints are inaccessible. Run `--help` for resource, polling, and timeout options. The script disables SDK retries and telemetry for this experiment. Polling remains explicit and includes its delay in the measured observation time.

## Measurement boundaries

All durations use the runner's monotonic clock and start immediately before `SandboxManager.resume_sandbox`. The resume portion has one shared timeout:

| Field | End of interval |
| --- | --- |
| `api_ack_ms` | Resume API returns; acceptance is not readiness |
| `running_ms` | First lifecycle GET reporting `Running` |
| `ready_ms` | A fresh `Sandbox.connect` resolves endpoints and passes execd health checks |
| `command_ms` | A foreground command returns successfully and prints the exact persisted marker |

These are cumulative times, not additive stages. `command_ms` includes an execd round trip and is the primary user-visible result. It does not measure an application-specific Agent initialization; add that workload in a separate experiment. Lifecycle observation and SDK readiness are sequential, so the polling interval and HTTP overhead affect results. No attempt is made to infer scheduler or image-pull durations from these client timings.

Creation, marker writing, pause, Pod deletion, optional cache preparation, and evidence collection, and cleanup are outside the resume interval. Waiting for source Pods to disappear avoids declaring a terminating source instance to be the restored instance. The runner verifies the created ID exists as a standalone rootfs BatchSandbox in the selected namespace before pausing it.

## Cache conditions

`--scenario` is an experiment label, not automatic proof of cache state. Choose `uncontrolled`, `snapshot-cache-hit`, or `snapshot-cache-miss`. The latter two require `--prepare-script` pointing to a Python preparation/verification script you supply. It runs after pause and Pod deletion, before the timer, as:

```text
python prepare.py <sandbox-id> <namespace> <context> <scenario>
```

The script must exit zero only after establishing and checking the declared condition. It can inspect the sandbox's owned SandboxSnapshot CR to find the committed image digest. Its stdout is retained as `preparation_evidence` in that trial; keep it concise and free of secrets. There is a bounded preparation timeout. A failure prevents resume and is retained as a failed trial.

For a cache-hit run, pre-pull the **committed snapshot image**, not just the base image, onto every eligible node and verify it is present. For a cache-miss run, use fresh isolated workers or a controlled cache setup and verify the snapshot is absent before resume. Node pinning, image pull policy, registry caching, and layer sharing affect the result; record them in the environment file. Every trial creates a new snapshot, so warmup trials alone do not establish snapshot cache hits. Do not mix scenarios, resource profiles, or proxy modes when comparing percentiles. The runner does not validate a preparation script's claims; inspect its evidence and the retained restored Pod node/image information. Collect kubelet events separately if you need image-pull or scheduling attribution.

## Results and interpretation

The new output path is opened exclusively to avoid overwriting previous results. JSONL contains an environment record, a flushed record for every completed trial (including excluded warmups), and a summary. Trials include their sandbox ID, stage of failure, completed timings, snapshot artifacts, restored Pod node/image information, and cleanup errors. Failed cleanup stops the run and prints the ID for manual deletion; creation response failures also stop and retain a `benchmark_run` metadata value to locate an ambiguously created sandbox; it never silently moves on to accumulating leaked sandboxes.

P50 and P95 use nearest-rank percentiles over **successful measured trials**. The summary separately reports measured failure rate and cleanup failures. A failure at a later stage does not enter partial timings into the percentile population. Warmups never enter the measured statistics. An all-failed run has null percentiles. A nonzero exit status indicates a trial or cleanup failure, including warmups. Interruptions leave the flushed records on disk; only a final summary with `completed_requested_trials: true` indicates a completed run.

Thirty samples are a starting point, not a stable tail-latency claim. Increase sample count, repeat independent runs, retain raw samples, and report successes, timeouts, failures, and environment alongside percentiles. Compare baseline and candidate revisions under the same conditions without machine-speed thresholds in CI. This guide provides a harness; it contains no measured performance claim.

---

### Guides Lifecycle Hooks (`docs/guides/lifecycle-hooks.md`)

---
title: Lifecycle Hooks
description: Run setup work before a sandbox entrypoint and schedule recurring work while it is running.
---

# Lifecycle Hooks

Lifecycle hooks let a sandbox run declarative commands at defined points in its runtime. OpenSandbox currently supports a blocking `preStart` hook and one or more non-blocking `periodic` hooks.

## Supported hooks

| Hook | Cardinality | When it runs | Failure behavior |
|---|---:|---|---|
| `preStart` | Zero or one | After execd starts listening, before the user entrypoint starts | A failure or timeout prevents the user entrypoint from starting |
| `periodic` | Zero or more | The scheduler starts after `preStart` succeeds and runs each hook on its schedule | A failed or timed-out run is logged and later runs normally continue; if a timed-out process cannot be terminated, that hook is disabled |

Both hooks run inside the sandbox with the sandbox environment. Commands are argument arrays and are executed directly. To use shell syntax such as pipes, redirects, or variable expansion, invoke a shell explicitly, for example `['sh', '-c', 'command > file']`.



The ordering is the same in both startup modes:

1. execd starts and its HTTP server begins listening.
2. `preStart` runs to completion, if configured.
3. The periodic scheduler starts, if configured.
4. The user entrypoint starts.

In execd-as-init mode, execd remains PID 1 and starts the entrypoint as its supervised child. In bootstrap mode, the bootstrap process starts the entrypoint after execd reports that lifecycle startup completed.

## Configuration

Lifecycle hooks are part of the sandbox creation request:

```json
{
  "image": { "uri": "ubuntu:24.04" },
  "entrypoint": ["tail", "-f", "/dev/null"],
  "resourceLimits": {
    "cpu": "1",
    "memory": "1Gi"
  },
  "lifecycle": {
    "preStart": {
      "command": ["sh", "-c", "echo ready > /tmp/prestart.done"],
      "timeoutSeconds": 120
    },
    "periodic": [
      {
        "name": "checkpoint",
        "schedule": "@every 5m",
        "command": ["sh", "-c", "date -u >> /tmp/checkpoints.log"],
        "timeoutSeconds": 120
      }
    ]
  }
}
```

### `preStart`

| Field | Required | Description |
|---|---:|---|
| `command` | Yes | Non-empty command and argument array |
| `timeoutSeconds` | No | When omitted, defaults to 60 seconds; explicit values must be from 1 through 10800 seconds (3 hours) |

`preStart` runs on each container start. Make the command idempotent so retrying or restarting a sandbox does not corrupt its state.

### `periodic`

| Field | Required | Description |
|---|---:|---|
| `name` | Yes | Non-blank name, unique within the sandbox |
| `schedule` | Yes | Standard five-field cron expression or descriptor such as `@hourly` or `@every 30s` |
| `command` | Yes | Non-empty command and argument array |
| `timeoutSeconds` | No | When omitted, defaults to 60 seconds; explicit values must be from 1 through 300 seconds |

An `@every` interval must be a whole number of seconds and at least one second. Runs of the same named hook never overlap: if the previous run is still active, the next scheduled run is skipped.

## Current availability

Lifecycle hooks currently require the Kubernetes provider. They are rejected by the Docker provider and Fast Sandbox backend. A request cannot combine `lifecycle` with `poolRef`.

The SDKs expose lifecycle fields on their sandbox creation APIs but do not enforce the timeout range. The Server is the authority for request validation. See the language-specific examples in the [SDK documentation](/sdks/).

## Operational guidance

- Keep `preStart` bounded and deterministic because the entrypoint waits for it.
- Make periodic work idempotent and safe to retry.
- Store credentials through the platform's supported secret mechanism rather than embedding them in commands.
- Write data that must survive node replacement to durable storage. Lifecycle hooks schedule recovery and synchronization work; they do not make the sandbox filesystem durable by themselves.

---

### Guides Live Vault Acceptance (`docs/guides/live-vault-acceptance.md`)

---
title: Live Vault Acceptance (Experimental)
description: Task spec, plan and acceptance evidence matrix for the OSEP-0023 experimental live credential-bound Vault loop in the egress sidecar.
---

# OSEP-0023 Live Credential-Bound Vault: Acceptance

::: warning Internal task document
This page separates the current Policy revision acceptance from the historical live-Vault evidence that preceded PR #2176. Earlier `fresh` or `pass` results do not validate the Policy integration or establish a passing current CI run. The runtime remains default-off and unsupported as a public deployment mode.
:::

## Policy revision integration acceptance

- Branch: `feat/osep0023-policy-revision`.
- Implementation starting commit: `1adfcecec408c2732071befb16f2412463bc3315`.
- Design baseline: `12b77ddefdeb7b35cd933bad18cf253e2c02fcbf` (PR #2176 merge).
- Scope: existing Policy API handlers and automatic always-rule reloads join the shared Policy/Vault mutation barrier. Subsequent Vault updates and bootstrap retain the committed internal Policy epoch. No public methods, schema fields, supported mode, HTTP/2 or Fast Sandbox support are added.

Success requires optional user-policy file persistence, nft application, exact receiver confirmation, then Go proxy/base publication. Reload omits user-policy persistence. This is not atomic pre-ACK visibility: nft can affect traffic first. Receiver failure after successful nft requires sticky recovery and quarantine, even on definitive rejection. API success waits for publication and old-request deadline registration, not the default 30-second drain. Expiry closes the whole connection, potentially affecting newer requests; repeated revisions do not extend the earliest old-request deadline.

The following rows separate executed checks from blocked acceptance layers. Selected unit-test passes are not full-suite or end-to-end acceptance. Command summaries below apply to the current implementation worktree; the final commit identity belongs in the corresponding validation handoff.

| ID | Required evidence | Current result |
|---|---|---|
| P1 | Go owner: Policy methods/reset, no-ops, binding preflight, exact candidate/identity, file/nft failures, cancellation, serialized reload and lifecycle cleanup | Fresh pass: selected owner units and focused race checks, including authoritative Policy shadowing and all three shutdown paths. Broader permitted selection: 18 packages, 594 top-level passes; exclusions and skips detailed below |
| P2 | Real Go → Unix → Python: Policy and reload confirmation, lost-ACK reconciliation, Policy → Vault epoch continuity, bootstrap fencing | Blocked: AF_UNIX socket creation returns EPERM in this environment |
| P3 | Real mitmdump: policy-only keepalive, still-covered idle reuse, long request/upgrade retirement, earliest deadline and whole-connection closure | Registry units: 69/69 pass. Real runtime blocked: AF_UNIX EPERM causes 14 setup failures; one negative startup check passes, not an aggregate runtime pass |
| P4 | Privileged Linux file/nft/quarantine: unchanged start failure, committed effect followed by receiver rejection/unknown, actual packet fence | Blocked: nft and required capabilities are unavailable; unshare returns EPERM |
| P5 | Current-checkout image: Policy methods, bound-deny rejection, real periodic always reload, default 30-second drain, Policy then Vault rotation; record image identity and owned-resource cleanup | Runner units: 33/33 pass. Image run fails prerequisite checks (exit 1, zero skips): Docker/nft absent and unshare returns EPERM |
| P6 | Existing Go SDK PatchPolicy/DeletePolicy 200 structure and 503 propagation; narrow spec generation; docs-site build | Fresh pass: Go SDK full suite and focused Policy cases; docs-site build. Python/TypeScript generation is limited to the documented responses; both Python handlers pass 200/503 response checks and the generated TypeScript contracts pass strict type checking |

Current checks (2026-10-11 UTC) ran on the starting commit plus the Policy integration worktree:

- Egress Go tests used package-scoped `go test -json` selections with 64 named Unix-socket or privileged tests explicitly excluded. The permitted selection passes in 18 packages, with 594 top-level test passes, four privileged tests skipped and two ACL subtests skipped. This is **not a full egress suite pass**.
- Selected owner units and `go test -race` checks pass, including the authoritative Policy shadowing regression and Policy API, always-rule reload and Vault shutdown paths. The broader focused race selection passes 75 top-level tests with zero skips or failures; it does not cover blocked IPC or privileged paths.
- `go vet ./...` and `go build .` pass with `GOFLAGS=-buildvcs=false`, because the environment cannot execute the VCS-stamping subprocess. Go dependency checks use the provisioned module cache with `GOPROXY=off` and `GOSUMDB=off`.
- Python discovery finds 363 tests; 53 runtime/Unix-socket cases are explicitly excluded from the pure-Python selection. All 310 selected tests pass with zero skips, including the 69 registry tests. This is **not full discovery acceptance** and does not replace real mitmdump evidence.
- `python -m unittest discover -v -s scripts -p 'test_*.py'` under `components/egress` passes 85 tests, including the 33 image-runner unit tests.
- `cd sdks/sandbox/go && go test ./...`: pass; main SDK package and the `poolredis` package (which has no tests).
- `go test -v -run 'Test(PatchPolicy|DeletePolicy|PolicyMutationServiceUnavailable)$'`: pass, including separate PATCH and DELETE 503 cases.
- Egress-only regeneration with `openapi-python-client 0.28.0` and `openapi-typescript 7.13.0`: baseline outputs match the checked-in files; updated outputs change only two Python Policy handlers and the TypeScript Policy response types/descriptions. Public schemas and methods are unchanged.
- Generated Python `sync_detailed` handlers: mocked HTTP 200 and plain-text 503 responses pass for PATCH and DELETE, including strict unexpected-status mode.
- TypeScript 5.9.3: strict no-emit check of the generated Policy 200/503 contracts passes. This is not a full Python or JavaScript SDK suite result.
- `cd docs && pnpm docs:build`: pass under Node 22.23.3 and pnpm 9.15.0; VitePress reports a bundle-size warning, with zero build errors.

The image runner includes `TestPolicyRevisionAPIMutations`, `TestPolicyRevisionRejectsBoundDeny`, `TestAlwaysReloadRevision`, `TestPolicyOnlyDrain` and `TestPolicyThenVaultMutation`. The Policy drain case uses the production 30-second default without an override; always-rule cases wait for the actual one-minute reload job. Their presence and runner-unit coverage do not establish image acceptance when prerequisites fail. No image was built and no real-image case ran in this environment. Real IPC, mitmdump, nft/packet and image acceptance remain blocked; no full acceptance or current GitHub CI pass is claimed.

Neither unit tests nor loopback runtime tests replace image/packet evidence. Missing Docker, nft, namespace permissions or dependencies must be reported as blocked rather than a passing skip. Image evidence must identify the actual commit/tree, dirty-diff fingerprint, image ID, executed cases, return codes and cleanup results. Do not expose credentials or add a public epoch debug API.

## Historical live-Vault evidence (PR #2176)

The remaining record is retained from the earlier live-Vault work. Its branch, baselines, session-relative evidence states and deferred checks belong to that work and are not the status of the Policy revision integration above.

- Branch: `feat/osep0023-live-vault`
- Validation baseline: `f46ff91ef572589daeeb67b453ea4e8714a68a26` was the clean starting commit. Local checks ran against that baseline plus the repair worktree, not against the unchanged baseline alone.
- Retained implementation base: `447a7be3bd3c0ee0ac2e9da5412c281aa44b1ca9`. This publication does not rebase onto upstream main; PR creation and base selection remain with the user.
- Owner: current Devin session (implementation + focused tests). Independent review is unavailable in this context — **blocked**; lead review of the full diff is complete and does not count as independent.
- Write set: `components/egress/**`, `docs/guides/credential-vault.md`, `docs/guides/live-vault-acceptance.md`, `components/egress/docs/live-vault-acceptance.md`, `oseps/0023-credential-bound-tls-interception.md`.
- Historical evidence notes: earlier runs reported "18 Go packages ok", "309 Python tests, 0 skips", "6/6 image cases PASS" — these are self-reported and unverified; no current pass is claimed until fresh logs land in the evidence directory for this session.

### Task spec

Harden the experimental live credential-bound loop before it can be trusted:

1. Unknown mutation outcomes must latch sticky recovery instead of restarting from possibly-stale prior state (AC1).
2. `ssl_insecure` must be refused wherever the live admission bundle is active (AC2).
3. ECH-bearing ClientHellos must be detected from the actual extension list and passed through opaquely; uninspectable data denies (AC3).
4. Decrypted HTTP/1 requests must prove their authority equals the admitted SNI before any credential can be injected (AC4).
5. The real Vault lifecycle (rotation, keepalive, stale revision, remove/re-add) holds under the real runtime (AC5).
6. Drain must page all expired transports, retry failed closes, and clean up all runner-created resources with recorded results (AC6).
7. Legacy installation-only behavior is unchanged (AC7).

### Plan

- Go: latch `requireRevisionRecoveryLocked(ExternalEffectsUnknown)` before every terminal detach in `mutateRevisionVault`; reject `LiveAdmission && ssl_insecure` in `validateRevisionIPCConfig`.
- Python addon: ECH detection from `ClientHello.extensions`, strict H1 authority validation, ssl_insecure fences in load/configure/per-request, cursor-based drain paging.
- Harness: live-vault image runner records per-resource cleanup results and a sanitized evidence schema; real-mitmproxy and raw-wire tests provide the boundary evidence.
- Verification: focused offline Go tests, focused Python unit tests, real loopback mitmdump tests; Docker image tests only after explicit authorization.

### Acceptance criteria and evidence matrix

Evidence state is tracked per AC: `fresh` = produced by this session's runs, `reused` = pre-existing unchanged coverage, `missing` = not yet run, `blocked` = needs a permission or resource this context lacks. Pass/fail is recorded only for checks that actually executed in this session.

| AC | Requirement | Evidence layers | Evidence state | Result |
|----|-------------|-----------------|----------------|--------|
| AC1 | Uncertain lifecycle: every terminal detach latches recovery; fresh bootstrap, stale tickets and watcher restarts rejected; old credentials never reauthorized; known rejection/cancelled/shutdown paths nonsticky | Go unit + Python-IPC integration tests (`TestRevision*`) | fresh | pass — `go test ./...` exit 0, 18 packages (log `rework-20261010/go-test-all.txt`); Go test-case skip count not recorded (not verbose) |
| AC2 | Cert/security config: live bundle + `ssl_insecure` rejected in Go launch validation and by real mitmdump startup; safe setting and legacy accepted; lazy handshake verifies upstream | Go env tests, real mitmdump startup tests | fresh | pass — 3/3 `LiveSslInsecureStartupTest` under real mitmdump |
| AC3 | ECH opaque: extension 0xfe0d (incl. GREASE) detected from the real extension list; opaque ignore_connection, no token, zero credential visibility; malformed data denies | subprocess-isolated raw-wire tests parsed by real `mitmproxy.tls.ClientHello` + real-mitmdump boundary test | fresh | pass — 2/2 `test_ech_wire` incl. real-mitmdump origin-vs-mitm cert contrast |
| AC4 | H1 authority: exactly one strict Host equal to SNI and admission token; absolute target agreement; CONNECT/dup/missing/malformed/H2 denied before injection | unit + subprocess-isolated real `read_request_head` tests | fresh (unit/parser); deferred (Docker raw H1 inputs, image layer) | pass — 17 real-parser worker cases; image layer deferred |
| AC5 | Real Vault lifecycle: rotation on keepalive, stale expectedRevision conflict, removal fence, remove/re-add | real mitmdump runtime tests; Docker image run | fresh (loopback); deferred (image run, user decision) | pass (loopback) — 344 Python tests incl. 6 runtime cases, 0 skips; Vault HTTP API stale/remove-readd in real Docker remains deferred |
| AC6 | Idle/multipage drain: after_serial paging, failed close retried, no starvation, no deadline extension, full cleanup accounting | unit tests + real mitmdump idle-close loopback test + runner unit tests; image idle-drain case | fresh (unit+loopback); deferred (image layer) | pass — idle transport closes only at deadline; runner mocked-cleanup units pass |
| AC7 | Legacy unchanged: installation-only receiver, pull-based vault path and non-live envs keep existing behavior | existing tests kept green | fresh | pass — included in the 344-test discovery |

The Docker image layer (real transparent port-443 interception, container lifecycle) is **deferred by user decision**: image acceptance is deferred to GitHub CI or a later session. It has not run, no CI has passed, and this deferral is not an acceptance downgrade — the code is prepared so the image suite can run unchanged once authorized. The docs-site build also remains deferred (offline dependency store incomplete).

### Evidence layers

Unit tests do not substitute for the real-runtime run, and no environment skip is reported as a pass. Fresh per-command logs for this session are written under the session evidence directory (`/tmp/live-vault-evidence-final/`), one child directory per run, and are linked from the handoff report rather than copied into this page.

- Go unit and cross-language IPC integration: `cd components/egress && umask 022 && GOPROXY=off GOSUMDB=off go test . -run 'TestRevision(VaultMutation|WriteHandler|Recovery)'` and `go test ./pkg/mitmproxy ./pkg/revisionruntime`.
- Python addon unit tests: `python -m unittest discover -v -s tests -p "test_*.py"` under the provisioned mitmproxy 11.0.2 venv.
- Real-mitmdump runtime tests (loopback only, disposable): `test_credential_bound_runtime.py`, `test_mitmproxy_runtime.py`, ECH/H1 wire tests.
- Real Docker image end-to-end: `components/egress/scripts/test_live_vault_image.py --image <egress image> --artifacts <evidence dir>` — **deferred by user decision** (to GitHub CI or a later session); not run in this session.

### Scope exclusions

- HTTP/2 GOAWAY/stream drain, the fast-sandbox subject profile and recovery across a complete sidecar replacement remain out of scope. Dynamic Policy and always-rule reload participation were excluded from this historical slice; they have a separate acceptance matrix above.
- Dynamic DNS learning and Kubernetes deployments are not verified by this experiment's current evidence; the Docker-network runner pins origin addresses and uses static allow rules (the local Docker embedded DNS is a runner convenience, unrelated to runtime DNS or Kubernetes evidence).
- The Dockerfile build knobs `GOPROXY` and `APT_MIRROR` remain optional and empty by default; they are local-mirror conveniences for restricted build networks only. Override setup errors fail the build; `APT_MIRROR` also requires at least one supported APT source file.

### Evidence hygiene

Test output and this document contain no credential values: fixtures mask injected headers, the origin records only header names, the runner sanitizes known fixture secrets in recorded commands, and runtime failures are reported through fixed error classes.

### Commit Identity Rewrite

The user authorized rewriting only the four live-Vault slice commits. Their author and committer are now `hpliStartAgain` with the verified-account noreply address `118508408+hpliStartAgain@users.noreply.github.com`. Each commit retains its original tree, timestamps, and Claude co-author trailer. The evidence commit's message now references the rewritten implementation commit.

| Original SHA | Rewritten SHA |
|---|---|
| `a90a41330029fb646be5c63de86b3d4d8f4ac40a` | `362e7136f14621e5015ab7447b9fb7eff19feeae` |
| `a6354dea8f8df4c523fa68b146e1b8d2e4df9341` | `6c6619d38b00b6672680ac4ec69a430c7959e1a9` |
| `205d6c8601997415a34b3159b6a7e027dffe8559` | `87a3133b94202480d737057bc3043684ab24c57a` |
| `f46ff91ef572589daeeb67b453ea4e8714a68a26` | `437ff10c43cf830bd3cf91e762f33b78cd1a45ad` |

The old baseline references identify the original validation history. The security repair is a separate child commit, not part of this metadata-only rewrite. A local backup ref retains the old published tip.

---

### Guides Multi Tenancy (`docs/guides/multi-tenancy.md`)

---
title: Multi-Tenancy
description: Isolate sandbox workloads across Kubernetes namespaces with per-tenant API keys and automatic namespace routing.
---

# Multi-Tenancy

Multi-tenancy enables a single OpenSandbox Server deployment to serve multiple independent consumers, each isolated in its own Kubernetes namespace. Each tenant gets dedicated API key(s), and all sandbox lifecycle operations (create, list, get, delete) are automatically scoped to the tenant's namespace.

## Requirements

- `opensandbox-server` >= 0.2.2
- Kubernetes runtime (`runtime.type = "kubernetes"` in `server.toml`)
- One Kubernetes namespace per tenant, **pre-created by a cluster admin**
- Default Helm chart RBAC (already includes ClusterRole with cross-namespace access)

::: tip Proxy routes in multi-tenant mode
Unlike single-tenant mode, proxy routes require `OPEN-SANDBOX-API-KEY` when multi-tenancy is enabled. Ensure clients include the header for proxy requests.
:::

::: warning Docker not supported
Multi-tenancy requires Kubernetes namespace isolation. If `runtime.type = "docker"` is configured and tenants are enabled, the server refuses to start with a fatal error.
:::

## How It Works

```
Request with OPEN-SANDBOX-API-KEY header
       │
       ├── Tenant provider configured?
       │       ├── YES → lookup key in tenant provider
       │       │       ├── found  → inject tenant context, route to tenant namespace
       │       │       └── not found → 401 Unauthorized
       │       └── NO  → validate against server.api_key (single-tenant legacy)
```

When multi-tenancy is active:

1. Auth middleware resolves the API key to a `TenantEntry` via the configured provider.
2. The tenant's namespace is injected into a request-scoped `ContextVar`.
3. Sandbox lifecycle operations (create/list/get/delete) use the resolved namespace.
4. List operations only return sandboxes within the authenticated tenant's namespace.
5. Proxy routes (`/sandboxes/{id}/proxy/...`) also require `OPEN-SANDBOX-API-KEY` in multi-tenant mode.
6. Create stamps `opensandbox.io/tenant=<name>` on the workload. When several tenants share one namespace, list/get/delete/proxy hide other tenants' labeled sandboxes (`404`). Unlabeled legacy sandboxes stay visible until they expire.

## Configuration

Multi-tenancy is enabled by adding a `[tenants]` section to `server.toml`. Two provider backends are available: **file** (local TOML) and **http** (remote endpoint).

### File Provider (default)

Add to `server.toml`:

```toml
[tenants]
provider = "file"
```

Then create `tenants.toml` at one of these paths (checked in order):

1. `$SANDBOX_TENANTS_CONFIG_PATH` environment variable
2. `~/.opensandbox/tenants.toml`

Example `tenants.toml`:

```toml
[[tenants]]
name = "team-alpha"
namespace = "sandbox-alpha"
api_keys = ["sk-alpha-key-1", "sk-alpha-key-2"]

[[tenants]]
name = "team-beta"
namespace = "sandbox-beta"
api_keys = ["sk-beta-key-1"]
```

Each tenant entry requires:

| Field | Description |
|-------|-------------|
| `name` | Unique human-readable tenant identifier |
| `namespace` | Target Kubernetes namespace (must exist at server startup) |
| `api_keys` | One or more API keys for this tenant (supports key rotation) |

Constraints:
- API keys must be unique across all tenants (duplicate → startup error).
- Every tenant must have at least one key.
- `server.api_key` in `server.toml` must be removed when using `[tenants]`.

### HTTP Provider

For environments where tenants are managed by an external IAM or tenant management system:

```toml
[tenants]
provider = "http"
endpoint = "https://iam.internal/opensandbox/tenant-lookup"
max_stale_seconds = 300.0
timeout_seconds = 5.0
auth_header = "X-Internal-Auth"
auth_token = "bearer-token-here"
```

| Field | Default | Description |
|-------|---------|-------------|
| `provider` | `"file"` | `"file"` or `"http"` |
| `endpoint` | — | Required for HTTP. URL the server GETs to resolve a key. |
| `max_stale_seconds` | `300.0` | Max seconds to serve stale cache when endpoint is unreachable. |
| `timeout_seconds` | `5.0` | HTTP request timeout. |
| `auth_header` | — | Optional header name for provider-level auth. |
| `auth_token` | — | Optional token value for that header. |

**HTTP endpoint contract:**

Request:
```
GET {endpoint}
Header: OPEN-SANDBOX-API-KEY: <api_key>
```

Response (200):
```json
{
  "namespace": "sandbox-alpha",
  "ttl": 60
}
```

Response (401):
```json
{
  "code": "UNAUTHORIZED",
  "message": "Invalid API key"
}
```

The HTTP provider caches results per key using the server-suggested `ttl`. On TTL expiry it re-fetches synchronously. If the endpoint is unreachable, stale entries are served up to `max_stale_seconds`, after which requests fail with 503.

::: warning HTTP provider skips startup namespace validation
The HTTP provider resolves tenants per API key and cannot enumerate all tenants at startup, so the fail-fast namespace check is skipped for it (a warning is logged instead). The file provider, which loads the full `tenants.toml` at startup, still fails fast when any tenant namespace is missing or inaccessible. With the HTTP provider, ensure namespaces exist and are accessible before issuing tenant API keys.
:::

## Namespace Setup

Before onboarding a tenant, the cluster admin must prepare the target namespace.

### 1. Create namespace

```bash
kubectl create namespace sandbox-alpha
```

### 2. ResourceQuota (recommended)

```yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-quota
  namespace: sandbox-alpha
spec:
  hard:
    requests.cpu: "8"
    requests.memory: 16Gi
    limits.cpu: "16"
    limits.memory: 32Gi
    pods: "20"
```

#### Quota exhaustion behavior

When the namespace quota is exhausted, `POST /v1/sandboxes` fails fast with `403 KUBERNETES::QUOTA_EXCEEDED` instead of waiting for the sandbox-creation timeout and returning `KUBERNETES::POD_READY_TIMEOUT`:

- **Count quotas** (`count/sandboxes.agents.x-k8s.io`): the Sandbox create is rejected by Kubernetes admission immediately.
- **Compute quotas** (`requests.cpu`, `requests.memory`, `pods`): the Sandbox is created, but its Pod cannot be admitted; the server surfaces the rejection from the workload status (`ReconcilerError`) as soon as the controller records it (typically within a second).

The response body uses the standard `ErrorResponse` shape, and `message` carries the Kubernetes admission details, e.g. `exceeded quota: tenant-quota, requested: requests.cpu=1, used: requests.cpu=8, limited: requests.cpu=8`. SDK clients surface this as a typed error with HTTP status 403. Raise the quota or free capacity, then retry — no server restart is needed.

This fail-fast contract currently applies to the `agent-sandbox` workload provider (`[kubernetes] workload_provider = "agent-sandbox"`). With the default `batchsandbox` provider, the controller does not yet propagate admission rejections onto the CR status, so quota exhaustion still waits for the sandbox creation timeout and returns `KUBERNETES::POD_READY_TIMEOUT`.

### 3. LimitRange (recommended)

```yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: tenant-limits
  namespace: sandbox-alpha
spec:
  limits:
  - default:
      cpu: "1"
      memory: 2Gi
    defaultRequest:
      cpu: 250m
      memory: 512Mi
    type: Container
```

The `Container` defaults apply to every container that omits the corresponding resource setting, including the OpenSandbox egress sidecar. Configure smaller, sidecar-specific values in the lifecycle server so egress containers do not inherit defaults intended for sandbox workloads.

### 4. Egress sidecar resources (recommended)

Add Kubernetes resource settings to the server's `[egress]` configuration:

```toml
[egress]
image = "opensandbox/egress:v1.1.7"
requests = { cpu = "25m", memory = "64Mi" }
limits = { cpu = "250m", memory = "256Mi" }
```

Requests and limits can be configured independently. These values are a starting point for basic DNS/nft enforcement; benchmark your workload and allow more headroom when using Credential Vault or transparent mitmproxy. If your `LimitRange` defines minimum values, the egress settings must satisfy them.

### 5. Server RBAC

The default Helm chart (`opensandbox-server`) already deploys a ClusterRole + ClusterRoleBinding with cross-namespace access. No additional RBAC changes are needed for multi-tenancy — the server ServiceAccount can already operate in any namespace.

## Hot Reload

### File provider

The file provider watches `tenants.toml` for changes (polls every 2 seconds). Changes take effect without server restart:

- **New key added** → immediately valid on next request.
- **Key removed** → immediately invalid (401).
- **Parse error during reload** → previous config retained, warning logged.
- **File deleted** → all tenant keys invalidated.

The watcher monitors the parent directory to handle Kubernetes ConfigMap atomic symlink swaps.

### HTTP provider

No explicit reload needed — cache entries expire per their `ttl` and are re-fetched on next lookup.

## Key Rotation

Multi-tenancy supports multiple API keys per tenant specifically for zero-downtime key rotation:

1. Add the new key to `tenants.toml` (or issue it via HTTP provider).
2. Wait for hot-reload (file) or TTL expiry (HTTP).
3. Update clients to use the new key.
4. Remove the old key from config.

Both old and new keys are valid simultaneously during the transition window.

## Migration from Single-Tenant

1. Create target namespace(s) for your tenant(s).
2. Write `tenants.toml` with your existing API key as a tenant entry, pointing to the existing namespace:
   ```toml
   [[tenants]]
   name = "default"
   namespace = "your-existing-namespace"
   api_keys = ["your-existing-api-key"]
   ```
3. Add `[tenants]` section to `server.toml`:
   ```toml
   [tenants]
   provider = "file"
   ```
4. Remove `api_key` from `[server]` in `server.toml`.
5. Deploy. The existing key continues working, now as a tenant key.
6. Add more tenants as needed.

**Rollback:** Remove the `[tenants]` section from `server.toml`, restore `server.api_key`, restart.

## Isolation Model

The server itself does not enforce resource quotas or network policies. Isolation is delegated to Kubernetes:

| Dimension | Kubernetes Mechanism | Scope |
|-----------|---------------------|-------|
| Resource quota | `ResourceQuota` | Per-namespace CPU, memory, storage |
| Default limits | `LimitRange` | Per-namespace default container resources |
| Network isolation | Egress sidecar | Per-sandbox outbound policy via egress proxy |
| Sandbox count | `ResourceQuota` (pod count) | Per-namespace pod limit |
| RBAC | `RoleBinding` | Per-namespace API access |

::: info Pool APIs are not tenant-scoped
Pool management routes (`/pools`) currently operate in the server's configured default namespace, not the authenticated tenant's namespace. Pools are shared resources. If you need per-tenant pool isolation, create separate server deployments or wait for future per-tenant pool support.
:::

## Troubleshooting

| Symptom | Cause | Fix |
|---------|-------|-----|
| Server exits at startup: "Multi-tenancy requires Kubernetes namespaces" | `runtime.type = "docker"` with `[tenants]` configured | Use Kubernetes runtime or remove `[tenants]` |
| Server exits: "Remove server.api_key from server.toml" | Both `server.api_key` and `[tenants]` present | Remove `api_key` from `[server]` section |
| Server exits: namespace not found | Tenant namespace doesn't exist | `kubectl create namespace <ns>` before starting server |
| 401 on valid key after config change | Hot-reload hasn't picked up change yet | Wait 2s (file) or TTL seconds (HTTP) |
| 503 TENANT_PROVIDER_UNAVAILABLE | HTTP endpoint unreachable and cache expired past `max_stale_seconds` | Fix endpoint connectivity; increase `max_stale_seconds` |
| Duplicate api_key error at startup | Same key assigned to multiple tenants | Ensure each key is unique across all tenants |

---

### Guides Network Isolation (`docs/guides/network-isolation.md`)

---
title: Network Isolation on Kubernetes
description: Why Kubernetes NetworkPolicy is insufficient for sandbox isolation and how OpenSandbox uses egress sidecar controls instead.
---

# Network Isolation on Kubernetes

## Problem

On a Kubernetes cluster, each OpenSandbox sandbox runs as an independent Pod with a dedicated Pod IP assigned by the CNI plugin. By default, any sandbox can reach other sandboxes in the same cluster directly via Pod IP. This introduces several security risks:

- **Internal network scanning**: malicious code inside a sandbox can scan the cluster Pod CIDR range to discover other sandboxes.
- **Unauthorized access**: an attacker can connect to other sandboxes' listening ports directly, bypassing OpenSandbox's authentication and authorization mechanisms.
- **Data leakage**: when sandboxes from different tenants co-exist in the same cluster, the lack of isolation may lead to unauthorized cross-tenant data access.

A mechanism is needed to prevent direct IP-based communication between sandboxes while preserving legitimate external access paths (via OpenSandbox Ingress).

## Native Kubernetes NetworkPolicy

Kubernetes NetworkPolicy controls traffic through **Pod label selectors**. It faces fundamental limitations in the sandbox isolation scenario:

- **Unpredictable labels**: sandbox Pod labels are injected automatically by the platform and are not user-controllable. Sandboxes from different tenants or security levels may share the same label set, making it impossible to define precise isolation boundaries with label selectors.
- **Dynamic lifecycle**: sandboxes are created and destroyed frequently. NetworkPolicy is a static declaration and cannot track the expected isolation relationships of each sandbox in real time.
- **Granularity mismatch**: NetworkPolicy operates on Pod sets (namespace + label selector), while sandbox isolation requires each sandbox to be an independent security domain that denies access from all other sandboxes by default. Expressing "deny access from all other Pods to me" with NetworkPolicy would require a separate rule per sandbox, and cannot cover sandboxes created in the future.
- **No outbound control**: NetworkPolicy Ingress rules can block inbound traffic, but cannot prevent sandbox processes from initiating outbound connections (e.g., curling another sandbox's Pod IP). Blocking outbound direct connections requires bidirectional (Ingress + Egress) policies per sandbox, which runs into the same label unpredictability and dynamicity problems.

Therefore, OpenSandbox does not rely on native Kubernetes NetworkPolicy. Instead, it uses **sandbox-level egress control** to enforce sandbox-to-sandbox isolation. The control point lives inside the sandbox (egress sidecar), not at the cluster network layer.

## Approach 1: Global Enforced Isolation via Egress Sidecar `deny.always`

### How It Works

The OpenSandbox egress sidecar provides a `deny.always` mechanism: place a file at `/var/egress/rules/deny.always` inside the sidecar container, and the rules declared in it take effect **unconditionally**, with higher priority than any allow rules set by users via the SDK or API.

The implementation is in `components/egress/pkg/policy/always_rules.go`:

```go
func MergeAlwaysOverlay(user *NetworkPolicy, alwaysDeny, alwaysAllow []EgressRule) *NetworkPolicy {
    // alwaysDeny is prepended so it matches first, achieving unconditional denial
    merged = append(merged, alwaysDeny...)
    merged = append(merged, alwaysAllow...)
    merged = append(merged, out.Egress...)
}
```

`MergeAlwaysOverlay` is called on every policy change (both initial startup and runtime updates), merging always-deny, always-allow, and user policies by priority. Since rules match in order, `deny.always` rules have the highest priority and cannot be overridden by users through the SDK or API.

Additionally, the `deny.always` file is automatically reloaded every minute, so updates take effect without restarting the container.

### Configuration Steps

#### 1. Determine Cluster Pod CIDR and Service CIDR

Obtain the Pod and Service CIDR ranges of the Kubernetes cluster:

```bash
# Service CIDR: usually set by kube-controller-manager's --service-cluster-ip-range
# Pod CIDR: usually set by the CNI plugin or kube-controller-manager's --pod-network-cidr
```

Common example values:
- Pod CIDR: `10.244.0.0/16`
- Service CIDR: `10.96.0.0/12`

#### 2. Create the `deny.always` Rule File

```text
10.244.0.0/16
10.96.0.0/12
```

Supported target types: IP addresses (e.g., `10.0.0.5`), CIDR ranges (e.g., `10.0.0.0/8`), or domain names (e.g., `internal.service.local`, with `*.` wildcard prefix support).

#### 3. Build a Custom Image Based on the Official Egress Image

Create a `Dockerfile` that embeds the `deny.always` file into the image:

```dockerfile
FROM opensandbox/egress:latest

COPY deny.always /var/egress/rules/deny.always
```

Build and push to a registry accessible by the cluster:

```bash
docker build -t registry.example.com/opensandbox/egress:hardened .
docker push registry.example.com/opensandbox/egress:hardened
```

#### 4. Update the Server Configuration

In the OpenSandbox server configuration, point the `[egress]` `image` to the custom image:

```toml
[egress]
image = "registry.example.com/opensandbox/egress:hardened"
mode = "dns+nft"
```

After this configuration is applied, all newly created sandboxes will use the egress sidecar image that includes the `deny.always` rules. No changes to individual sandbox creation requests are needed.

> Note: the `deny.always` file is hot-reloaded every minute. To change the rules (e.g., after a cluster CIDR change), update the `deny.always` file, rebuild the image, and perform a rolling update of the server configuration.

### Effects

- **Pod IP blocked**: sandboxes cannot directly reach other Pods in the same cluster via Pod IP.
- **Service ClusterIP blocked**: sandboxes cannot reach in-cluster services via Service ClusterIP.
- **Forced access path**: legitimate cross-sandbox communication must go through the `GetEndpoint()` API, which returns an external access endpoint proxied by OpenSandbox Ingress with authentication and authorization.
- **Transparent to users**: users do not need to declare any additional parameters in SDK calls. Isolation is enforced automatically at the platform level.

## Allowing Legitimate In-Cluster Services

When `defaultAction: deny` is enabled, allowing a sandbox to reach a Kubernetes Service requires **both** layers of the egress sidecar to agree:

1. **DNS layer**: allow the service hostname so the DNS proxy does not return `NXDOMAIN`.
2. **Network layer** (`dns+nft` mode): allow the Service CIDR (or a narrower ClusterIP range) so nftables does not drop the resolved TCP connection.

For example, if a sandbox should reach `postgres.opensandbox.svc.cluster.local`, this is not enough by itself:

```json
{ "action": "allow", "target": "postgres.opensandbox.svc.cluster.local" }
```

The request can still fail after DNS resolution if the Service ClusterIP falls inside a denied CIDR such as `10.96.0.0/12`. In `dns+nft` mode you must also allow the Service CIDR (or the specific ClusterIP range you want exposed):

```json
{ "action": "allow", "target": "10.96.0.0/12" }
```

### Recommended Patterns

- **Platform-managed allowlist**: if many sandboxes need the same internal services, keep the Service CIDR allowance in `allow.always` and let users request only the DNS names they should reach.
- **Per-sandbox allowlist**: if access should be tightly scoped, include both the service FQDN and the required Service CIDR in the sandbox's `network_policy` / `networkPolicy`.
- **Avoid assuming CoreDNS exemptions are enough**: the sidecar automatically allows nameserver IPs so DNS forwarding works, but it does **not** automatically allow arbitrary Service ClusterIPs for application traffic.

## Approach 2: On-Demand Isolation via Per-Sandbox NetworkPolicy

If global enforced isolation is not desired, or if specific sandboxes need more permissive access, you can explicitly deny internal network access at sandbox creation time via the `network_policy` parameter:

::: warning Pool mode limitation
Per-request `networkPolicy` is not supported when the lifecycle API creates a sandbox from a pre-warmed Pool via `extensions.poolRef`. Pool pods already exist, so the server cannot inject the egress sidecar for an individual allocation and rejects requests that contain both fields. Configure the required network controls in the Pool pod template before its pods are created, or use a non-pooled sandbox when each request needs its own policy.
:::

```python
from opensandbox import Sandbox, NetworkPolicy, EgressRule

sandbox = await Sandbox.create(
    image="python:3.11",
    network_policy=NetworkPolicy(
        default_action="deny",
        egress=[
            EgressRule(action="deny", target="10.244.0.0/16"),   # Deny Pod CIDR
            EgressRule(action="deny", target="10.96.0.0/12"),   # Deny Service CIDR
            EgressRule(action="allow", target="*.example.com"),  # Allow external domains
        ],
    ),
)
```

### Limitations

- **Per-sandbox declaration**: each sandbox must explicitly pass the `network_policy` parameter at creation. The platform cannot enforce it by default.
- **CIDR exposure**: users need to know the cluster's Pod CIDR and Service CIDR. In multi-tenant scenarios, this is sensitive information that should not be exposed to users.
- **Overridable**: users can declare higher-priority allow rules within the same `network_policy` to override their own deny rules, making this less secure than the `deny.always` enforcement in Approach 1.

## Comparison

| Dimension | Global `deny.always` | Per-Sandbox NetworkPolicy |
|-----------|---------------------|---------------------------|
| Enforcement | Yes (user cannot override) | No (user can modify policy) |
| User awareness | Transparent (platform-level) | Requires explicit declaration |
| Operational cost | Low (one-time image build, global effect) | High (declare per sandbox) |
| Cluster CIDR exposure | Not exposed to users | Must be exposed to users |
| Use case | Platform-wide default isolation, recommended | Whitelist mode, fine-grained control |

## Runtime Compatibility

Both Approach 1 (`deny.always` via egress sidecar) and Approach 2 (per-sandbox `network_policy`) depend on the egress sidecar, which uses an iptables `nat` table REDIRECT rule for DNS interception. This works with `runc` (default) and all Kata Containers variants (`kata-qemu`, `kata-clh`, `kata-fc`), but **not with gVisor** — gVisor's netstack does not implement the `nat` table.

If you need both gVisor's syscall isolation and FQDN egress control:
- Use `kata-qemu` instead — it provides comparable security isolation and supports the egress sidecar.
- Alternatively, use a CNI-level FQDN policy (e.g., Cilium `toFQDNs`) for network isolation alongside gVisor.

The same architectural constraint applies to transparent service meshes such as Istio/Envoy sidecar injection: OpenSandbox egress expects to own outbound interception inside the pod network namespace. If a mesh sidecar also rewrites outbound traffic in that namespace, egress-sidecar features such as per-sandbox network policy, transparent MITM, and Credential Vault are not currently supported together. Prefer excluding sandbox pods from mesh injection or enforcing outbound policy at the platform network layer instead.

See the [Compatibility Matrix](/guides/secure-container#compatibility-matrix) in the Secure Container Runtime Guide for the full feature support table.

## Recommendations

1. **Default full isolation**: use `deny.always` to block the cluster's internal CIDR ranges as the platform's default security baseline.
2. **Whitelist with `allow.always`**: for scenarios requiring cross-sandbox communication (e.g., an agent dispatching tasks to sandboxes), use the `allow.always` file (`/var/egress/rules/allow.always`) to open specific Pod IPs or Service DNS names. `allow.always` has higher priority than user policies but lower than `deny.always`, giving the platform precise control over the open scope.
3. **External access entry**: the only way for a sandbox to expose services externally should be the `GetEndpoint()` API, proxied through OpenSandbox Ingress. Pod IPs should not be used as external service endpoints.

---

### Guides Node Agent (`docs/guides/node-agent.md`)

---
title: Node Agent configuration
description: Deploy and configure the OpenSandbox Node Agent — enable the DaemonSet, choose a file or OSS sink, opt into syscall collection, and operate it safely.
---

# Node Agent configuration

The [Node Agent](/architecture/data-plane/node-agent) is an optional DaemonSet that collects sandbox stdout/stderr (and, optionally, syscalls) on every Linux node and writes them to durable storage. This guide covers enabling it, choosing a sink, and the operational rules that protect its delivery guarantees. For the full values reference, see the [node-agent chart README](https://github.com/opensandbox-group/OpenSandbox/tree/main/manifests/charts/node-agent).

## Prerequisites

- A Kubernetes cluster from the [OpenSandbox deployment](/deployment/) — the Agent is order-independent, so you can enable it at any time.
- One Agent per Linux node is automatic: the chart pins `kubernetes.io/os: linux`.
- Each node needs local disk for checkpoint state (`hostPaths.state`, default `/var/lib/opensandbox/nodeagent`), which must survive Pod restarts — it is a `hostPath` volume.

The Agent discovers sandbox Pods by identity (`opensandbox.io/id` label, `sandbox` container) and skips pooled warm Pods. No per-Pod configuration is needed.

## Enable the Agent

**Umbrella chart** — set the sub-chart condition:

```bash
helm upgrade opensandbox opensandbox \
  --namespace opensandbox-system \
  --set opensandbox-node-agent.enabled=true
```

**Standalone chart** — install it directly:

```bash
helm install node-agent manifests/charts/node-agent \
  --namespace opensandbox-system \
  --create-namespace
```

Verify the DaemonSet rolled out and the Agent reports ready. Readiness is meaningful: it only turns green when configuration, recovery state, sink target, and host reserves are all valid.

```bash
kubectl rollout status daemonset -l app.kubernetes.io/component=node-agent -n opensandbox-system

kubectl port-forward -n opensandbox-system daemonset/node-agent 8080:8080
curl http://127.0.0.1:8080/readyz   # {"ready":true,"reasons":[]}
```

::: tip Always set a real cluster ID
`config.clusterID` (default `dev-cluster`) names your cluster in every record and object path, and is part of the Agent's target identity. Set it once, before go-live — changing it later is a target change and requires draining (see [Changing sink or cluster ID](#changing-sink-or-cluster-id)).
:::

## Choose a sink

Exactly one sink target per Agent. Records land under `<keyPrefix>/<cluster>/<namespace>/<sandbox_id>/<pod_uid>/` as numbered generations plus a `sandbox.finalized.<revision>.json` completeness marker per stream.

The examples below use the umbrella-chart key prefix (`opensandbox-node-agent.`); if you installed the standalone chart, drop that prefix.

### Local files (default)

```yaml
# values-node-agent.yaml
opensandbox-node-agent:
  enabled: true
  config:
    clusterID: prod-cluster
  sink:
    type: file
    file:
      path: /var/lib/opensandbox/nodeagent-data
      maxBytes: 1073741824      # rotate a generation at 1 GiB
      maxFiles: 16              # per stream
      maxTotalBytes: 10737418240
      retention: 24h            # cleanup after the marker's repair window
```

Keep `sink.file.path` and `hostPaths.fileData` in sync — the chart uses them for the mount and the write target. Retention-driven cleanup runs inside the Agent and only ever removes whole, already-finalized families.

The file sink is for single-node debugging and small clusters. For anything durable across node loss, use OSS.

### Alibaba Cloud OSS

Create the credentials first. Collection credentials must be able to append, read, and write — **never delete**. Deletion happens through a separate offline tool (below), with its own credentials.

```bash
kubectl create secret generic nodeagent-oss-credentials \
  --namespace opensandbox-system \
  --from-literal=access-key-id=... \
  --from-literal=access-key-secret=...        # optional: --from-literal=session-token=...
```

```yaml
opensandbox-node-agent:
  enabled: true
  config:
    clusterID: prod-cluster
  sink:
    type: oss
    oss:
      endpoint: https://oss-cn-zhangjiakou.aliyuncs.com   # HTTPS origin required
      bucket: my-sandbox-logs
      keyPrefix: logs
      existingSecret: nodeagent-oss-credentials
```

The bucket must satisfy the Agent's integrity assumptions — startup fails permanently otherwise:

- versioning disabled, no WORM retention policy
- no lifecycle rule whose prefix overlaps `<keyPrefix>/<clusterID>/` (lifecycle deletion would corrupt the marker guarantees)

Objects are written with appendable writes and sealed into immutable generations; markers are written with overwrite forbidden, so consumers can verify them by size and checksum. Because collection credentials cannot delete, cleanup is an explicit, marker-aware offline operation — run [`nodeagent-oss-cleanup`](https://github.com/opensandbox-group/OpenSandbox/tree/main/components/nodeagent/cmd/oss-cleanup) from an operator machine with delete-capable credentials. It plans before it deletes and requires an explicit target-drain confirmation.

## Opt into syscall collection

The default source set is `container-logs` only. Adding `syscalls` enables an embedded eBPF program that records one event per syscall made by each sandbox:

```yaml
opensandbox-node-agent:
  config:
    sources:
      - container-logs
      - syscalls
```

The chart handles the Kubernetes plumbing automatically when `syscalls` is present: it mounts the host cgroup v2 hierarchy and tracefs read-only and adds the `BPF` and `PERFMON` capabilities. The nodes themselves must provide:

- Linux kernel 5.11 or newer
- cgroup v2 unified hierarchy
- tracefs mounted at `/sys/kernel/tracing`

Syscall records land in their own `<cluster>/_streams/syscall/…` tree and carry their own markers. Expect the syscall marker to be `incomplete` by design in some cases — for example, the tracer attaches only after Kubernetes reports the container ID, so the interval before attachment is reported as a gap rather than claimed as covered.

## Key values

| Value | Default | What it controls |
|---|---|---|
| `config.sources` | `["container-logs"]` | Enabled sources; the image must contain every named source |
| `config.clusterID` | `dev-cluster` | Cluster name in records, paths, and target identity |
| `config.memoryBudgetBytes` | 256 MiB | Global queue budget for buffered records |
| `config.perSandboxQueueBytes` | 16 MiB | Per-sandbox queue budget before backpressure |
| `config.perSandboxRateLimit` | `0` (off) | Per-sandbox records rate limit |
| `config.dropPolicy` | `block` | `block` applies backpressure; `drop` discards and accounts for it in markers |
| `config.sinkTimeout` / `config.retryMaxInterval` | 30s / 30s | Sink write timeout and retry backoff cap |
| `config.stateDir` + `hostPaths.state` | `/var/lib/opensandbox/nodeagent` | Checkpoint state location (keep the two in sync) |
| `config.maxLineBytes` | 1 MiB | Longest log line; longer lines are dropped and accounted |
| `config.endedStateRetention` | 24h | How long a finished stream can still accept late data before its state is pruned |
| `resources` | 50m/128Mi – 1/512Mi | DaemonSet resources (experimental defaults — benchmark before production sizing) |

The defaults follow the safe posture: `block` backpressure means a slow sink slows collection instead of losing data. Only switch to `drop` if you understand that dropped records are gone — they are reported in the stream marker, but not recoverable.

## Metrics

The Agent exports OTLP metrics (`opensandbox.nodeagent.*`: records, bytes, drops, retries, queue depth, sink latency) with no sandbox or Pod identifiers as labels. It requires an explicit endpoint — the node-IP fallback used by other components is disabled:

```yaml
opensandbox-node-agent:
  extraEnv:
    - name: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
      value: http://otel-collector.observability:4318/v1/metrics
```

See [telemetry](/guides/telemetry#node-agent) for the metric list and [shared exporter behavior](/guides/telemetry#shared-otlp-configuration-for-go-components).

## Operating rules

These protect the delivery guarantee — violating them turns streams `incomplete` or leaves the Agent not-ready (which is the intended, visible outcome):

- **Never remove or rotate the state directory.** It holds checkpoints and finalize intents, not payloads. Deleting it mid-stream forfeits recovery.
- **Do not rotate or delete the file sink's output out-of-band.** The Agent owns its layout and cleanup.
- **Readiness is diagnostic.** `kubectl get daemonset` + `curl /readyz` gives named reasons (`invalid-config`, `state-unavailable`, `operation-retrying`, `store-stale`, …). An unready Agent never silently drops data.
- **Expect a restart to be honest, not lossless.** After an Agent restart, affected markers report `incomplete` — the guarantee is at-least-once within the provable coverage window, and markers never claim more.

### Changing sink or cluster ID

The target identity is a digest of the sink configuration plus cluster ID. If you change `sink.*` or `config.clusterID`, the Agent refuses to reuse old state — by design, so it cannot append one cluster's records onto another target. To change targets safely:

1. Stop collection (scale the DaemonSet to zero) and let streams finalize.
2. Drain the old target (for OSS, complete the offline cleanup/drain confirmation).
3. Update values and redeploy.

---

### Guides Pause Resume (`docs/guides/pause-resume.md`)

---
title: Pause & Resume
description: Pause sandbox state to an OCI image and resume from snapshot on Kubernetes.
---

# Pause and Resume Guide

This guide explains how to use the pause and resume features for Kubernetes-backed sandboxes in OpenSandbox. Pause commits the sandbox's root filesystem as an OCI image and releases cluster resources. Resume restores the sandbox from that image.

## Table of Contents

- [Overview](#overview)
- [Architecture](#architecture)
- [Prerequisites](#prerequisites)
- [Controller Configuration](#controller-configuration)
- [Registry and Secret Setup](#registry-and-secret-setup)
- [Usage Guide](#usage-guide)
- [Administrator Guide](#administrator-guide)
- [SandboxSnapshot Reference](#sandboxsnapshot-reference)
- [Troubleshooting](#troubleshooting)

---

## Overview

### What Pause and Resume Does

| | Behavior |
|--|---------|
| **Pause** | Creates an internal `SandboxSnapshot`, commits the running container root filesystem as an OCI image, then quiesces the sandbox runtime and releases Pods / pooled allocations |
| **Resume** | Reuses the same `BatchSandbox`, rewrites its template to the latest snapshot image, and recreates the runtime from that image |
| **sandboxId** | Stable across pause/resume cycles — callers use the same ID throughout the sandbox lifetime |
| **Replica support** | Currently limited to `BatchSandbox.spec.replicas=1`. Server-created Kubernetes sandboxes use `replicas: 1`; direct CRs with another replica count are rejected by the controller pause entry. |

### Key Design Principle

**Controller-level configuration**: Registry URL and push/pull secrets are configured on the Kubernetes controller manager, not in `~/.sandbox.toml`. SDK users and API callers require **no code changes** to use pause/resume — they just call `pause` and `resume` on the existing sandbox ID.

### Lifecycle

```text
Time ---------------------------------------------------------------->

Sandbox lifecycle:   [Running]--[Pausing]--[Paused]--[Resuming]--[Running]
                         |                     |
                  commit rootfs          rewrite template images
                  push to registry       recreate runtime from snapshot
                  release pods/alloc
```

### State Machine Details

The sandbox transitions through both stable and intermediate states:

| State | Type | Description |
|-------|------|-------------|
| `Running` | Stable | Sandbox is active and processing requests |
| `Pausing` | Intermediate | Pause operation in progress. Snapshot commit is coordinated through an internal `SandboxSnapshot` resource. |
| `Paused` | Stable | Sandbox is paused, the latest rootfs snapshot is ready, and runtime Pods / pooled allocations have been released |
| `Resuming` | Intermediate | Resume operation in progress. The controller is rewriting the sandbox template to the latest snapshot image and recreating the runtime |
| `Failed` | Stable | Operation failed (check `reason` and `message` for details) |

The Lifecycle API exposes only the coarse-grained sandbox states above. For detailed snapshot progress, inspect the internal `SandboxSnapshot` resource:

Pod termination can continue after the controller reports `Paused`. A resume request can be submitted during this interval; the controller waits for the old Pod to be removed before recreating its replacement. Pods with a deletion timestamp do not contribute new runtime failure conditions, including terminal exit statuses reported by Kubernetes during deletion. Failures already recorded on the sandbox remain terminal, and failures of replacement Pods are still reported normally.

- `Pending`: snapshot request accepted, waiting to resolve source Pod / create commit Job
- `Committing`: commit Job is running and pushing snapshot images
- `Succeed`: snapshot is ready and can be used for the next resume
- `Failed`: snapshot creation failed

### What Is Preserved

| | Preserved? |
|--|-----------|
| Root filesystem contents | ✅ Yes — committed as OCI image |
| Environment variables | ✅ Yes — from BatchSandbox template |
| Running processes / memory | Rootfs mode: no. Opt-in QEMU-in-runc mode: the QEMU process and Guest memory are restored; other outer processes restart. See [QEMU VMState Snapshots](/guides/qemu-vmstate-snapshots). |
| Explicit volume mounts | Depends on volume type |
| Credential Vault entries | No - stored only in egress sidecar memory; re-inject from a trusted control plane after resume |

Pause/resume is currently single-replica only. The internal pause snapshot records one source Pod's container images and does not store per-replica state, so the Kubernetes controller rejects pause requests unless `BatchSandbox.spec.replicas=1`.

See [Credential Vault](/guides/credential-vault#persistence-across-pause-and-resume) for the required post-resume credential re-injection procedure.

### Durability and abnormal termination

The root filesystem is durable only after the snapshot reaches `Succeed` and the OCI image has been pushed to the configured registry. A pause request being accepted, or a snapshot being in `Pending` or `Committing`, does not provide a durability guarantee.

Changes made after the last successful snapshot remain in the container's writable layer. They are not automatically snapshotted when the process exits, the container is OOM-killed, the Pod enters `Failed`, the Pod is evicted, or the node becomes unavailable. The lifecycle API does not currently expose a post-failure snapshot or recovery operation, and a new container created by a Kubernetes restart does not inherit the old writable layer.

The image-committer can sometimes read a stopped container directly from containerd while its metadata and writable snapshot still exist on the source node. This is a best-effort operator recovery path, not a supported durability contract: the normal snapshot API requires a `Running` sandbox, and containerd or kubelet garbage collection, Pod replacement, node loss, or a reused container identity can make the old layer unavailable or ambiguous.

For data that must survive an unexpected termination, store it in a persistent volume or an external service and take explicit root filesystem snapshots for reproducible environment state. Files in mounted volumes follow the persistence and backup behavior of that volume; they are not included in the OCI rootfs snapshot.

---

## Architecture

```
API caller
    │ POST /v1/sandboxes/{id}/pause
    ▼
OpenSandbox Server
    │ PATCH BatchSandbox.spec.pause=true
    ▼
BatchSandbox Controller (Kubernetes)
    │ validates lifecycle state
    │ creates internal SandboxSnapshot CR
    ▼
SandboxSnapshot Controller
    │ resolves running Pod
    │ creates commit Job on the same node
    ▼
commit Job Pod (image-committer)
    │ containerd API: commit container rootfs → OCI image
    │ OCI registry resolver: push image
    ▼
SandboxSnapshot.status.phase = Succeed
    │ BatchSandbox.status.phase = Paused
    │ deletes Pods or releases pooled allocation
    ▼
Cluster resources released

--- Later: resume ---

API caller
    │ POST /v1/sandboxes/{id}/resume
    ▼
OpenSandbox Server
    │ PATCH BatchSandbox.spec.pause=false
    ▼
BatchSandbox Controller
    │ reads internal SandboxSnapshot
    │ rewrites pod template images from snapshot
    │ clears poolRef for pooled sandboxes
    │ recreates runtime Pods
    ▼
Sandbox running again with restored filesystem
```

---

## Prerequisites

1. **Kubernetes cluster** with the OpenSandbox controller deployed
2. **OCI-compatible container registry** accessible from cluster nodes (push) and the Kubernetes API (pull)
3. **Kubernetes Secrets** of type `kubernetes.io/dockerconfigjson` for registry authentication
4. **Controller manager** configured with snapshot registry and secret flags

---

## Controller Configuration

Configure the controller manager deployment with snapshot flags:

```yaml
- --snapshot-registry=registry.example.com/sandboxes
- --snapshot-registry-insecure=false
- --snapshot-push-secret=registry-snapshot-push-secret
- --resume-pull-secret=registry-pull-secret
```

### Configuration Reference

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `--snapshot-registry` | string | `""` | **Required.** OCI registry prefix. By default, images are stored as `<registry>/<sandboxName>-<container>:snap-gen<N>`. |
| `--snapshot-image-uri-template` | string | `""` | Go named-field template for snapshot image URIs; empty uses the default naming rule below. |
| `--snapshot-registry-insecure` | bool | `false` | Enables insecure registry mode for snapshot push operations. Use only for HTTP or self-signed local registries. |
| `--snapshot-push-secret` | string | `""` | Kubernetes Secret name for pushing snapshots. Must be `kubernetes.io/dockerconfigjson` type. |
| `--image-committer-pod-template-file` | string | `""` | Path to a PodTemplateSpec overlay for commit Job Pods. |
| `--resume-pull-secret` | string | `""` | Kubernetes Secret name injected into resumed sandboxes for pulling snapshot images. Can be the same as push secret. |
| `--image-committer-image` | string | `"image-committer:dev"` | Image used by commit Jobs. |
| `--commit-job-timeout` | duration | `"10m"` | Timeout for commit Jobs. |

### Helm chart support

The `opensandbox-controller` Helm chart now exposes the snapshot-related controller values directly:

- `controller.snapshot.imageCommitterImage`
- `controller.snapshot.imageCommitterPodTemplate`
- `controller.snapshot.commitJobTimeout`
- `controller.snapshot.registry`
- `controller.snapshot.imageURITemplate`
- `controller.snapshot.registryInsecure`
- `controller.snapshot.snapshotPushSecret`
- `controller.snapshot.resumePullSecret`

For the all-in-one `opensandbox` chart, use the same values under the `opensandbox-controller.*` prefix.

### Startup behavior

The server no longer carries dedicated pause/resume config. Missing registry or secret settings are surfaced by the Kubernetes controllers when a `SandboxSnapshot` is processed, for example as `SandboxSnapshot.status.conditions[type=Failed]` with reasons like `RegistryNotConfigured`.

---

## Registry and Secret Setup

### Step 1: Prepare your registry

Any OCI-compatible registry works (Docker Hub, GitHub Container Registry, Harbor, a private `registry:2` instance, etc.). The registry must be:

- **Reachable from cluster nodes** (for the commit Job to push)
- **Reachable from the Kubernetes API server / kubelet** (for image pull on resume)

### Step 2: Create the push secret

```bash
kubectl create secret docker-registry registry-snapshot-push-secret \
  --docker-server=registry.example.com \
  --docker-username=<username> \
  --docker-password=<password-or-token> \
  --namespace=<sandbox-namespace>
```

### Step 3: Create the pull secret

The pull secret is used by the resumed `BatchSandbox` Pod to pull the snapshot image. It can be the same secret as the push secret if your credentials have both read and write access:

```bash
kubectl create secret docker-registry registry-pull-secret \
  --docker-server=registry.example.com \
  --docker-username=<username> \
  --docker-password=<password-or-token> \
  --namespace=<sandbox-namespace>
```

### Using a private `registry:2` (development)

For development with a cluster-internal `registry:2` deployment:

```bash
# Create a registry deployment
kubectl create deployment docker-registry \
  --image=registry:2 --port=5000

kubectl expose deployment docker-registry --port=5000

# No authentication needed for internal registry
# Leave snapshot push/pull secret flags empty on the controller manager
```

---

## Usage Guide

Once the controller manager is configured and the server is running, pause/resume works through the standard Lifecycle API. No SDK changes are needed.

### Pause a sandbox

```bash
curl -X POST http://localhost:8080/v1/sandboxes/{sandbox_id}/pause \
  -H "Content-Type: application/json"
```

**Response:** `202 Accepted` with an empty body.

The pause is asynchronous. The sandbox transitions through: `running` → `pausing` → `paused`

### Check pause status

```bash
curl http://localhost:8080/v1/sandboxes/{sandbox_id}
```

When `status` is `paused`, the filesystem has been committed and cluster resources have been released.

### Resume a sandbox

```bash
curl -X POST http://localhost:8080/v1/sandboxes/{sandbox_id}/resume \
  -H "Content-Type: application/json"
```

**Response:** `202 Accepted` with an empty body.

The sandbox transitions through: `paused` → `resuming` → `running`

### Multiple pause/resume cycles

Pause and resume can be repeated. Each pause cycle produces a new snapshot image tag (`snap-gen1`, `snap-gen2`, ...). The latest snapshot is always used for the next resume.

---

## Administrator Guide

### Controller RBAC

The OpenSandbox controller requires the following RBAC permissions for pause/resume (included in the Helm chart and `make manifests` output):

| Resource | Verbs | Purpose |
|----------|-------|---------|
| `sandboxsnapshots` | get, list, watch, create, update, patch, delete | Manage SandboxSnapshot CRs |
| `jobs` / `jobs/status` | full | Create/monitor commit Jobs |
| `secrets` | get | Validate push secret exists before creating commit Job |
| `pods` | get, list, watch | Find running Pod for commit |

### Snapshot image naming

Internal pause/resume snapshot images are named:
```
<snapshot-registry>/<sandboxName>-<containerName>:snap-gen<N>
```

For example, with `--snapshot-registry=registry.example.com/sandboxes`, sandbox `my-sandbox`, container `sandbox`, first pause:
```
registry.example.com/sandboxes/my-sandbox-sandbox:snap-gen1
```

Server-managed public snapshots use the same repository layout but a stable snapshot-id-derived tag:
```
<snapshot-registry>/<sandboxName>-<containerName>:snap-<snapshotIdHex>
```

The controller distinguishes the two modes by owner reference. Pause/resume snapshots are created by the `BatchSandbox` controller and have a controller ownerReference to the owning `BatchSandbox`; public snapshots are created by the Lifecycle server and do not use that ownerReference.

#### Custom image names

Set `--snapshot-image-uri-template` on the controller manager, or `controller.snapshot.imageURITemplate` in the controller Helm chart, to replace the naming rule. An empty value uses this Go `text/template`:

```text
{{.Registry}}/{{.SandboxName}}-{{.ContainerName}}:{{.SnapshotTag}}
```

The template receives these named fields:

| Field | Value |
|-------|-------|
| `Registry` | The required `--snapshot-registry` prefix, including any repository path |
| `Namespace` | The snapshot's Kubernetes namespace |
| `SandboxName` | The source BatchSandbox name |
| `SnapshotName` | The SandboxSnapshot name |
| `SnapshotUID` | The SandboxSnapshot UID |
| `SnapshotCreationTime` | The SandboxSnapshot `metadata.creationTimestamp` as a Go `time.Time` in the controller process's local timezone |
| `ContainerName` | The source container name, or `vmstate` for a QEMU VM-state artifact |
| `SnapshotTag` | The existing `snap-gen<N>` or public snapshot tag described above |
| `ArtifactKind` | `rootfs` for container images, or `vmstate` for a QEMU VM-state artifact |

For example, use one repository with a distinct tag for each snapshot artifact:

```yaml
controller:
  snapshot:
    registry: registry.example.com/sandboxes
    imageURITemplate: '{{.Registry}}/snapshots:{{.SnapshotUID}}-{{.ArtifactKind}}-{{.ContainerName}}'
```

To group images by the snapshot creation date in the controller's local timezone while retaining the sandbox, container, and snapshot tag:

```yaml
controller:
  snapshot:
    registry: registry.example.com/team
    imageURITemplate: '{{.Registry}}/library/snapshots-{{date "2006-01-02" .SnapshotCreationTime}}:{{.SandboxName}}-{{.ContainerName}}-{{.SnapshotTag}}'
```

The date functions follow Helm's names and argument order and accept a Go `time.Time`, such as `SnapshotCreationTime`:

```text
{{date "2006-01-02" .SnapshotCreationTime}}
{{dateInZone "2006-01-02" .SnapshotCreationTime "UTC"}}
{{dateInZone "2006-01-02-15-04-05" .SnapshotCreationTime "+08:00"}}
{{dateInZone "2006-01-02" .SnapshotCreationTime "America/New_York"}}
```

`date` uses Go's `time.Local`. `dateInZone` accepts `Local` or an empty timezone for the same default, `UTC` or `Z`, a signed `HH:MM` offset (hours 00–23, minutes 00–59), or an IANA `Area/Location` name. Fixed offsets such as `+05:30` do not change with daylight saving time; named zones follow the rules for the snapshot's creation instant. Unknown zones and malformed offsets fail template execution. These helpers reject a zero timestamp instead of substituting the current time.

On Unix, the local timezone comes from `TZ`, or `/etc/localtime` when `TZ` is unset. In Kubernetes this is the controller container's timezone; set its `TZ` environment variable or provide its timezone files to select the intended local zone. The controller embeds timezone data for named zones when the OS has none.

Formatting uses Go's standard layouts: `2006-01-02` for a date, `20060102-150405` for a compact timestamp, and `2006-01-02-15-04-05.000` for milliseconds. Java-style `yyyy-MM-dd` patterns are not supported. Layouts must produce a valid image reference in their template position: spaces, colons, or uppercase month names are unsuitable in a repository name and fail validation.

Native methods remain available: `.SnapshotCreationTime.Format "2006-01-02"` uses the local timezone, while `.SnapshotCreationTime.UTC.Format "2006-01-02"` selects UTC explicitly. Templates written for the earlier UTC-only field should select UTC explicitly to preserve their date when the controller's local zone is not UTC.

The source is always the snapshot creation timestamp, so retries after midnight keep the same date with the same timezone configuration. Existing commit Jobs retain their resolved targets when the template or timezone setting changes.

For the umbrella chart, nest these values under `opensandbox-controller`. Helm passes the template literally; it does not evaluate the Go template fields. For Kustomize or a direct Deployment, add an argument to the manager container:

```yaml
- '--snapshot-image-uri-template={{.Registry}}/snapshots:{{.SnapshotUID}}-{{.ArtifactKind}}-{{.ContainerName}}'
```

The controller renders the URI before creating the commit Job. The image committer pushes directly to that URI, and restore uses the recorded reference; there is no intermediate push or rename. QEMU rootfs and VM-state artifacts use the same template and original snapshot tag.

Templates must produce valid tagged OCI image references without a digest. Custom template results are normalized to fully qualified references before they are recorded and passed to the image committer: `snapshots:tag` becomes `docker.io/library/snapshots:tag`, and `team/snapshots:tag` becomes `docker.io/team/snapshots:tag`. Include an explicit registry host (a hostname containing a dot, a host with a port, or `localhost`) to target another registry. An empty template keeps the original naming rule and registry prefix unchanged. Names are not automatically sanitized or truncated. Include fields that keep artifacts and snapshots distinct; the controller rejects duplicate targets within a snapshot, but cannot detect collisions with other snapshots. Template syntax errors prevent controller startup; rendering errors, invalid references, and duplicate targets fail the snapshot before a commit Job is created. Changes apply when creating new commit Jobs; existing Jobs keep their resolved targets, and completed snapshots retain their recorded restore references.

### Runtime compatibility

The built-in `rootfs-v1` image committer requires a container runtime that exposes compatible containerd task pause/resume and writable-snapshot APIs. gVisor RuntimeClasses whose handler is `runsc` do not currently satisfy that contract. The Lifecycle server rejects public snapshot creation for those workloads before it persists a snapshot record or creates a `SandboxSnapshot` resource, leaving the running sandbox untouched.

Native gVisor checkpoint/restore requires a dedicated snapshot backend and is not provided by the `rootfs-v1` committer.

### Commit Job

The controller creates a short-lived Kubernetes `Job` for each pause:

- **Job name**: `<snapshotName>-commit`
- **Node affinity**: Runs on the **same node** as the source Pod (containerd socket access required)
- **Timeout**: 10 minutes (`ActiveDeadlineSeconds`)
- **TTL**: 5 minutes after completion (`TTLSecondsAfterFinished`)
- **Image**: `image-committer` (configurable via controller `--image-committer-image` flag)

The commit Job mounts the host containerd socket from the source node and runs as UID 0. This gives the `image-committer` image node-level container runtime access. Use only a trusted image, preferably pinned by digest or controlled by an admission policy.

The built-in image committer uses containerd APIs directly for container lookup, task pause/unpause, writable-snapshot image creation, and registry push. Before committing, it verifies that the node's content store contains every base-image blob for the node platform and fetches missing blobs from the original image digest; this avoids snapshot failures after CRI discards compressed content during image unpack. Source registries use HTTPS by default. For a trusted source registry that requires skipped TLS verification or plain HTTP, inject `SOURCE_IMAGE_REGISTRY_INSECURE=true` into the `commit` container through the image-committer Pod template. The reusable Go contracts and default implementations are exposed in [`pkg/imagecommitter`](https://github.com/opensandbox-group/OpenSandbox/tree/main/kubernetes/pkg/imagecommitter); provider-specific binaries can supply a credential provider and reuse the standard CLI contract. The Job also retains the host `/run/containerd/fifo` mount so compatible implementations can use containerd task exec. Any preparation command used by the built-in implementation is best effort and does not change the commit interface.

The operator-controlled Pod template can add labels, annotations, a ServiceAccount, scheduling settings, init containers, sidecars, and settings on the required `commit` container such as resources, `env`, and `envFrom`. The template is the merge base, and controller-generated invariants override conflicting template values. The controller reserves the source `nodeName`, restart policy, committer image, image pull policy, command, arguments, security context, termination-message settings, and required environment variables, mounts, and volumes. Environment variables, mounts, and volumes are merged by name, with required controller values winning conflicts. Additional regular sidecars must terminate for the Job to complete.

Any ServiceAccount or admission configuration referenced by the template must exist in every sandbox namespace. Unpause Jobs do not receive the commit Pod template because they do not access the registry.

If the commit Job fails, the controller creates a best-effort `<snapshotName>-unpause` Job on the same node to unpause any source containers that may have been left paused by an abrupt committer exit.

Deleting a `SandboxSnapshot` cleans up Kubernetes commit/unpause Jobs, but does not delete pushed OCI images from the registry. Repeated pause cycles create tags such as `snap-gen<N>`; configure registry retention or garbage collection externally.

### Monitoring

Check SandboxSnapshot status:

```bash
kubectl get sandboxsnapshot -n <namespace>
# NAME          PHASE       SANDBOX_ID     AGE
# my-snapshot   Succeed     my-sandbox     5m

kubectl describe sandboxsnapshot my-snapshot -n <namespace>
```

Key fields to watch:

- `status.phase`: `Pending` → `Committing` → `Succeed` / `Failed`
- `status.conditions`: readiness or failure reasons with human-readable messages
- `status.containers`: image URIs for each committed container
- `status.sourcePodName` / `status.sourceNodeName`: resolved execution source for the snapshot

#### Monitoring Sandbox State Transitions

When checking sandbox state via the Lifecycle API, you'll see intermediate states:

**Pause flow:**
```bash
curl http://localhost:8080/v1/sandboxes/{sandbox_id}
# Response during pause:
{
  "id": "my-sandbox",
  "status": {
    "state": "Pausing",
    "reason": "PAUSING",
    "message": "Pausing sandbox"
  }
}
```

**Resume flow:**
```bash
curl http://localhost:8080/v1/sandboxes/{sandbox_id}
# Response during resume:
{
  "id": "my-sandbox",
  "status": {
    "state": "Resuming",
    "reason": "RESUMING",
    "message": "Resuming sandbox"
  }
}
```

---

## SandboxSnapshot Reference

### Spec fields

| Field | Type | Description |
|-------|------|-------------|
| `sandboxName` | string | Target `BatchSandbox` name in the same namespace |

### Status fields (set by Controller)

| Field | Type | Description |
|-------|------|-------------|
| `phase` | string | `Pending` / `Committing` / `Succeed` / `Failed` |
| `conditions` | list | `Ready` / `Failed` conditions with reason and message |
| `sourcePodName` | string | Pod name used for commit |
| `sourceNodeName` | string | Node where commit Job runs |
| `containers` | list | `{containerName, imageUri, imageDigest}` per container. `imageDigest` is the image config digest, preserving the image ID semantics of earlier image-committer versions. |
| `observedGeneration` | int | Last processed spec generation |

---

## Troubleshooting

### 1. Snapshot stuck in `Failed` — push secret not found

**Cause**: The controller manager was configured with a `--snapshot-push-secret` that does not exist in the sandbox namespace.

**Solution**:
```bash
kubectl get secret registry-snapshot-push-secret -n <namespace>
# If missing:
kubectl create secret docker-registry registry-snapshot-push-secret \
  --docker-server=<registry> \
  --docker-username=<user> \
  --docker-password=<token> \
  -n <namespace>
```

The controller validates secret existence **before** creating the commit Job (fail-fast). Once the secret is created, trigger a new pause cycle.

---

### 2. Snapshot stuck in `Committing` for a long time

**Check the commit Job and its Pod:**

```bash
kubectl get job -n <namespace> -l sandbox.opensandbox.io/snapshot=<snapshotName>
kubectl describe pod <commit-pod-name> -n <namespace>
```

**Common causes:**

| Symptom | Cause | Solution |
|---------|-------|---------|
| `ContainerCreating` for >30s | Secret missing or wrong type | Re-create secret as `kubernetes.io/dockerconfigjson` |
| `FailedMount` event | Secret not found | See issue #1 above |
| Pod running but job never completes | Registry unreachable from node | Check network connectivity from node to registry |
| `unauthorized` in Pod logs | Wrong credentials in secret | Verify secret content with `kubectl get secret ... -o yaml` |

---

### 3. Wrong secret type

Docker registry secrets **must** be type `kubernetes.io/dockerconfigjson`. Generic secrets (`Opaque`) will cause a `FailedMount` error.

```bash
# Check secret type
kubectl get secret registry-snapshot-push-secret -o jsonpath='{.type}'
# Expected: kubernetes.io/dockerconfigjson

# If wrong type, delete and recreate:
kubectl delete secret registry-snapshot-push-secret
kubectl create secret docker-registry registry-snapshot-push-secret \
  --docker-server=<registry> \
  --docker-username=<user> \
  --docker-password=<token>
```

---

### 4. Registry unreachable (`Committing` → `Failed` after timeout)

**Symptoms**: Commit Job Pod starts, runs for a while, then fails with a push error.

**Check:**

```bash
# Inspect commit Pod logs
kubectl logs <commit-pod-name> -n <namespace>

# Test registry connectivity from a node
kubectl run registry-test --rm -it --image=alpine -- \
  wget -O- https://<registry>/v2/ --timeout=5
```

**Common causes:**
- Registry behind a firewall not accessible from cluster nodes
- Self-signed TLS certificate not trusted by containerd
- Wrong registry URL (http vs https)

---

### 5. Resume accepted but the runtime Pod fails to start

**Cause**: The snapshot image cannot be pulled.

```bash
kubectl describe pod <resumed-pod-name> -n <namespace>
# Look for: ErrImagePull or ImagePullBackOff
```

**Check:**
- `--resume-pull-secret` is correctly configured and the Secret exists in the namespace
- The registry is accessible from the node pulling the image
- The snapshot image was successfully pushed during pause (check `status.containers`)

---

### 6. SandboxSnapshot not being processed (no status)

**Cause**: The OpenSandbox controller is not running.

```bash
kubectl get pods -n opensandbox-system
kubectl logs -n opensandbox-system deployment/opensandbox-controller-manager
```

---

## Getting Help

- **Documentation**: [OpenSandbox GitHub](https://github.com/opensandbox-group/OpenSandbox)
- **Issues**: [GitHub Issues](https://github.com/opensandbox-group/OpenSandbox/issues)
- **Kubernetes controller**: [Kubernetes Overview](/architecture/control-plane/operator)

## Measure resume latency

See [Kubernetes Resume Benchmark](/guides/kubernetes-resume-benchmark) for an opt-in rootfs benchmark with raw samples, P50/P95, failure reporting, and explicit cache conditions.

---

### Guides Pod Recovery (`docs/guides/pod-recovery.md`)

---
title: Pod Provision Failure Recovery
description: How the Kubernetes controller automatically replaces BatchSandbox pods stuck in provisioning failures (such as image pull) during initial startup.
---

# Pod Provision Failure Recovery

When a BatchSandbox pod lands on an unhealthy node, provisioning can stall before the pod ever becomes Ready. The most common class is a main container image pull that fails persistently — for example `ImagePullBackOff` caused by a full node disk:

```text
failed to prepare extraction snapshot ...: no space left on device
```

The sandbox stays `Pending` forever: kubelet keeps retrying on the same broken node, and nothing recovers the sandbox. The controller replaces such pods automatically during initial startup, so the scheduler gets a chance to place the recreated pod on a healthy node.

## Behavior

The controller watches sandboxes that have **never been Ready** (`Phase` is `Pending` or empty). A pod is considered stuck when it stays in one of the tracked failure conditions longer than the threshold. Today's tracked conditions:

- **Image pull failures** — `ImagePullBackOff` / `ErrImagePull`, typically caused by a broken node (for example a full disk).
- **Kubelet admission rejections** — the scheduler placed the pod, but kubelet's local resource ledger disagreed (insufficient CPU, memory, or ephemeral storage; node cordoned or not ready; node-pressure eviction). These pods land in `Failed` with reasons like `OutOfcpu`, `OutOfmemory`, `OutOfephemeral-storage`, `Evicted`, `NodeNotSchedulable`, `KubeletNotReady`, or `UnexpectedAdmissionError`, and Kubernetes never retries them on its own. The tracked rejection reason set is configurable via `pod-recovery-admission-reasons` (see [Configuration](#configuration)).

For a stuck pod:

1. The controller deletes it. The sandbox recreates the pod from its template and the pod is rescheduled like any new pod.
2. Replacement attempts are bounded: after `pod-recovery-max-attempts` replacements, the controller stops and records a `PodRecoveryLimitReached` warning event. The sandbox stays `Pending` and needs operator attention.
3. Updating the sandbox template (for example, fixing the image) resets the replacement attempts.

::: tip Intentionally excluded
`CreateContainerConfigError` is a spec problem that rescheduling cannot fix. Terminal failures (`PodFailed`, `ContainerFailed`, `InitContainerFailed`) keep their existing freeze semantics. `CrashLoopBackOff` is not covered: moving a crashing pod rarely helps and can mask application bugs. Scheduling failures (`Unschedulable`) are left to the kube-scheduler, which retries them automatically. Pool-mode sandboxes have a different lifecycle and are out of scope.
:::

### Broken images

An image that cannot be pulled **from any node** (missing tag or manifest, repository does not exist, or denied access) fails identically after every replacement. The controller recognizes these cases and does **not** waste replacement attempts: the pod stays, and an `ImagePullPermanentFailure` warning event names the image error. Fix the image or credentials; once fixed, the pod recovers on its next pull attempt without a restart.

::: warning Interplay with upper-layer Pending watchdogs
If an upper-layer manager tears down sandboxes stuck in `Pending` after its own timeout (for example ~7.5 minutes), note that with the default settings (`1m` threshold, 3 attempts) the controller finishes its recovery attempts within roughly 4 minutes — before such a watchdog fires. Raising `pod-recovery-stuck-threshold` above about a third of the watchdog timeout means replacement attempts may still be in flight when the sandbox gets torn down.
:::

::: warning Large images and cold pulls
A low threshold replaces pods whose image pull simply takes longer than the threshold. While kubelet is actively pulling, the container shows `ContainerCreating` (not counted); only explicit pull failures advance the timer. If your clusters pull very large images from slow registries, raise `pod-recovery-stuck-threshold`.
:::

## Configuration

Feature configuration lives in the `feature-flags` ConfigMap in the controller's namespace (plain `data` key-value entries). Changes are hot-reloaded without restarting the controller:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: feature-flags
  namespace: opensandbox-system  # the controller's namespace
data:
  # How long a pod must stay stuck in a tracked failure condition during
  # initial startup before it is replaced. Defaults to 1m.
  pod-recovery-stuck-threshold: "1m"
  # Maximum number of stuck-pod replacements per BatchSandbox template.
  # Defaults to 3.
  pod-recovery-max-attempts: "3"
  # Optional: comma-separated kubelet admission rejection reasons that
  # replacement can recover. When set, it replaces the built-in set
  # (NodeNotSchedulable, KubeletNotReady, UnexpectedAdmissionError, Evicted);
  # the OutOf* family (OutOfcpu, OutOfmemory, ...) always applies. Useful for
  # vendor-specific kubelet rejection reasons.
  # pod-recovery-admission-reasons: "KubeletNotReady,Evicted"
```

Missing or invalid entries fall back to the built-in defaults (`1m` / `3`), and deleting the ConfigMap restores the defaults.

Helm (controller chart) renders this ConfigMap from values:

```yaml
controller:
  podRecovery:
    stuckThreshold: "1m"
    maxAttempts: 3
    # Optional; leave empty for the built-in admission reasons.
    admissionReasons: ""
```

## Monitoring

Events recorded on the BatchSandbox:

| Event | Type | Meaning |
| --- | --- | --- |
| `ReplacedStuckPod` | Normal | A stuck pod was deleted and will be recreated on another node. |
| `PodRecoveryLimitReached` | Warning | Replacement attempts are exhausted; the sandbox stays `Pending` and needs operator attention. |
| `ImagePullPermanentFailure` | Warning | The image itself cannot be pulled (missing manifest/repository or denied access); fix the image or credentials. |

Prometheus metrics on the controller metrics endpoint:

| Metric | Labels | Description |
| --- | --- | --- |
| `opensandbox_pod_recovery_replacements_total` | `condition` | Stuck pods replaced during initial startup. |
| `opensandbox_pod_recovery_skips_total` | `condition`, `reason` | Stuck pods detected but not replaced; `reason` is `permanent_failure` (broken image) or `budget_exhausted` (operator attention needed). |

To find affected sandboxes:

```bash
kubectl get events --field-selector reason=ReplacedStuckPod,reason=PodRecoveryLimitReached,reason=ImagePullPermanentFailure
```

---

### Guides Qemu Vmstate Snapshots (`docs/guides/qemu-vmstate-snapshots.md`)

---
title: QEMU VMState Snapshots
description: Configure and operate process-level pause and resume for QEMU running inside a runc sandbox.
---

# QEMU VMState Snapshots

This guide explains how to prepare, deploy, and validate a QEMU-in-runc workload that supports process-level pause and resume. The workload runs QEMU inside a normal runc container; it does not use a virtualized Kubernetes `RuntimeClass`.

This mode is experimental. It restores the QEMU Guest memory, vCPU, and migratable device state. Other processes in the outer runc container restart from the `BatchSandbox` Pod template.

The initial `qemu-v1` implementation supports pause and resume of the same `BatchSandbox`. The public snapshot clone API does not yet restore QEMU VMState because its snapshot record does not persist the complete Pod template and QEMU launch plan. A standalone public snapshot operation resumes its source VM after publishing the artifacts.

## State and artifact model

| State | Storage |
|---|---|
| Outer container rootfs | Existing rootfs snapshot image |
| Writable guest qcow2 overlay | Rootfs snapshot image; the overlay must not be under a Kubernetes volume mount |
| Guest RAM, vCPU, and emulated device state | VMState image containing a zstd-compressed QEMU migration stream |
| Immutable guest base image | Original image layer or an independently available read-only volume |
| PVC, hostPath, cloud disk, network peer state | Not copied by OpenSandbox |

Both images are pushed to the configured image Registry and recorded by manifest digest in one `SandboxSnapshot.status`. Resume never relies on mutable tags.

## Prepare the QEMU workload image

QEMU support is a contract between the workload image and OpenSandbox. The image must:

1. start QEMU with a QMP Unix socket;
2. generate an OpenSandbox launch manifest from the effective QEMU settings;
3. keep each writable Guest overlay captured by v1 in the container rootfs;
4. recognize the OpenSandbox restore environment and add QEMU `-incoming`;
5. become Ready only after the QMP socket and Guest service are available;
6. for an internal BatchSandbox pause snapshot, ensure that workload-controlled processes do not independently resume the source VM via QMP (e.g. `cont`) or restart the source QEMU process after migration export and before the source Pod is deleted (this does not restrict the snapshot worker from resuming the source on failure, or during standalone public snapshots).

### Declare the Pod template contract

Set these annotations on the Pod template that creates the QEMU container. For a standalone sandbox, use `BatchSandbox.spec.template.metadata`. For a pooled sandbox, use `Pool.spec.template.metadata` so the allocated Pod carries the contract.

```yaml
annotations:
  sandbox.opensandbox.io/checkpoint-provider: qemu
  sandbox.opensandbox.io/qemu-container: qemu
  sandbox.opensandbox.io/qemu-qmp-socket: /run/qemu/qmp.sock
  sandbox.opensandbox.io/qemu-launch-manifest: /run/qemu/launch.json
  # Optional: constrain restore to compatible nodes.
  sandbox.opensandbox.io/qemu-required-node-class: shenlong-v1
```

| Annotation | Required | Purpose |
|---|---:|---|
| `checkpoint-provider` | Yes | Selects the `qemu` provider. OpenSandbox does not scan process names. |
| `qemu-container` | Yes | Names the Pod container that owns QEMU, QMP, and the launch manifest. |
| `qemu-qmp-socket` | Yes | Clean absolute path of the QMP Unix socket inside that container. |
| `qemu-launch-manifest` | Yes | Clean absolute path of the OpenSandbox launch manifest inside that container. |
| `qemu-required-node-class` | No | Restricts restore to nodes carrying the matching OpenSandbox QEMU node-class label. |

The paths are container paths. They do not need to be mounted into the controller Pod. The snapshot worker reaches the target container through the node container runtime.

### Generate the OpenSandbox launch manifest

The launch manifest is defined by OpenSandbox. It is not a native QEMU file, and QEMU does not create it automatically. The workload image owner is responsible for producing it before the Pod becomes Ready.

Generate the file in the container entrypoint from the same effective values used to build the QEMU command. Do not bake a static file into the Docker image when CPU, memory, disks, or devices can change through environment variables or the Pod template.

Example launch manifest:

```json
{
  "formatVersion": "qemu-v1",
  "architecture": "amd64",
  "qemuVersion": "6.2.0",
  "machineType": "pc-q35-6.2",
  "cpuModel": "host",
  "vcpus": 2,
  "memoryBytes": 536870912,
  "qemuConfigDigest": "sha256:...",
  "disks": [
    {
      "id": "osdisk",
      "overlayPath": "/vm/state.qcow2",
      "capture": "rootfs"
    }
  ]
}
```

| Field | Meaning |
|---|---|
| `formatVersion` | Must be `qemu-v1`. |
| `architecture` | Guest host architecture used by QEMU, for example `amd64`. |
| `qemuVersion` | Version reported by the running QEMU process. The worker verifies this value through QMP. |
| `machineType` | Explicitly versioned machine type, for example `pc-q35-6.2`. |
| `cpuModel` | Effective QEMU CPU model. `host` normally requires homogeneous restore nodes. |
| `vcpus` | Effective vCPU count. |
| `memoryBytes` | Effective Guest RAM size in bytes. |
| `qemuConfigDigest` | Workload-generated SHA-256 identity of compatibility-sensitive QEMU configuration. |
| `disks` | Writable overlays and their capture policy. v1 supports only `capture: rootfs`. |

`qemuConfigDigest` is an opaque compatibility value in v1. Compute it deterministically from a canonical representation of machine, CPU, memory, firmware, disk, network, and device settings. Exclude transient values such as PID, timestamps, QMP paths, and generated socket names. OpenSandbox records the digest but does not reconstruct the QEMU command line from it.

Write the manifest atomically so a concurrent snapshot cannot read a partial JSON file:

```bash
runtime_dir=/run/qemu
manifest_tmp="$runtime_dir/launch.json.tmp"
manifest="$runtime_dir/launch.json"

mkdir -p "$runtime_dir"
cat >"$manifest_tmp" <<EOF
{
  "formatVersion": "qemu-v1",
  "architecture": "amd64",
  "qemuVersion": "$qemu_version",
  "machineType": "$machine_type",
  "cpuModel": "$cpu_model",
  "vcpus": $vcpus,
  "memoryBytes": $memory_bytes,
  "qemuConfigDigest": "$qemu_config_digest",
  "disks": [
    {"id":"osdisk","overlayPath":"/vm/state.qcow2","capture":"rootfs"}
  ]
}
EOF
mv "$manifest_tmp" "$manifest"
```

The reference E2E entrypoint shows the manifest and QEMU arguments being built from one set of variables. See the [reference entrypoint](https://github.com/alibaba/OpenSandbox/blob/main/kubernetes/test/e2e_qemu/testdata/entrypoint.sh).

### Expose QMP and support restore

Create the declared QMP Unix socket when QEMU starts:

```bash
qemu_args+=(
  -qmp "unix:/run/qemu/qmp.sock,server=on,wait=off"
)
```

QMP controls the live QEMU process and transports the migration stream. The launch manifest separately declares compatibility and disk-capture intent that cannot be recovered reliably from QMP alone.

The workload entrypoint must detect `OPENSANDBOX_RESTORE_MODE=qemu-v1` and start QEMU with the loader stream supplied in `OPENSANDBOX_VMSTATE_DIR`:

```bash
if [[ "${OPENSANDBOX_RESTORE_MODE:-}" == "qemu-v1" ]]; then
  test -x "$OPENSANDBOX_VMSTATE_DIR/vmstate-loader"
  qemu_args+=(
    -incoming
    "exec:$OPENSANDBOX_VMSTATE_DIR/vmstate-loader stream --dir $OPENSANDBOX_VMSTATE_DIR"
  )
fi
```

OpenSandbox injects the restore directory and loader; the workload still owns the complete QEMU command line. It must recreate the same machine, CPU, memory, firmware, disks, network, and device topology before consuming the incoming stream.

### Keep the writable overlay in the rootfs

Every disk declared with `capture: rootfs` must be a file in the QEMU container's writable rootfs. It must not resolve under a Kubernetes `volumeMount` or `volumeDevice`, including PVC, hostPath, projected volume, or `emptyDir`. `nerdctl commit` does not capture those mounts, and the snapshot worker rejects the configuration instead of creating an incomplete snapshot.

An immutable base image can come from the original image layer or another independently available read-only source. Existing user init containers run again before the injected VMState loader on resume, so they must be idempotent and must not overwrite the restored writable overlay.

## Pause and resume sequence

1. The controller resolves the annotated QEMU container and schedules the snapshot Job on the same node.
2. The worker validates that every writable overlay declared as `capture: rootfs` is outside the container's Kubernetes volume mount paths.
3. The worker uses QMP migration to export VM state, compresses it with zstd, and puts it into a standard container image.
4. After QEMU reaches post-migration state, the worker freezes companion outer containers (leaving the QEMU container unfrozen to avoid kernel worker freeze deadlocks while QEMU remains quiesced in postmigrate), commits their root filesystems, and pushes both image types.
5. The controller publishes rootfs and VMState manifest digests atomically, then removes the source Pod.
6. On resume, a loader init container restores and verifies the VMState files in an `emptyDir`. It is appended after all user init containers.
7. The recreated QEMU container consumes the migration stream. Other outer-container processes start normally from the Pod template.

`savevm` is not used. It creates an internal snapshot coupled to qcow2 storage and is not the transport for live Guest RAM used here.

## Production deployment with Helm

There is no QEMU-specific Helm feature switch. Helm installs the cluster-level snapshot capability; each `BatchSandbox` or `Pool` Pod template opts in through the annotations above. Rootfs-only workloads continue to use the existing behavior.

### Prerequisites

- Linux nodes with `/dev/kvm` exposed to the QEMU container.
- The image Registry must accept Docker schema 2 or OCI image manifests and be reachable from both snapshot Jobs and kubelet/containerd.
- The QEMU version, versioned machine type, CPU model, vCPU count, RAM size, device configuration digest, and optional node class must match on restore.
- The snapshot Job needs the host containerd runtime directory, host PID namespace, and `SYS_PTRACE`. It already operates inside the node-level trust boundary because it controls the containerd socket. Pod Security and admission policies must explicitly allow this Job.
- VMState can contain credentials and user data from Guest RAM. Apply the same access control, encryption, retention, and deletion policy as sensitive persistent storage.
- Plan Registry capacity from measured compressed payload size. Compression depends on Guest memory contents; it is not guaranteed to be small.

The controller image and image-committer image must both come from a version that supports `qemu-v1`. In particular, do not rely on an older chart default for `controller.snapshot.imageCommitterImage`; override it explicitly.

### Create Registry credentials

The commit Job and resumed Pod run in the sandbox namespace, so the referenced Secrets must exist in every namespace that uses snapshot and restore:

```bash
kubectl -n sandbox-team create secret docker-registry snapshot-registry \
  --docker-server=registry.example.com \
  --docker-username='<username>' \
  --docker-password='<password-or-token>'
```

One Secret can be used for push, image-committer pull, and resume pull when the credential has all three permissions. Production deployments may use separate least-privilege Secrets with the same names in each sandbox namespace.

### Configure and install the chart

Create a values file:

```yaml
controller:
  image:
    repository: registry.example.com/opensandbox/controller
    tag: qemu-v1
  snapshot:
    imageCommitterImage: registry.example.com/opensandbox/image-committer:qemu-v1
    containerdSocketPath: /var/run/containerd/containerd.sock
    commitJobTimeout: 15m
    registry: registry.example.com/opensandbox-snapshots
    registryInsecure: false
    snapshotPushSecret: snapshot-registry
    imageCommitterPullSecret: snapshot-registry
    resumePullSecret: snapshot-registry
```

Install or upgrade the controller. On a fresh cluster, install the base chart first — the controller chart no longer ships the CRDs:

```bash
helm upgrade --install base ./manifests/charts/base

helm upgrade --install opensandbox-controller \
  ./manifests/charts/controller \
  --namespace opensandbox-system \
  --create-namespace \
  --values qemu-snapshot-values.yaml
```

Sync the CRDs into the base chart (`make -C manifests helm-gen-crds`) and upgrade `base` so the updated `SandboxSnapshot` CRD lands. Verify the deployed capability before creating workloads:

```bash
kubectl -n opensandbox-system rollout status \
  deployment/opensandbox-controller-manager
kubectl -n opensandbox-system get deployment,pod
kubectl get crd sandboxsnapshots.sandbox.opensandbox.io
kubectl get crd sandboxsnapshots.sandbox.opensandbox.io \
  -o jsonpath='{.spec.versions[0].schema.openAPIV3Schema.properties.status.properties.virtualMachine.type}{"\n"}'
```

Confirm that cluster admission permits the node-trusted snapshot Job. QEMU mode requires the host PID namespace, `SYS_PTRACE`, and the host containerd runtime directory. The QEMU workload itself needs `/dev/kvm` and normally runs privileged unless the platform supplies narrower device permissions.

For heterogeneous fleets, label compatible restore nodes and declare the matching class on the workload:

```bash
kubectl label node <node> \
  sandbox.opensandbox.io/qemu-node-class=shenlong-v1
```

```yaml
metadata:
  annotations:
    sandbox.opensandbox.io/qemu-required-node-class: shenlong-v1
```

By default, snapshot image names are generated below the configured Registry prefix. A QEMU sandbox produces normal container rootfs images such as `<prefix>/<sandbox>-<container>:<tag>` and a VMState image such as `<prefix>/<sandbox>-vmstate:<tag>`. Both artifact names can be customized with [`--snapshot-image-uri-template`](/guides/pause-resume#custom-image-names); VM-state artifacts receive `ContainerName` and `ArtifactKind` equal to `vmstate`. Resume resolves and uses their immutable manifest digests.

## Validation

The dedicated E2E allocates a warm QEMU-in-runc Pod from a `Pool`, writes independent values into an anonymous mmap, the raw Guest disk, and the outer rootfs, snapshots both images, and restores a standalone Pod. It verifies Pool replenishment and detachment, immutable rootfs and VMState image digests, loader completion, snapshot cleanup, all three values, the Guest boot ID, and the live counter:

```bash
cd kubernetes
make test-e2e-qemu
```

The test requires Linux amd64, Docker, Kind, kubectl, and `/dev/kvm`. By default it deploys an isolated `registry:2` Pod in the Kind cluster. Set `QEMU_E2E_SNAPSHOT_REGISTRY` and `QEMU_E2E_DOCKER_CONFIG` to verify the same flow against an authenticated external Registry; the two repositories are `<prefix>/qemu-rootfs` and `<prefix>/qemu-vmstate`. Set `KEEP_QEMU_E2E_CLUSTER=true` to retain the dedicated Kind cluster for diagnostics. See the [E2E README](https://github.com/alibaba/OpenSandbox/tree/main/kubernetes/test/e2e_qemu) for all overrides.

### Reusable Kind validation environment

The E2E harness can build the images, create a dedicated KVM-enabled Kind cluster, deploy the controller and Registry, run the reference Pool flow, and leave the environment available for manual checks:

```bash
cd kubernetes
KEEP_QEMU_E2E_CLUSTER=true make test-e2e-qemu
```

The resulting context is `kind-opensandbox-qemu-vmstate-e2e`. The controller runs in `opensandbox-system`, while the in-cluster `registry:2` Pod and the automated test objects run in `qemu-vmstate-e2e`:

```bash
kubectl --context kind-opensandbox-qemu-vmstate-e2e \
  -n opensandbox-system get deploy,pod
kubectl --context kind-opensandbox-qemu-vmstate-e2e \
  -n qemu-vmstate-e2e get pod,service,pool,batchsandbox
```

To exercise an authenticated external Registry instead, provide its repository prefix and a local Docker `config.json`; the harness creates the temporary Kubernetes pull/push Secret without printing its contents:

```bash
QEMU_E2E_SNAPSHOT_REGISTRY=registry.example.com/team \
QEMU_E2E_DOCKER_CONFIG=/path/to/config.json \
KEEP_QEMU_E2E_CLUSTER=true \
make test-e2e-qemu
```

The commands below use a separate `qemu-manual` namespace, so the retained E2E objects do not need to be deleted first.

### Manual validation in a prepared Kind cluster

This flow deliberately leaves lifecycle actions to the operator. It assumes the controller, snapshot Registry, image-committer image, and demo QEMU image are already installed in a Kind cluster with `/dev/kvm`. From the repository root, select that cluster and create only the standalone `BatchSandbox`:

```bash
export OSB_QEMU_CONTEXT=kind-opensandbox-qemu-vmstate-e2e
export OSB_QEMU_NAMESPACE=qemu-manual
export OSB_QEMU_SANDBOX=qemu-standalone
export OSB_QEMU_POD=qemu-standalone-0

test -c /dev/kvm
kubectl --context "$OSB_QEMU_CONTEXT" get nodes
kubectl --context "$OSB_QEMU_CONTEXT" -n opensandbox-system \
  rollout status deployment/opensandbox-controller-manager
kubectl --context "$OSB_QEMU_CONTEXT" apply \
  -f kubernetes/config/samples/alibaba/qemu-vmstate/standalone.yaml
kubectl --context "$OSB_QEMU_CONTEXT" -n "$OSB_QEMU_NAMESPACE" \
  wait --for=condition=Ready pod/"$OSB_QEMU_POD" --timeout=180s
```

Before pausing, verify the exact contract that the snapshot worker will use:

```bash
kubectl --context "$OSB_QEMU_CONTEXT" -n "$OSB_QEMU_NAMESPACE" \
  get pod "$OSB_QEMU_POD" \
  -o jsonpath='{.metadata.annotations}{"\n"}'
kubectl --context "$OSB_QEMU_CONTEXT" -n "$OSB_QEMU_NAMESPACE" \
  exec "$OSB_QEMU_POD" -c qemu -- \
  test -S /run/qemu-e2e/qmp.sock
kubectl --context "$OSB_QEMU_CONTEXT" -n "$OSB_QEMU_NAMESPACE" \
  exec "$OSB_QEMU_POD" -c qemu -- \
  cat /run/qemu-e2e/launch.json
```

Do not continue if the annotated container, socket path, manifest path, or effective QEMU settings disagree.

The demo Guest runs the HTTP server and the mutable memory map in the same PID 1 process. QEMU user networking forwards the outer container's loopback port `18080` to Guest port `8080`; the service is not exposed on the Pod IP.

| Endpoint | Purpose |
| --- | --- |
| `GET /healthz` | Verify that the resumed Guest process is serving requests |
| `PUT /value` | Store a value of up to 512 bytes in the process memory map |
| `GET /value` | Read the current memory-map value |
| `GET /status` | Read PID, Guest boot ID, mmap-backed live counter, and value |
| `PUT /disk` | Store a marker in the raw writable Guest disk |
| `GET /disk` | Read the raw Guest disk marker |

Define a phase waiter and put three independent markers in Guest memory, the Guest disk, and the outer container rootfs:

```
/* Detailed source-code truncated for AI context efficiency. */
```

In another terminal, watch the resources involved in pause and resume:

```bash
watch -n 1 kubectl --context "$OSB_QEMU_CONTEXT" \
  -n "$OSB_QEMU_NAMESPACE" get batchsandbox,sandboxsnapshot,pod,job
```

The initial manifest intentionally omits `spec.pause`: setting it to `false` on a brand-new object is interpreted as an explicit resume request. Pause the sandbox by adding `spec.pause: true`, then inspect the two immutable image artifacts:

```bash
kubectl --context "$OSB_QEMU_CONTEXT" -n "$OSB_QEMU_NAMESPACE" \
  edit batchsandbox "$OSB_QEMU_SANDBOX"
wait_for_sandbox_phase Paused

export OSB_SNAPSHOT_NAME="$OSB_QEMU_SANDBOX-pause"
kubectl --context "$OSB_QEMU_CONTEXT" -n "$OSB_QEMU_NAMESPACE" \
  get sandboxsnapshot "$OSB_SNAPSHOT_NAME" -o yaml
kubectl --context "$OSB_QEMU_CONTEXT" -n "$OSB_QEMU_NAMESPACE" \
  wait --for=delete pod/"$OSB_QEMU_POD" --timeout=120s
```

The snapshot must report `status.format: qemu-v1`, a rootfs image digest under `status.containers`, and a different VMState image digest plus a non-zero compressed size under `status.virtualMachine`.

Resume by editing `spec.pause` to `false`, then verify process-level continuity and both filesystem layers:

```
/* Detailed source-code truncated for AI context efficiency. */
```

All assertions must return zero. The Pod UID must change, the Guest boot ID must remain the same, the live counter must increase, all three tokens must match, and the VMState loader init container must report `Completed`. The internal `SandboxSnapshot` is deleted after a successful resume. Remove only the test object when finished; the Kind infrastructure remains available:

```bash
kubectl --context "$OSB_QEMU_CONTEXT" delete \
  -f kubernetes/config/samples/alibaba/qemu-vmstate/standalone.yaml
```

### Manual validation with a Pool

Create the Pool first and wait for its warm QEMU Pod. Then create a `BatchSandbox` that allocates that Pod:

```bash
kubectl --context "$OSB_QEMU_CONTEXT" apply \
  -f kubernetes/config/samples/alibaba/qemu-vmstate/pool.yaml
kubectl --context "$OSB_QEMU_CONTEXT" -n "$OSB_QEMU_NAMESPACE" \
  get pool qemu-pool -w
```

After the Pool reports ready capacity, stop the watch and run:

```bash
kubectl --context "$OSB_QEMU_CONTEXT" apply \
  -f kubernetes/config/samples/alibaba/qemu-vmstate/pooled-sandbox.yaml
kubectl --context "$OSB_QEMU_CONTEXT" -n "$OSB_QEMU_NAMESPACE" \
  get batchsandbox qemu-pooled -w
```

Read the allocated Pod name from the allocation annotation and write the same three markers used by the standalone case:

```bash
export OSB_POOLED_POD=$(kubectl --context "$OSB_QEMU_CONTEXT" \
  -n "$OSB_QEMU_NAMESPACE" get batchsandbox qemu-pooled \
  -o jsonpath='{.metadata.annotations.sandbox\.opensandbox\.io/alloc-status}' \
  | jq -r '.pods[0]')

export OSB_POOL_MEMORY_TOKEN="POOL-MMAP-$(date -u +%Y%m%dT%H%M%SZ)"
export OSB_POOL_DISK_TOKEN="POOL-DISK-$(date -u +%Y%m%dT%H%M%SZ)"
export OSB_POOL_ROOTFS_TOKEN="POOL-ROOTFS-$(date -u +%Y%m%dT%H%M%SZ)"

kubectl --context "$OSB_QEMU_CONTEXT" -n "$OSB_QEMU_NAMESPACE" \
  exec "$OSB_POOLED_POD" -c qemu -- \
  curl --fail --silent --show-error --request PUT \
  --data-binary "$OSB_POOL_MEMORY_TOKEN" http://127.0.0.1:18080/value
kubectl --context "$OSB_QEMU_CONTEXT" -n "$OSB_QEMU_NAMESPACE" \
  exec "$OSB_POOLED_POD" -c qemu -- \
  curl --fail --silent --show-error --request PUT \
  --data-binary "$OSB_POOL_DISK_TOKEN" http://127.0.0.1:18080/disk
kubectl --context "$OSB_QEMU_CONTEXT" -n "$OSB_QEMU_NAMESPACE" \
  exec "$OSB_POOLED_POD" -c qemu -- sh -c \
  'mkdir -p /var/lib/opensandbox && printf "%s" "$1" > /var/lib/opensandbox/rootfs-marker' \
  sh "$OSB_POOL_ROOTFS_TOKEN"
```

Run `kubectl edit batchsandbox qemu-pooled -n qemu-manual` and add `spec.pause: true`. On successful pause, verify the Pool-specific handoff:

```bash
kubectl --context "$OSB_QEMU_CONTEXT" -n "$OSB_QEMU_NAMESPACE" \
  get batchsandbox qemu-pooled \
  -o jsonpath='phase={.status.phase}{" poolRef="}{.spec.poolRef}{" templateContainers="}{.spec.template.spec.containers[*].name}{"\n"}'
kubectl --context "$OSB_QEMU_CONTEXT" -n "$OSB_QEMU_NAMESPACE" \
  get sandboxsnapshot qemu-pooled-pause -o yaml
```

The phase must be `Paused`. The controller materializes the allocated Pod's template into the `BatchSandbox` and clears `spec.poolRef`; this detach is intentional, so resume creates an independent Pod instead of returning to the Pool. Edit `spec.pause` back to `false`, wait for `qemu-pooled-0` to become Ready, then call `GET /status`, `GET /disk`, and the rootfs marker check against it.

Clean up the Pool example without deleting the Kind cluster:

```bash
kubectl --context "$OSB_QEMU_CONTEXT" delete \
  -f kubernetes/config/samples/alibaba/qemu-vmstate/pooled-sandbox.yaml
kubectl --context "$OSB_QEMU_CONTEXT" delete \
  -f kubernetes/config/samples/alibaba/qemu-vmstate/pool.yaml
```

## Troubleshooting

| Symptom | Check |
|---|---|
| `InvalidCheckpointContract` | Confirm all four required annotations are on the actual Pod template and name an existing container. Paths must be clean and absolute. |
| Launch manifest copy or decode failure | Exec into the annotated container, read the exact path, and verify that the entrypoint writes complete JSON before readiness succeeds. |
| QMP probe failure | Verify that the declared path is a Unix socket and that QEMU uses `server=on,wait=off`. Check whether a supervisor removed or replaced the socket. |
| QEMU version mismatch | Compare `qemuVersion` in the manifest with the running binary. Generate the manifest at container startup instead of baking a stale version into the image. |
| Writable disk rejected | Check every `volumeMount` and `volumeDevice` on the QEMU container. A `capture: rootfs` overlay cannot live below any mounted path. |
| Snapshot Job rejected by admission | Permit the image-committer Job identity to use host PID, `SYS_PTRACE`, and the host containerd runtime directory on snapshot-capable nodes. |
| Snapshot Job `ImagePullBackOff` | Ensure `imageCommitterPullSecret` exists in the sandbox namespace and can pull the configured image-committer image. |
| Resumed Pod `ImagePullBackOff` | Ensure `resumePullSecret` exists in the sandbox namespace and can pull both rootfs and VMState image repositories. |
| QEMU exits while consuming `-incoming` | Compare QEMU version, machine type, CPU model, vCPU count, memory, firmware, disks, network, and device topology with the captured compatibility data. |
| Pod cannot schedule after resume | Check `/dev/kvm`, node affinity, and `qemu-required-node-class` against the node's `qemu-node-class` label. |

Start diagnosis from the snapshot status and commit Job logs:

```bash
kubectl -n <namespace> get sandboxsnapshot <name> -o yaml
kubectl -n <namespace> get job,pod \
  -l sandbox.opensandbox.io/sandbox-snapshot-name=<name>
kubectl -n <namespace> logs job/<commit-job-name> --all-containers
```

---

### Guides Renew On Access (`docs/guides/renew-on-access.md`)

---
title: Renew-on-Access
description: Keep busy sandboxes alive by extending their expiration automatically on observed access — opt-in via lifecycle.renewalPolicy, driven by server-proxy hits or ingress gateway renew-intents.
---

# Renew-on-Access Guide

This guide shows how to opt sandboxes into **renew-on-access** (OSEP-0009): when the server proxy or the ingress gateway observes traffic for a sandbox, the lifecycle server silently moves the sandbox's expiration forward — a busy sandbox stays alive, an idle one still expires on schedule, and clients never call the renew API themselves.

For the design rationale, see [OSEP-0009](https://github.com/opensandbox-group/OpenSandbox/blob/main/oseps/0009-auto-renew-sandbox-on-ingress-access.md). For expiring *idle* fast sandboxes instead of busy ones, see [Auto-Pause & Wake-on-Access](/guides/auto-pause-wake).

## How it works in one minute

```text
access    a request hits /sandboxes/{id}/proxy/{port} (server proxy mode)
          and/or is proxied by the ingress gateway (Redis queue mode)
intent    the hit becomes a renew intent, throttled per sandbox
renew     the server checks the gates and moves the expiration to
          max(now + extendSeconds, current expiresAt)
```

Nothing happens unless all three layers agree:

1. **The server operator** enables the renew-intent integration (`[renew_intent]`).
2. **The trigger path** exists: server-proxy hits (built into the server) and/or the ingress gateway publishing intents to Redis.
3. **The sandbox opts in** at creation with `lifecycle.renewalPolicy`.

A sandbox that never declares a `renewalPolicy` is never auto-renewed — omission is exactly today's behavior.

## Step 1 — Enable the integration on the server

In the server's `config.toml`:

```toml
[renew_intent]
enabled = true                  # master switch
min_interval_seconds = 60       # per-sandbox renewal cooldown

[renew_intent.redis]
enabled = true                  # consume ingress renew-intents (queue mode)
dsn = "redis://127.0.0.1:6379/0"
# queue_key = "opensandbox:renew:intent"
# consumer_concurrency = 8
```

`[renew_intent] redis` is only needed for ingress-gateway mode. Server-proxy mode (SDK `useServerProxy`) works with `enabled = true` alone. See [`[renew_intent]` in the server configuration reference](https://github.com/opensandbox-group/OpenSandbox/blob/main/server/configuration.md) for all keys.

## Step 2 — Enable the ingress gateway (queue mode)

```text
--renew-intent-enabled=true
--renew-intent-redis-dsn=redis://127.0.0.1:6379/0
```

| Flag | Default | Purpose |
|---|---|---|
| `--renew-intent-enabled` | `false` | Publish renew intents while proxying |
| `--renew-intent-redis-dsn` | `redis://127.0.0.1:6379/0` | Redis for the intent queue |
| `--renew-intent-queue-key` | `opensandbox:renew:intent` | Redis list key |
| `--renew-intent-queue-max-len` | `0` (uncapped) | Queue length cap |
| `--renew-intent-min-interval` | `60s` | Per-sandbox client-side publish throttle |

Publishing is async and best-effort: it never adds latency to proxied requests, and dropped intents only mean a missed renewal.

## Step 3 — Declare the policy on the sandbox

Pass `lifecycle.renewalPolicy.extendSeconds` (300–86400) in the create request.

::: code-group

```python Python
from datetime import timedelta

from opensandbox import Sandbox
from opensandbox.models import RenewalPolicy, SandboxLifecycle

sandbox = await Sandbox.create(
    "ubuntu:24.04",
    connection_config=config,
    timeout=timedelta(seconds=3600),
    lifecycle=SandboxLifecycle(
        renewal_policy=RenewalPolicy(extend_seconds=3600),
    ),
)
```

```javascript JavaScript
import { Sandbox } from "@alibaba-group/opensandbox";

const sandbox = await Sandbox.create({
  connectionConfig: { domain: "http://127.0.0.1:8080" },
  image: "ubuntu:24.04",
  timeoutSeconds: 3600,
  lifecycle: {
    renewalPolicy: { extendSeconds: 3600 },
  },
});
```

```go Go
ttl := 3600
info, err := opensandbox.CreateSandbox(ctx, cfg, opensandbox.SandboxCreateOptions{
    Image:           "ubuntu:24.04",
    TimeoutSeconds:  &ttl,
    Lifecycle: &opensandbox.SandboxLifecycle{
        RenewalPolicy: &opensandbox.RenewalPolicy{ExtendSeconds: 3600},
    },
})
```

```csharp C#
var sandbox = await OpenSandbox.Sandbox.CreateAsync(new SandboxCreateOptions
{
    Image = "ubuntu:24.04",
    TimeoutSeconds = 3600,
    Lifecycle = new SandboxLifecycle
    {
        RenewalPolicy = new RenewalPolicy { ExtendSeconds = 3600 },
    },
});
```

```kotlin Kotlin
val sandbox = Sandbox.builder()
    .image("ubuntu:24.04")
    .timeout(Duration.ofSeconds(3600))
    .lifecycle(
        SandboxLifecycle.builder()
            .renewalPolicy { extendSeconds(3600) }
            .build()
    )
    .build()
```

:::

Each renewal sets the expiration to `max(now + extendSeconds, current expiresAt)` — renewals never shorten a sandbox's life, and the expiration remains visible via the normal sandbox info / `expiresAt` response field. Manual `POST /sandboxes/{sandboxId}/renew-expiration` keeps working and composes with the policy.

## Backend support

| Backend | `lifecycle.renewalPolicy` | Renewal actually fires |
|---|---|---|
| Docker | accepted (persisted as a container label) | yes |
| Kubernetes (pod) | accepted (persisted as an annotation) | yes, including `extensions.poolRef` pool mode |
| Fast Sandbox (fsb) | **rejected with 400** | no — fsb does not support renew-on-access yet |

The field is control-plane state: it never reaches the guest and may accompany `extensions.poolRef`.

## Deprecated extensions key

Before this field existed, opt-in used the opaque creation key `extensions["access.renew.extend.seconds"]` (a decimal string with the same 300–86400 bounds).

::: warning Deprecation timeline
The extensions key is **deprecated** in favor of `lifecycle.renewalPolicy`. It remains supported throughout the entire **1.x** series and is **removed in 2.0**. New code should use `lifecycle.renewalPolicy`; existing SDKs, scripts, and integrations that still send the extensions key keep working on 1.x.
:::

Transition rules while both are accepted:

- Either input on its own works; both inputs with the same parsed value work.
- Both inputs with **conflicting values are rejected with HTTP 400** — values are compared after parsing, so `"3600"` and `" 3600 "` are equivalent.
- Both inputs normalize to the same persisted metadata (container label / workload annotation), so sandboxes created before a server upgrade keep renewing after the upgrade.

A single request can opt out of renew-intent publishing with the `OpenSandbox-Access-Renew: skip` request header through the ingress gateway.

---

### Guides Secure Access (`docs/guides/secure-access.md`)

---
title: Secure Access
description: Authenticate inbound HTTP and WebSocket traffic to sandbox endpoints using static header tokens or signed short-lived URLs.
---

# Secure Access

Secure Access is OpenSandbox's inbound access control for sandbox endpoints. When enabled on a sandbox, the ingress gateway (and the server proxy path) require every caller to prove they are authorized before HTTP or WebSocket traffic reaches the sandbox.

Its counterparts:

- [Credential Vault](/guides/credential-vault) — protects **outbound** requests.
- [Secure Container Runtime](/guides/secure-container) — isolates the **workload**.

## Requirements

- Kubernetes runtime (Docker rejects `secureAccess: true` with `400`).
- `[ingress] mode = "gateway"`.
- Signed URLs additionally require `[ingress.secure_access]` with at least one signing key.

## How It Works



On the **control plane**, creating a sandbox with `secureAccess: true` and calling `GetEndpoint` yields a per-sandbox opaque `SecureAccessToken`. Passing `?expires=<unix_seconds>` also mints a signed, time-limited routing token using the active key under `[ingress.secure_access]`.

On the **data plane**, the ingress gateway evaluates two mechanisms in a fixed order:

1. **Header present** → constant-time compare against `SecureAccessToken`. Mismatch returns `401` immediately; there is **no** fallback to the signed-URL branch.
2. **Header absent** → parse the signed token, check `now ≤ expires`, verify the signature.

Requests that pass either check are forwarded upstream with the header or signed prefix stripped, so the workload never sees the credential.

## Static Header Token

`GetEndpoint` returns an opaque `SecureAccessToken`. Clients attach it as:

```http
OpenSandbox-Secure-Access: <token>
```

SDKs pick this header up from the endpoint response automatically, so application code usually does not touch it.

## Signed Short-Lived URLs

Mint a signed URL by adding `?expires=<unix_seconds>` (decimal `uint64` epoch seconds, **not** milliseconds):

```
GET /sandboxes/{sandboxId}/endpoints/{port}?expires=<unix_seconds>
```

The returned routing token has the shape:

```
{sandbox_id}-{port}-{expires_b36}-{signature}
```

where `expires_b36` is base-36 seconds and `signature` is 8 hex digits plus the 1-character `key_id` that signed it. The gateway accepts it in three routing modes:

| Mode     | Where the token appears |
|----------|------------------------|
| wildcard | Host: `{token}.<parent>` |
| header   | Value of the sandbox routing header |
| uri      | Path prefix: `/{sandbox_id}/{port}/{expires_b36}/{signature}/…` |

Use signed URLs to hand out shareable, time-limited endpoints without exposing the static token.

## Server Configuration

Signed URLs need signing keys; the static header token does not.

```toml
# ~/.sandbox.toml
[ingress]
mode = "gateway"

[ingress.gateway]
# Wildcard mode requires a wildcard address (e.g. *.sandbox.example.com);
# header and uri modes take a plain host (e.g. sandbox.example.com).
address = "*.sandbox.example.com"

[ingress.gateway.route]
mode = "wildcard"   # or "header" or "uri"

[ingress.secure_access]
active_key = "a"    # 1 char [0-9a-z], must exist in keys

[[ingress.secure_access.keys]]
key_id = "a"
key = "<base64-encoded-secret>"   # raw base64, no "base64:" prefix
```

The `opensandbox-server` Helm chart exposes the same shape under `server.gateway.secureAccess` and wires the keys into both the server and the ingress gateway.

To keep key material out of the config file, the server also accepts the `OPENSANDBOX_SECURE_ACCESS_KEYS` (`a=<base64-secret>[,b=...]`) and `OPENSANDBOX_SECURE_ACCESS_ACTIVE_KEY` (`a`) environment variables, which override the TOML block; the chart's `server.gateway.secureAccess.existingSecret` option sources them from a Secret.

**Key rotation.** Add a new key entry, deploy, then flip `active_key`. Verification loads every configured key, so tokens signed by older keys stay valid until you remove them — keep old entries until the longest possible token lifetime has elapsed.

## Enabling It

Set `secureAccess: true` on create. SDK equivalents: Python `secure_access=True`, JS/TS `secureAccess: true`, C# `SecureAccess = true`.

```bash
curl -X POST http://localhost:8080/v1/sandboxes \
  -H "Content-Type: application/json" \
  -d '{
    "image": {"uri": "python:3.11"},
    "resourceLimits": {"cpu": "500m", "memory": "512Mi"},
    "entrypoint": ["python", "/app/main.py"],
    "secureAccess": true
  }'
```

## Calling a Secured Endpoint

With the static header token:

```bash
# Fetch endpoint; the response carries OpenSandbox-Secure-Access.
curl -s http://localhost:8080/v1/sandboxes/${SANDBOX_ID}/endpoints/8080

curl -H "OpenSandbox-Secure-Access: ${TOKEN}" \
  https://${SANDBOX_ID}-8080.sandbox.example.com/
```

With a signed URL (valid for one hour):

```bash
EXPIRES=$(( $(date +%s) + 3600 ))
curl -s "http://localhost:8080/v1/sandboxes/${SANDBOX_ID}/endpoints/8080?expires=${EXPIRES}"
# → https://{sandbox_id}-8080-{expires_b36}-{signature}.sandbox.example.com/
```

## Errors

| Code | Cause |
|------|-------|
| `400` | `secureAccess: true` on Docker; malformed `expires` or routing token; invalid `expires_b36` / `port` / `signature`. |
| `401` | Header mismatch; signed URL expired; bad signature; unknown key ID; secure access required but no credential presented. |

Omitting `expires` on `GetEndpoint` is not a `400` — it returns the unsigned response, still carrying the static `SecureAccessToken`.

## Notes

- Enforcement covers both ingress gateway and the server proxy path (`/v1/sandboxes/{id}/proxy/...`), HTTP and WebSocket.
- The gateway strips `OpenSandbox-Secure-Access` and any signed URI prefix before forwarding.
- In URI mode, path segments that resemble `expires_b36/signature` are only interpreted as a signed prefix when the sandbox actually has secure access enabled; otherwise the full path is forwarded unchanged.
- Secure Access is independent of the execd access token (`X-EXECD-ACCESS-TOKEN`) and Credential Vault; all can be used together.

## See Also

- [Credential Vault](/guides/credential-vault)
- [Secure Container Runtime](/guides/secure-container)
- [Ingress component](/architecture/network/ingress)

---

### Guides Secure Container (`docs/guides/secure-container.md`)

---
title: Secure Container Runtime
description: Use gVisor, Kata Containers, and Firecracker microVMs for hardware-level sandbox isolation.
---

# Secure Container Runtime Guide

This guide explains how to use secure container runtimes with OpenSandbox to provide hardware-level isolation for executing untrusted AI-generated code.

## Table of Contents

- [Overview](#overview)
- [Server Configuration](#server-configuration)
- [Multiple Runtime Profiles on Kubernetes](#multiple-runtime-profiles-on-kubernetes)
- [Docker Mode](#docker-mode)
- [Kubernetes Mode](#kubernetes-mode)
- [User Guide](#user-guide)
- [Administrator Guide](#administrator-guide)
- [Troubleshooting and Best Practices](#troubleshooting-and-best-practices)

---

## Overview

### What are Secure Container Runtimes?

Secure container runtimes provide stronger isolation than the standard runc runtime used by Docker and containerd. They add additional security layers through different mechanisms:

| Runtime | Isolation Mechanism | Startup Overhead | Memory Overhead | Best For |
|---------|---------------------|------------------|-----------------|----------|
| **runc** (default) | Process-level cgroups | ~0ms | Minimal | Trusted workloads, local development |
| **gVisor** | User-space kernel (syscall interception) | ~10-50ms | ~50MB | General workloads with low overhead |
| **Kata (QEMU)** | Full VM with QEMU hypervisor | ~500ms | ~20-50MB | Maximum compatibility and isolation |
| **Kata (Firecracker)** | MicroVM with Firecracker hypervisor | ~125ms | ~5MB | High density, minimal footprint |
| **Kata (CLH)** | Cloud Hypervisor | ~200ms | ~10-20MB | Balanced performance and isolation |

### Why Use Secure Runtimes?

OpenSandbox is designed to execute untrusted code generated by AI models (Claude, GPT-4, Gemini, etc.). Secure runtimes provide:

1. **Container Escape Protection**: Prevents malicious code from breaking out of the container
2. **Kernel-Level Isolation**: Each sandbox gets its own kernel context
3. **Multi-Tenant Safety**: Different users' sandboxes are strongly isolated
4. **Compliance**: Meets security requirements for regulated industries

### Supported Runtime Types

OpenSandbox supports the following secure runtime types through server-level configuration:

- `"gvisor"` - Google gVisor with runsc
- `"kata"` - Kata Containers with QEMU hypervisor (default)
- `"firecracker"` - Kata Containers with Firecracker hypervisor
- `""` (empty) - Standard runc (default, no secure runtime)

### Key Design Principle

**Server-Level Configuration**: The secure runtime is configured once at the server level by administrators. All sandboxes on that server transparently use the configured runtime. SDK users and API callers require **no code changes**.

---

## Server Configuration

Secure runtimes are configured through the `~/.sandbox.toml` configuration file. The server validates the configured runtime at startup and will refuse to start if the runtime is unavailable.

### Configuration File

Edit `~/.sandbox.toml`:

```toml
[runtime]
type = "docker"  # or "kubernetes"
execd_image = "opensandbox/execd:latest"

# Secure container runtime configuration
# When enabled, ALL sandboxes on this server use the specified runtime
[secure_runtime]
# Runtime type: "", "gvisor", "kata", "firecracker"
type = ""

# Docker mode: OCI runtime name (e.g., "runsc" for gVisor, "kata-runtime" for Kata)
# Required when runtime.type = "docker" and type is not empty
docker_runtime = "runsc"

# Kubernetes mode: RuntimeClass name (e.g., "gvisor", "kata-qemu", "kata-fc")
# Required when runtime.type = "kubernetes" and type is not empty
k8s_runtime_class = "gvisor"
```

### Configuration Examples

#### Example 1: gVisor on Docker

```toml
[runtime]
type = "docker"
execd_image = "opensandbox/execd:latest"

[secure_runtime]
type = "gvisor"
docker_runtime = "runsc"
k8s_runtime_class = "gvisor"
```

#### Example 2: Kata Containers on Kubernetes

```toml
[runtime]
type = "kubernetes"
execd_image = "opensandbox/execd:latest"

[secure_runtime]
type = "kata"
docker_runtime = "kata-runtime"
k8s_runtime_class = "kata-qemu"
```

#### Example 3: Kata + Firecracker on Kubernetes

```toml
[runtime]
type = "kubernetes"
execd_image = "opensandbox/execd:latest"

[secure_runtime]
type = "firecracker"
docker_runtime = ""  # Not supported in Docker mode
k8s_runtime_class = "kata-fc"
```

### Startup Validation

When the server starts, it automatically validates that the configured secure runtime is available:

```bash
$ opensandbox-server
INFO     Validating secure runtime for Docker backend
INFO     Docker OCI runtime 'runsc' is available: {...}
INFO     Application startup complete.
```

If the runtime is not available, the server will refuse to start with a clear error message:

```
ERROR    Configured Docker runtime 'runsc' is not available.
        Available runtimes: runc.
        Please install and configure it in /etc/docker/daemon.json.
```

---

## Multiple Runtime Profiles on Kubernetes

`[secure_runtime]` is a server startup setting, not a sandbox creation field. One Lifecycle Server process therefore applies one runtime profile to every template-mode sandbox it creates. To offer runc and a stronger runtime such as Kata in the same Kubernetes cluster, run one independently addressed Lifecycle Server release per profile and route each client to the appropriate Server API.

| Server profile | API Service | `[secure_runtime]` | Resulting Pod field |
|----------------|-------------|--------------------|---------------------|
| Standard | `opensandbox-runc` | `type = ""` | `runtimeClassName` omitted |
| Isolated | `opensandbox-kata` | `type = "kata"`, `k8s_runtime_class = "kata-qemu"` | `runtimeClassName: kata-qemu` |

Both profiles can use the same OpenSandbox controller and sandbox workload namespace. The RuntimeClass should carry its own scheduling constraints so Kata Pods land only on nodes where Kata is installed.

::: warning Resource names must be unique
The Server chart defaults `fullnameOverride` to `opensandbox-server`, so changing only the Helm release name is not sufficient: two default releases would render the same Deployment, ServiceAccount, RBAC, ConfigMap, and Service names. Set a different `fullnameOverride` for every profile. The standalone ingress-gateway chart also defaults to a fixed `fullnameOverride` (`opensandbox-ingress-gateway`); give each gateway a distinct value, such as `opensandbox-runc-gateway` and `opensandbox-kata-gateway`. Its ClusterRole and ClusterRoleBinding names derive from this value and must be unique across the cluster. Separate control-plane namespaces alone do not prevent those RBAC collisions.
:::

### Deploy two Server profiles

Create the `opensandbox` workload namespace and one control-plane namespace per profile. Create the API-key Secrets in their corresponding control-plane namespaces as described in [Kubernetes Deployment](/deployment/#configure-api-authentication). Use `opensandbox-runc-api-key` in `opensandbox-runc-system` and `opensandbox-kata-api-key` in `opensandbox-kata-system` to match the references below.

Use the values files below for both releases. They replace `configToml` in full, including the `[egress]` image and mode needed for sandbox network policies. The examples use direct sandbox endpoints: clients must be able to reach the sandbox Pod IPs inside the cluster. For external clients, configure a gateway as described below. Match the execd and egress image tags to the checked-out release.

::: code-group

```yaml [values-runc.yaml]
fullnameOverride: opensandbox-runc
namespaceOverride: opensandbox-runc-system

server:
  replicaCount: 1
  gateway:
    enabled: false
  env:
    - name: OPENSANDBOX_SERVER_API_KEY
      valueFrom:
        secretKeyRef:
          name: opensandbox-runc-api-key
          key: api-key

configToml: |
  [server]
  host = "0.0.0.0"
  port = 80
  api_key = ""

  [log]
  level = "INFO"

  [runtime]
  type = "kubernetes"
  execd_image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/execd:release-1.1.1-rc.1"

  [kubernetes]
  namespace = "opensandbox"
  workload_provider = "batchsandbox"

  [egress]
  image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/egress:release-1.1.1-rc.1"
  mode = "dns+nft"

  [secure_runtime]
  type = ""
```

```yaml [values-kata.yaml]
fullnameOverride: opensandbox-kata
namespaceOverride: opensandbox-kata-system

server:
  replicaCount: 1
  gateway:
    enabled: false
  env:
    - name: OPENSANDBOX_SERVER_API_KEY
      valueFrom:
        secretKeyRef:
          name: opensandbox-kata-api-key
          key: api-key

configToml: |
  [server]
  host = "0.0.0.0"
  port = 80
  api_key = ""

  [log]
  level = "INFO"

  [runtime]
  type = "kubernetes"
  execd_image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/execd:release-1.1.1-rc.1"

  [kubernetes]
  namespace = "opensandbox"
  workload_provider = "batchsandbox"

  [egress]
  image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/egress:release-1.1.1-rc.1"
  mode = "dns+nft"

  [secure_runtime]
  type = "kata"
  k8s_runtime_class = "kata-qemu"
```

:::

From an OpenSandbox checkout with the base CRDs and controller already installed, install both profiles using the Server chart at `manifests/charts/server`. The chart supplies its matching Server image tag by default:

```bash
kubectl create namespace opensandbox --dry-run=client -o yaml | kubectl apply -f -

helm upgrade --install opensandbox-runc manifests/charts/server \
  --namespace opensandbox-runc-system \
  --create-namespace \
  --values values-runc.yaml

helm upgrade --install opensandbox-kata manifests/charts/server \
  --namespace opensandbox-kata-system \
  --create-namespace \
  --values values-kata.yaml
```

Expose the two Services under different internal DNS names, hostnames, or API gateway routes. Do not put them behind an unqualified round-robin route: runtime selection is made by choosing the Server endpoint. Keep subsequent get, renew, pause, resume, snapshot, and delete calls on the same profile endpoint that created the sandbox.

### Gateway access

`server.gateway.enabled=true` only makes the Server announce a gateway address; it does not deploy the gateway. Install one standalone `manifests/charts/ingress-gateway` release per profile with the distinct `fullnameOverride` values above. Set `gateway.dataplaneNamespace` to that profile's workload namespace (`opensandbox` in these examples).

Expose each gateway through its own client-reachable host, and set the matching Server's `server.gateway.host` to that host. Match `server.gateway.gatewayRouteMode` to the gateway's `gateway.gatewayRouteMode`. If signed access is enabled, configure the same signing key ring on each Server/gateway pair as described in [Secure Access](/guides/secure-access).

For example, install the two gateways and update the existing Server releases:

```bash
helm upgrade --install opensandbox-runc-gateway manifests/charts/ingress-gateway \
  --namespace opensandbox-runc-system \
  --set namespaceOverride=opensandbox-runc-system \
  --set fullnameOverride=opensandbox-runc-gateway \
  --set gateway.dataplaneNamespace=opensandbox \
  --set gateway.gatewayRouteMode=header

helm upgrade --install opensandbox-kata-gateway manifests/charts/ingress-gateway \
  --namespace opensandbox-kata-system \
  --set namespaceOverride=opensandbox-kata-system \
  --set fullnameOverride=opensandbox-kata-gateway \
  --set gateway.dataplaneNamespace=opensandbox \
  --set gateway.gatewayRouteMode=header

helm upgrade --install opensandbox-runc manifests/charts/server \
  --namespace opensandbox-runc-system --values values-runc.yaml \
  --set server.gateway.enabled=true \
  --set-string server.gateway.host=runc-gateway.example.com \
  --set server.gateway.gatewayRouteMode=header

helm upgrade --install opensandbox-kata manifests/charts/server \
  --namespace opensandbox-kata-system --values values-kata.yaml \
  --set server.gateway.enabled=true \
  --set-string server.gateway.host=kata-gateway.example.com \
  --set server.gateway.gatewayRouteMode=header
```

The gateway chart uses `namespaceOverride` for its resource namespace; setting only Helm's `--namespace` does not change that default. The gateway Services above are `ClusterIP` by default. Expose them through your cluster's ingress or load balancer and replace the `*.example.com` hosts with those reachable addresses before running the Server upgrades. A gateway host routes sandbox traffic; clients still use the corresponding Server API endpoint for lifecycle operations.

### Verify profile selection

Create one sandbox through each Server, then inspect the generated BatchSandbox Pod template:

```bash
kubectl get batchsandbox --namespace opensandbox <runc-sandbox-id> \
  -o jsonpath='{.spec.template.spec.runtimeClassName}'
# Empty output: the cluster's default OCI runtime is used.

kubectl get batchsandbox --namespace opensandbox <kata-sandbox-id> \
  -o jsonpath='{.spec.template.spec.runtimeClassName}'
# kata-qemu
```

::: tip Pool mode
Pool-backed sandboxes reuse Pods that already exist, so the Server's `[secure_runtime]` setting does not rewrite their runtime. Create a separate Pool per profile, set `spec.template.spec.runtimeClassName` on each Pool explicitly, and expose only the matching `poolRef` through that profile.
:::

::: info Operational boundary
Multiple Server profiles provide deterministic runtime selection, not a security or tenancy boundary. Servers targeting the same workload namespace can observe the same Kubernetes resources. Use separate workload namespaces (and suitably scoped RBAC or separate clusters) when profiles must also be isolated from one another. Separate gateway names and hosts avoid resource and routing collisions; they do not restrict access to workloads in a shared namespace.
:::

---

## Docker Mode

Docker mode is fully supported for secure container runtimes.

### Prerequisites

- Docker daemon installed and running
- Secure runtime installed on the host

### gVisor Setup for Docker

#### Step 1: Install gVisor runsc

For Docker mode, you only need to install the **runsc** OCI runtime:

```bash
# Ubuntu/Debian
curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | \
  sudo tee /etc/apt/sources.list.d/gvisor.list
sudo apt-get update && sudo apt-get install -y runsc

# Verify installation
runsc --version
```

> **Note**: For Docker mode, only `runsc` is required. The `containerd-shim-runsc-v1` is only needed for Kubernetes/containerd.
>
> **Reference**: See [gVisor Installation Guide](https://gvisor.dev/docs/user_guide/install/) for other distributions and installation methods.

#### Step 2: Configure Docker daemon

Use the `runsc install` command to automatically configure Docker daemon:

```bash
sudo runsc install
```

Or manually edit `/etc/docker/daemon.json`:

```json
{
  "runtimes": {
    "runsc": {
      "path": "/usr/bin/runsc",
      "runtimeArgs": [
        "--platform=systrap",
        "--network=host",
        "--overlay2=none"
      ]
    }
  }
}
```

::: warning Docker snapshots require `--overlay2=none`
runsc normally keeps root filesystem changes in its own `root:self` overlay. Docker cannot include those changes in `docker commit`, so a Docker-backed OpenSandbox snapshot can report success while restoring only the original image. Keep `--overlay2=none` in the runtime arguments when you use snapshot, fork, or restore workflows. This makes sandbox writes visible to Docker's storage driver and therefore available to the snapshot image.

Existing runsc installations created without this argument must update `daemon.json` and restart Docker before taking snapshots. See gVisor's [filesystem documentation](https://gvisor.dev/docs/user_guide/filesystem/) for the root filesystem overlay behavior.
:::

Restart Docker:

```bash
sudo systemctl restart docker
```

> **Reference**: See [gVisor Docker Quick Start](https://gvisor.dev/docs/user_guide/quick_start/docker/) for more details.

#### Step 3: Configure OpenSandbox Server

Edit `~/.sandbox.toml`:

```toml
[runtime]
type = "docker"
execd_image = "opensandbox/execd:latest"

[secure_runtime]
type = "gvisor"
docker_runtime = "runsc"
```

#### Step 4: Start Server and Verify

```bash
opensandbox-server
```

Create a test sandbox:

```bash
curl -X POST http://localhost:8080/v1/sandboxes \
  -H "Content-Type: application/json" \
  -d '{
    "image": {"uri": "python:3.11"},
    "timeout": 3600,
    "resourceLimits": {"cpu": "500m", "memory": "512Mi"},
    "entrypoint": ["python", "-u", "-c", "import time\nwhile True: print('hello from gVisor!'); time.sleep(1)"],
    "metadata": {
      "name": "gvisor-docker-sandbox"
    }
  }'
```

Verify the runtime:

```bash
docker ps --format "{{.ID}}\t{{.Image}}\t{{.Names}}"
docker inspect <container_id> | grep -A2 Runtime
# Expected output:
# "Runtime": "runsc",
```

### Kata Containers Setup for Docker

#### System Requirements

Kata Containers requires hardware virtualization support. Verify your system meets the following requirements:

**Hardware Virtualization Support:**
```bash
# Check if CPU supports hardware virtualization (VT-x for Intel, AMD-V for AMD)
lscpu | grep Virtualization
# Expected output: Virtualization: VT-x (Intel) or AMD-V (AMD)

# Alternatively on Intel
grep -E --color=auto 'vmx|svm' /proc/cpuinfo
# Expected: vmx (Intel) or svm (AMD) flags present
```

**KVM Module:**
```bash
# Check if KVM module is loaded
lsmod | grep kvm
# Expected: kvm_intel (Intel) or kvm_amd (AMD)

# If not loaded, load KVM module
sudo modprobe kvm_intel  # For Intel
# or
sudo modprobe kvm_amd    # For AMD
```

**Kernel Requirements:**
- Linux kernel 5.10 or later recommended
- KVM enabled in kernel config

**Docker Requirements:**
- Docker 20.10 or later
- `/etc/docker/daemon.json` configured for Kata runtime

#### Installation

Download and install Kata Containers static binaries from GitHub releases:

```bash
# Find the latest release at https://github.com/kata-containers/kata-containers/releases
KATA_VERSION="3.27.0"
wget https://github.com/kata-containers/kata-containers/releases/download/${KATA_VERSION}/kata-static-${KATA_VERSION}-amd64.tar.zst

# Extract to root directory - Kata will be installed in /opt/kata
zstd -d kata-static-${KATA_VERSION}-amd64.tar.zst
tar -xvf kata-static-${KATA_VERSION}-amd64.tar -C /

# Create symbolic links for PATH access
sudo ln -sf /opt/kata/bin/kata-runtime /usr/local/bin/kata-runtime
sudo ln -sf /opt/kata/bin/containerd-shim-kata-v2 /usr/local/bin/containerd-shim-kata-v2

# Verify installation
kata-runtime --version
```

#### Configure Docker Daemon

Edit `/etc/docker/daemon.json` to register Kata as a runtime:

```json
{
  "default-runtime": "runc",
  "runtimes": {
    "kata": {
      "runtimeType": "io.containerd.kata.v2"
    }
  }
}
```

Restart Docker to apply changes:

```bash
sudo systemctl restart docker

# Verify Kata is available in Docker
docker info | grep -A5 Runtimes
# Expected output should include "io.containerd.runc.v2 kata"
```

#### Configure OpenSandbox Server

Edit `~/.sandbox.toml`:

```toml
[runtime]
type = "docker"
execd_image = "opensandbox/execd:latest"

[secure_runtime]
type = "kata"
docker_runtime = "kata"
```

#### Verify Installation

**Test with OpenSandbox API**

Create a sandbox and verify it's running in a VM by checking the kernel:

```bash
# Create a test sandbox
curl --location 'http://127.0.0.1:8080/v1/sandboxes' \
  --header 'Content-Type: application/json' \
  --data '{
    "image": {"uri": "ubuntu:latest"},
    "timeout": 3600,
    "resourceLimits": {"cpu": "500m", "memory": "512Mi"},
    "entrypoint": ["/bin/bash", "-c", "while true; do uname -a; sleep 1; done"],
    "metadata": {
      "name": "kata-sandbox"
    }
  }'
```

Check the container's kernel to verify VM isolation:

```bash
# Get the container ID
docker ps | grep kata-sandbox

# Check the kernel inside the container (should be different from host)
docker exec <container_id> uname -a
# Expected output: Linux <hostname> 5.10.x-generic #x86_64 ... (Kata VM kernel)

# Compare with host kernel
uname -a
# Host kernel might be different version or have different hostname
```

**Key Indicators of Kata VM:**
- Container runs in a separate kernel with different hostname
- Kernel version is typically `5.10.x` (Kata's guest kernel)
- Host process list shows `qemu-system-x86_64` or similar hypervisor process
---

## Kubernetes Mode

Kubernetes mode supports secure runtimes through RuntimeClass resources.

### Prerequisites

- Kubernetes cluster with containerd runtime
- Secure runtime installed on all nodes
- RuntimeClass CRDs created

### gVisor Setup for Kubernetes

#### Step 1: Install gVisor Components on All Nodes

For Kubernetes with containerd, you need to install **two** components:

1. **runsc** - the gVisor OCI runtime
2. **containerd-shim-runsc-v1** - the containerd shim for gVisor

```bash
# On each node - Ubuntu/Debian
curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | \
  sudo tee /etc/apt/sources.list.d/gvisor.list
sudo apt-get update

# Install both gVisor components
sudo apt-get install -y runsc containerd-shim-runsc-v1

# Verify installation
runsc --version
containerd-shim-runsc-v1 --version
```
> **Reference**: See [gVisor Installation Guide](https://gvisor.dev/docs/user_guide/containerd/configuration/) for complete installation instructions and other distributions.

#### Step 2: Configure containerd

Edit `/etc/containerd/config.toml`:

```toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
          runtime_type = "io.containerd.runsc.v1"
          [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc.options]
            TypeUrl = "io.containerd.runsc.v1.options"
            ConfigPath = "/etc/containerd/runsc.toml"
```

```bash
sudo tee /etc/containerd/runsc.toml > /dev/null <<'EOF'
[runsc]
  platform = "ptrace"
EOF
```

Restart containerd:

```bash
sudo systemctl restart containerd
```

#### Step 3: Create RuntimeClass CRD

```yaml
# gvisor-runtimeclass.yaml
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc
scheduling:
  nodeSelector:
    kubernetes.io/arch: amd64
```

```bash
kubectl apply -f gvisor-runtimeclass.yaml
```

#### Step 4: Configure OpenSandbox Server

Edit `~/.sandbox.toml`:

```toml
[runtime]
type = "kubernetes"
execd_image = "opensandbox/execd:latest"

[secure_runtime]
type = "gvisor"
k8s_runtime_class = "gvisor"
```

#### Step 5: Verify Installation

```bash
# Test the RuntimeClass
kubectl run test-gvisor --restart=Never --image=hello-world --runtime-class=gvisor
kubectl logs test-gvisor
kubectl delete pod test-gvisor
```

### Kata Containers Setup for Kubernetes

#### Step 1: Install Kata Containers

Follow the [official Kata Containers installation guide](https://github.com/kata-containers/kata-containers/blob/main/tools/packaging/kata-deploy/helm-chart/README.md).

Quick installation using Helm:

```bash
# Install kata-deploy which will set up Kata Containers via DaemonSet
helm install kata-deploy "oci://ghcr.io/kata-containers/kata-deploy-charts/kata-deploy" --version "3.27.0" --namespace kube-system --create-namespace

# Wait for kata-deploy pods to be ready
kubectl wait --for=condition=ready pod -l name=kata-deploy -n kube-system --timeout=300s
```

> **Note**: The `kata-deploy` DaemonSet will automatically configure containerd on all nodes. Manual containerd configuration is not required when using kata-deploy.

#### Step 2: Verify Installation

Check that Kata Containers is installed and RuntimeClasses are created:

```bash
# Check RuntimeClasses
kubectl get runtimeclass

# Expected output:
# NAME         HANDLER     AGE
# kata         kata-qemu   10m
# kata-qemu    kata-qemu   10m
# kata-clh     kata-clh    10m
# kata-fc      kata-fc     10m

# Test Kata with a simple pod
kubectl run test-kata --restart=Never --image=hello-world --runtime-class=kata-qemu
kubectl logs test-kata
kubectl delete pod test-kata
```

### Creating Pools for Different Runtimes (Optional)

When using Pool CRDs for pre-warmed sandboxes, create separate pools for each runtime type:

```yaml
# gvisor-pool.yaml
apiVersion: sandbox.opensandbox.io/v1alpha1
kind: Pool
metadata:
  name: gvisor-pool
  labels:
    runtime: gvisor
spec:
  template:
    spec:
      runtimeClassName: gvisor
      containers:
        - name: sandbox-container
          image: opensandbox/code-interpreter:v1.1.0
  capacitySpec:
    bufferMax: 10
    bufferMin: 2
    poolMax: 20
    poolMin: 5
```

---

## User Guide

This section is for AI application developers using OpenSandbox.

### No Code Changes Required

**Important**: The secure runtime is configured at the server level. Your code does not need to change.

Simply create a sandbox using the OpenSandbox Lifecycle API - the server automatically applies the configured secure runtime:

**Create a test sandbox:**

```bash
curl -X POST http://localhost:8080/v1/sandboxes \
  -H "Content-Type: application/json" \
  -d '{
    "image": {"uri": "python:3.11"},
    "timeout": 3600,
    "resourceLimits": {"cpu": "500m", "memory": "512Mi"},
    "entrypoint": ["python", "-u", "-c", "import time\nwhile True: print(\"hello from secure sandbox!\"); time.sleep(1)"],
    "metadata": {
      "name": "my-secure-sandbox"
    }
  }'
```

**Response:**

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "running"
}
```

The sandbox will automatically use the secure runtime configured on the server (gVisor, Kata, or runc).

### How It Works

1. **Administrator** configures the secure runtime in `~/.sandbox.toml`
2. **Server** validates the runtime at startup
3. **Server** automatically injects the runtime into each sandbox:
   - Docker mode: Adds `runtime` to HostConfig
   - Kubernetes mode: Adds `runtimeClassName` to Pod spec
4. **User** creates sandboxes via API - no runtime parameter needed

### Verifying Runtime Isolation

After creating a sandbox, verify the runtime being used:

**Docker mode:**
```bash
docker ps --format "{{.ID}}\t{{.Image}}\t{{.Names}}"
docker inspect <container_id> | grep -A2 Runtime
# Expected output for gVisor:
# "Runtime": "runsc",
```

**Kubernetes mode:**
```bash
kubectl get pod <pod-name> -o jsonpath='{.spec.runtimeClassName}'
# Expected output for gVisor:
# gvisor
```

---

## Administrator Guide

This section is for platform operators and SREs managing secure runtime infrastructure.

### Prerequisites

Secure runtimes must be installed and configured on your infrastructure **before** configuring OpenSandbox. OpenSandbox does not install runtimes automatically.

### Installation Summary

| Runtime | Docker | Kubernetes |
|---------|--------|------------|
| gVisor | Install runsc → Configure daemon.json | Install runsc → Configure containerd → Create RuntimeClass |
| Kata (QEMU) | Install kata-runtime → Configure daemon.json | Install Kata → Configure containerd → Create RuntimeClass |
| Kata (Firecracker) | Not supported | Install Kata → Configure containerd → Create RuntimeClass |

### Configuration Validation

The server validates secure runtime configuration at startup:

1. **Docker mode**: Checks if the runtime exists in Docker daemon's runtime list
2. **Kubernetes mode**: Checks if the RuntimeClass exists in the cluster

If validation fails, the server refuses to start with a clear error message.

### Security Best Practices

1. **Default to gVisor**: Provides good security with acceptable performance for most workloads
2. **Use Kata for Untrusted Code**: Maximum isolation for completely unknown code
3. **Regular Updates**: Keep runtimes updated for security patches
4. **Test Compatibility**: Validate your workloads with the chosen runtime before production
5. **Monitor Resources**: Secure runtimes have higher memory overhead

### Runtime Selection Guidelines

| Use Case | Recommended Runtime | Reasoning |
|----------|---------------------|-----------|
| Development/Testing | runc (default) | Fastest startup, lowest overhead |
| Production AI Code Execution | gVisor | Good balance of security and performance |
| High-Security Requirements | Kata (QEMU) | Maximum isolation, full compatibility |
| High-Density Multi-Tenant | Kata (Firecracker) | Minimal memory overhead per sandbox |
| Untrusted Network Code | gVisor or Kata | Syscall filtering prevents network attacks |

---

## Troubleshooting and Best Practices

### Common Issues

#### 1. Runtime Not Found (Docker)

**Error**: `Configured Docker runtime 'runsc' is not available.`

**Solution**: Ensure the runtime is configured in `/etc/docker/daemon.json` and Docker has been restarted:

```bash
sudo systemctl restart docker
docker info | grep -A5 Runtimes
```

#### 2. RuntimeClass Not Found (Kubernetes)

**Error**: `RuntimeClass 'gvisor' does not exist.`

**Solution**: Create the RuntimeClass CRD:

```bash
kubectl get runtimeclass
kubectl apply -f gvisor-runtimeclass.yaml
```

#### 3. Syscall Compatibility Issues

**Error**: Container exits with code 1, no logs

**Cause**: gVisor doesn't implement all syscalls. Some applications may not be compatible.

**Solution**: Check the [gVisor compatibility guide](https://gvisor.dev/docs/user_guide/compatibility/). Try using Kata (QEMU) which has better compatibility.

#### 4. Pod Stuck in ContainerCreating

**Cause**: RuntimeClass handler not configured on the node.

**Solution**: Verify containerd configuration:

```bash
# On the node
sudo containerd config dump
sudo systemctl restart containerd
```

#### 5. Egress Sidecar Incompatible with gVisor

**Error**: Sandbox pods CrashLoopBackOff with egress container log:
```
iptables: Failed to initialize nft: Protocol not supported
```
Or with iptables-legacy:
```
iptables v1.8.9 (legacy): can't initialize iptables table 'nat': Table does not exist (do you need to insmod?)
```

**Cause**: gVisor's netstack implements the `filter` and `mangle` iptables tables but does not implement the `nat` table. The egress sidecar uses a REDIRECT rule in the `nat` table to intercept DNS queries (port 53 → 15353), so it cannot start under gVisor. This is an upstream gVisor limitation ([gvisor#170](https://github.com/google/gvisor/issues/170)).

**Solution**:
- Use `secure_runtime.type = "kata"` with `k8s_runtime_class = "kata-qemu"` — Kata provides a full Linux kernel per pod, so the `nat` table is available and the egress sidecar works unchanged.
- Use a CNI-level FQDN policy (e.g., Cilium `toFQDNs`) instead of the egress sidecar for network isolation under gVisor.
- Remove `network_policy` from sandbox creation requests if egress control is not required.

> **Note**: The server validates this combination at request time and returns HTTP 400 with a clear error message when `secure_runtime.type = "gvisor"` and `network_policy` are used together.

### Compatibility Matrix

| Feature | runc | gVisor | Kata (QEMU) | Kata (CLH) | Kata (FC) |
|---------|------|--------|-------------|------------|-----------|
| Syscall Compatibility | Full | Partial | Full | Full | Limited |
| GPU Support | Yes | No | Yes | Yes | No |
| IPv6 | Yes | Yes | Yes | Yes | Yes |
| Privileged Mode | Yes | No | Yes | Yes | No |
| Docker Volume | Yes | Yes | Yes | Yes | Yes |
| Systemd | Yes | No | Yes | Yes | No |
| iptables `nat` table (egress sidecar) | Yes | **No** | Yes | Yes | Yes |

### Getting Help

- **Documentation**: [OpenSandbox GitHub](https://github.com/opensandbox-group/OpenSandbox)
- **Issues**: Report bugs via [GitHub Issues](https://github.com/opensandbox-group/OpenSandbox/issues)

---

### Guides Telemetry (`docs/guides/telemetry.md`)

---
title: Telemetry
description: OpenTelemetry metrics reference for every OpenSandbox component, with per-metric meanings, attributes, and export configuration.
---

# Telemetry

OpenSandbox components export OpenTelemetry (OTLP/HTTP) metrics. This page is the unified reference: each component's metrics with their meaning and attributes, plus the configuration that controls export. SDK-side creation metrics and pool warmup tracing are covered separately in [SDK observability](/sdks/observability).

| Component | Meter / service name | Metric families |
| --- | --- | --- |
| [Lifecycle server](#lifecycle-server) | `opensandbox-server` | HTTP request latency, SDK-reported create latency |
| [Kubernetes controller](#kubernetes-controller) | `opensandbox/controller` | Allocator histograms, capacity gauges (+ controller-runtime endpoint) |
| [Execd](#execd) | `opensandbox/execd` | HTTP, execution, filesystem, system resources, OOM, isolation |
| [Egress sidecar](#egress-sidecar) | `opensandbox/egress` | DNS, policy, nftables, node/process resources |
| [Ingress gateway](#ingress-gateway) | `opensandbox-ingress-<version>` | HTTP/routing, upstream dial, network readiness shadow, wake-on-access, host |
| [Node agent](#node-agent) | `opensandbox.nodeagent.*` | Log pipeline records, bytes, drops, retries, queue depth, sink latency |

## Shared OTLP configuration for Go components

Execd, egress, ingress, and node-agent share the OTLP metrics exporter in `components/internal/telemetry`. Set these variables in the **component process environment**. Setting them on the lifecycle server or in a sandbox creation request does not automatically configure another container, such as the egress sidecar. For server-managed egress endpoints, see [egress observability](/architecture/network/egress#observability-opentelemetry).

### Export endpoints

- `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` takes precedence over `OTEL_EXPORTER_OTLP_ENDPOINT`.
- Export uses HTTP/protobuf against a collector's HTTP receiver (usually port 4318). gRPC export and switching transport with `OTEL_EXPORTER_OTLP_PROTOCOL` are not implemented.
- Node-IP fallback: when neither endpoint is set, execd, egress, and ingress fall back to `HOST_IP:4318`, or the node IP read from `/etc/hostinfo`, over plain HTTP. If no node IP resolves, metrics are not exported. Components that set `DisableEndpointFallback` in their shared telemetry configuration (node-agent) skip this fallback. Consequently, clearing the endpoint variables alone does not always disable metrics.

### Disabling metrics

Set either of these standard switches to prevent metrics export, including explicit endpoints and the node-IP fallback:

```sh
OTEL_SDK_DISABLED=true
# Or disable metrics only:
OTEL_METRICS_EXPORTER=none
```

Values are case-insensitive. Other values leave the existing export configuration unchanged; `OTEL_METRICS_EXPORTER` does not select an alternative exporter here. These switches do not disable native application logs or execd's local `/metrics` and `/metrics/watch` APIs. They also suppress the egress sidecar's automatic collector allow rule, including when the effective policy is rebuilt. Operator-provided allow rules remain in effect.

### Aggregation temporality

Set `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` to select the exporter's standard aggregation behavior:

| Value | Counter and Histogram | ObservableCounter | UpDownCounter and ObservableUpDownCounter |
|---|---|---|---|
| `cumulative` | Cumulative | Cumulative | Cumulative |
| `delta` | Delta | Delta | Cumulative |
| `lowmemory` | Delta | Cumulative | Cumulative |
| Unset, empty, or whitespace-only | Delta | Cumulative | Cumulative |

Values are case-insensitive. An invalid nonempty value uses the upstream exporter's cumulative default and produces its configuration warning. Gauges remain instantaneous measurements.

Prometheus-style backends expect cumulative counters and histograms. Either set `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative` in the component environment, or configure the Collector's `deltatocumulative` processor when ingesting delta metrics. Choose the setting before component startup; these environment variables are not hot-reloaded.

## Lifecycle server

Optional OTLP export for server HTTP requests and SDK-reported sandbox creation latency (`POST /v1/metrics/events`). Off by default; the HTTP middleware and the ingestion endpoint remain active but record as noops.

Configuration keys under `[otel]` (see the [server configuration reference](https://github.com/opensandbox-group/OpenSandbox/blob/main/server/configuration.md) for the authoritative list):

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | boolean | `false` | Enable OTLP metrics export. |
| `endpoint` | string \| omitted | `null` | OTLP HTTP metrics endpoint. When omitted, uses `OTEL_EXPORTER_OTLP_ENDPOINT` / `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`. |
| `service_name` | string | `"opensandbox-server"` | `service.name` resource attribute. |
| `export_interval_millis` | integer | `60000` | Periodic export interval (≥ 1000). |

Exported metrics:

| Metric | Type | Unit | Attributes | Description |
|-----|------|------|------------|-------------|
| `server.http.request.duration` | Histogram | `ms` | `http_method`, `http_route`, `http_status_code` | Server HTTP request latency. Histogram count provides request volume. |
| `opensandbox.sandbox.create.duration` | Histogram | `ms` | `sdk.language`, `sdk.version`, `success` | SDK-reported creation latency from create start until ready or failure. |

The HTTP metric uses the matched route template rather than the raw request path. Requests that do not reach a matched route, including early authentication failures and unmatched URLs, use `http_route=unknown`. Standard HTTP methods are recorded in uppercase, while extension methods use `http_method=OTHER` to keep attribute cardinality bounded. The metric never includes sandbox IDs, tenant IDs, API keys, request or response bodies, query strings, or other unbounded request data.

Client-side controls for the creation metric (`OPENSANDBOX_DISABLE_METRICS=1`) and pool warmup traces live in [SDK observability](/sdks/observability#creation-metrics).

## Kubernetes controller

The sandbox controller emits OTLP metrics for the pool allocation path and controller capacity. Design principles: only core signals (no speculative instrumentation), reuse what controller-runtime already exposes, and use standard OTel SDK instruments so measurements aggregate across controller replicas.

All custom controller metrics export through one process-wide global OTel meter provider, configured through the standard OpenTelemetry environment variables (see [Enabling OTLP export](#enabling-otlp-export-for-the-controller)). There are no controller-specific flags.

### Allocator-path metrics

All instruments are `Float64Histogram` on meter scope `opensandbox/controller`, recorded with unit `s` (seconds), with explicit bucket boundaries `[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]` (seconds). Success/error separation is expressed through the `success` boolean attribute; histogram `count` per attribute set provides the success/error counts.

| Name | Attributes | Covers | Emitted from |
|---|---|---|---|
| `controller.allocator.schedule.duration` | `namespace`, `pool_name`, `success` | The schedule decision itself (`Allocator.Schedule`, including recovery, request building, and the packing algorithm) | `pool_controller.go` `scheduleSandbox` |
| `controller.allocator.persist_alloc_state.duration` | `namespace`, `pool_name`, `success` | Persisting allocation state: the `alloc-status` annotation Patch against the API server | `allocator.go` `annoAllocationSyncer.SetAllocation` |
| `controller.allocator.sync_alloc_result.duration` | `namespace`, `pool_name`, `success` | Syncing the batch allocation result to all sandboxes of a pool (the full concurrent sync round) | `pool_controller.go` `doAllocate` |
| `controller.allocator.sync_single_alloc_result.duration` | `namespace`, `pool_name`, `success` | Per-sandbox allocation-result sync (in-memory store update + annotation Patch); recorded for both `allocated` and `released` syncs | `pool_controller.go` `syncSandboxConcurrently` |

Attribute semantics:

- `namespace`: Kubernetes namespace of the Pool (or BatchSandbox for the per-sandbox instruments).
- `pool_name`: Pool name (`pool.name` semantics; `BatchSandbox.spec.poolRef` for the per-sandbox instruments).
- `success`: `true` when the operation completed without error, `false` otherwise.

::: info Cardinality
The attribute set is intentionally bounded by (namespace, pool_name, success); there is deliberately no `sandbox_name` label. Per-sandbox identity remains available through Kubernetes objects and events; traces are the intended vehicle for per-sandbox latency attribution.
:::

### Capacity metrics

Gauge-style signals collected periodically from the informer cache, registered on the same global OTel meter provider and driven by a leader-elected manager Runnable that unregisters on shutdown:

| Name | Type | Unit | Attributes |
|---|---|---|---|
| `controller.pool.pods` | ObservableGauge | `{pod}` | `namespace`, `pool_name`, `state` (`total`/`allocated`/`available`/`updated`) |
| `controller.pool.cpu.requested` | ObservableGauge | `{cpu}` | `namespace`, `pool_name`, `state` |
| `controller.pool.memory.requested` | ObservableGauge | `By` | `namespace`, `pool_name`, `state` |
| `controller.batchsandbox.count` | ObservableGauge | `{batchsandbox}` | `namespace`, `phase`, `allocation_mode` (`pool`/`direct`) |
| `controller.batchsandbox.pods` | ObservableGauge | `{pod}` | `namespace`, `state` (`desired`/`current`/`allocated`/`ready`), `allocation_mode` |
| `controller.capacity.collect.duration` | ObservableGauge | `s` | — |

### Controller-runtime metrics (reused, not duplicated)

The controller-runtime metrics endpoint (disabled by default; enable with `--metrics-bind-address`) already exposes workqueue depth/latency, per-driver reconcile counts and durations (`controller_runtime_reconcile_*`), and REST client metrics. These are the source for reconcile health and workqueue signals; the controller does not emit competing custom versions. Pod provisioning failure recovery additionally publishes `opensandbox_pod_recovery_*` counters on that endpoint — see [pod provision failure recovery](/guides/pod-recovery).

### Enabling OTLP export for the controller

| Environment variable | Default | Description |
|---|---|---|
| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | (unset) | Per-signal OTLP/HTTP endpoint, used as-is (no path appended). |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | (unset) | Generic OTLP/HTTP endpoint, used as a base URL; `/v1/metrics` is appended when no path is present. Takes effect when the metrics-specific variable is unset. |
| `OTEL_EXPORTER_OTLP_METRICS_HEADERS` / `OTEL_EXPORTER_OTLP_HEADERS` | (unset) | W3C Baggage-style `key1=value1,key2=value2` headers attached to export requests. |
| `OTEL_METRIC_EXPORT_INTERVAL` | `60000` (ms) | Interval between OTLP metric exports. |
| `OTEL_SERVICE_NAME` | `opensandbox-controller` | `service.name` resource attribute. |
| `OTEL_RESOURCE_ATTRIBUTES` | (unset) | Additional resource attributes (`key1=value1,key2=value2`). |
| `OTEL_SDK_DISABLED` / `OTEL_METRICS_EXPORTER` | (unset) | Set `OTEL_SDK_DISABLED=true`, or set `OTEL_METRICS_EXPORTER` to a comma-separated list that does not include `otlp`, to disable export. |

Example (Kubernetes container env):

```yaml
env:
  - name: OTEL_EXPORTER_OTLP_ENDPOINT
    value: http://otel-collector.observability:4318
  - name: OTEL_METRIC_EXPORT_INTERVAL
    value: "30000"
```

Behavior:

- With no endpoint configured, the meter provider stays a no-op and nothing is exported.
- The capacity gauges register only on the elected leader (leader-elected Runnable); allocator histograms are recorded and exported on every replica.
- Export failures are retried by the OTLP exporter; setup/configuration errors never block controller startup (telemetry degrades with an error log).
- On shutdown the provider flushes pending data with a 5-second budget.

### Prometheus rendering

When exporting through an OTLP → Prometheus converter (e.g. the OpenTelemetry Collector's `prometheusremotewrite` exporter or a Prometheus OTLP receiver), dots become underscores and the unit is appended, for example:

```
controller_allocator_schedule_duration_seconds{namespace="default",pool_name="pool-a",success="true"}
```

## Execd

Meter: `opensandbox/execd` · Service name: `opensandbox-execd-<version>` (fixed by execd; `OTEL_SERVICE_NAME` is not read).

| Metric | Type | Unit | Attributes | Meaning |
|---|---|---|---|---|
| `execd.http.request.duration` | Histogram | `ms` | `http_method`, `http_route`, `http_status_code` | HTTP request latency. |
| `execd.execution.duration` | Histogram | `ms` | `operation`, `result` | Code/command execution duration. |
| `execd.filesystem.operations.duration` | Histogram | `ms` | `operation`, `result` | Filesystem operation duration. |
| `execd.system.process.count` | Observable Gauge | - | - | Current process count. |
| `execd.system.cpu.usage` | Observable Gauge | `%` | - | Sandbox CPU usage percent: sandbox cgroup when resource limits are configured, host `/proc` otherwise (e.g. microVM guests). |
| `execd.system.memory.usage_bytes` | Observable Gauge | `By` | - | Sandbox memory used bytes: sandbox cgroup when resource limits are configured, host `/proc` otherwise (e.g. microVM guests). |
| `execd.system.network.io.bytes` | Observable Counter | `By` | `direction` (`in`/`out`) | Cumulative network I/O bytes. |
| `execd.system.network.connections.active` | Observable Gauge | - | `protocol` (`tcp`/`udp`) | Current active network connections. |
| `execd.system.oom.kills` | Counter | `{kill}` | `level` (`local`/`descendant`, cgroup v2 only) | OOM kills detected in the sandbox cgroup (see below). |
| `execd.isolation.run.duration` | Histogram | `ms` | `result` | Isolated session run duration. |
| `execd.isolation.session.count` | Observable Gauge | - | - | Current number of active isolated sessions. |
| `execd.isolation.upper.usage_bytes` | Observable Gauge | `By` | - | Total bytes used by isolated session upper directories. |

When an isolated upper-directory scan fails (for example, because an active
upper directory is missing or a path cannot be read), execd omits that
collection's `execd.isolation.upper.usage_bytes` observation instead of
reporting an incorrect zero. A successfully measured empty upper still reports
zero, and the active-session count continues to be reported. Treat a missing
usage observation as **unknown**, not as zero usage.

### OOM kill observation

Execd polls its own cgroup every 5 seconds and diffs the kernel OOM kill counter: `memory.events` (`oom_kill`) on cgroup v2, `memory.oom_control` on cgroup v1. Coverage spans the whole sandbox cgroup subtree — user and business processes and nested cgroups (e.g. isolated sessions), not only the sandbox main process. cgroup v2 hosts are covered fully; cgroup v1 reports a single increment without `level`. Kernel logs (dmesg) are intentionally not used: the sandbox container has no `CAP_SYSLOG`, and the kernel ring buffer is not sandbox-scoped.

Every detection produces two signals:

- Metric `execd.system.oom.kills` with a `level` attribute: `local` for kills of processes in the sandbox cgroup itself, `descendant` for kills in nested cgroups.
- A structured warn log line on the container stdout (or the file configured with `EXECD_LOG_FILE`) with troubleshooting context:
  - `memory=<used>/<limit> peak=<peak>` — where memory stood against the cgroup limit when the detection fired
  - `events=[oom=N oom_kill=N oom_group_kill=N]` — deltas of the `memory.events` sub-counters (cgroup v2 only)
  - `kill_cgroups=a,b` — first-level child cgroups whose subtree received the kills
  - `exited=[pid=N comm=x ppid=N rss=N age=Ns, ...]` — processes that disappeared from `/proc` between the polls surrounding the event (candidates for victims; bounded by the kill count, capped at 5 entries)

On warm sandboxes rebound by `POST /internal/init`, the counter interval that crosses a binding handoff is dropped rather than attributed to the new sandbox identity.

### Shared attributes

- `sandbox_id` from `OPENSANDBOX_ID` (when set)
- extra key/value attributes from `OPENSANDBOX_EXECD_METRICS_EXTRA_ATTRS` (when set)

Execd additionally serves local CPU/memory snapshots and live streams through its `/metrics` and `/metrics/watch` APIs, independent of OTLP export — see [execd architecture](/architecture/data-plane/execd#observability).

### Endpoint configuration

Export is enabled only when at least one OTLP endpoint is set (`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, fallback `OTEL_EXPORTER_OTLP_ENDPOINT`); otherwise execd keeps metrics local. If both are unset, the node-IP fallback described [above](#export-endpoints) applies.

```bash
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="http://otel-collector:4318"
```

## Egress sidecar

Meter: `opensandbox/egress` · Service name: `opensandbox-egress-<version>`.

| Metric | Type | Unit | Meaning |
|---|---|---|---|
| `egress.dns.query.duration` | Histogram | `s` | Upstream DNS forward latency (recorded for allowed queries). |
| `egress.dns.query.failed_total` | Counter | - | Queries the proxy could not resolve, by `reason`. |
| `egress.dns.reply.failed_total` | Counter | - | Reply writes that failed after a decision, by `stage`. A nonzero count means a query was handled but its answer never reached the client. |
| `egress.mitm.tls.connections_total` | Counter | - | Experimental live sidecar TLS admission decisions, by closed `mode`, `decision`, and `reason` labels; not successful handshakes or HTTP requests. |
| `egress.policy.denied_total` | Counter | - | Number of DNS queries denied by policy. |
| `egress.nftables.rules.count` | Observable Gauge | `{element}` | Approximate policy size after last successful static apply (fast-sandbox profile: summed across every installed subject's policy, 0 while deny-first). |
| `egress.nftables.updates.count` | Counter | - | Number of successful nftables updates (static apply + dynamic IP add). |
| `egress.nftables.updates.failed_total` | Counter | - | nftables updates that failed, by `operation`. |
| `egress.system.memory.usage_bytes` | Observable Gauge | `By` | **Node** memory used bytes (Linux: gopsutil; non-Linux build: `0`). |
| `egress.system.cpu.utilization` | Observable Gauge | `1` | **Node** CPU busy ratio in `[0,1]` (Linux: gopsutil; non-Linux build: `0`). |
| `egress.process.memory.usage_bytes` | Observable Gauge | `By` | Memory charged to the sidecar's own cgroup. Only present when cgroupfs is readable. |
| `egress.process.cpu.time` | Observable Counter | `s` | CPU seconds consumed by the sidecar's own cgroup. Only present when cgroupfs is readable. |

### Experimental TLS admission decisions

`egress.mitm.tls.connections_total` is emitted only by the experimental credential-bound sidecar runtime. `mode` is always `credential-bound`; no new configuration or public interception mode is enabled. The existing request-weighted shadow metric remains separate.

| `decision` | Allowed `reason` values |
|---|---|
| `decrypt` | `binding_host` |
| `passthrough` | `ech`, `no_sni`, `static_ignore`, `no_binding_host` |
| `deny` | `unknown_identity`, `invalid_sni`, `snapshot_missing`, `generation_mismatch`, `invalid_input`, `registry_exhausted`, `admission_disabled`, `ssl_insecure`, `malformed_extensions` |

Each final ClientHello admission decision increments the counter once. A new handshake admission on the same transport is another decision; HTTP/1.1 requests and HTTP/2 streams do not increment it. `decrypt` means the registry admitted decryption, not that tracking, the TLS handshake, or later HTTP/2 handling succeeded. `snapshot_missing` includes bootstrap without an installed snapshot and a closed registry.

Collection uses best-effort `ctx.log.warn` events forwarded to Go. Record failures do not alter admission; events may be lost and add logging cost. No hostname, credential, subject identifier, revision, or raw error is added as a metric label. The closed label set bounds cardinality but does not authenticate the log source: use this as an operational observation, not security evidence or a standalone paging signal. Failed handshakes may produce no HTTP requests, so handshake event volume is not necessarily below request volume.

### `system` vs `process`

They measure different things, and the difference matters because this sidecar runs **per sandbox**:

- `egress.system.*` comes from gopsutil, i.e. `/proc/meminfo` and `/proc/stat`, which inside a container describe the **node**. Every sandbox on a node therefore publishes the same figure under its own `sandbox_id`. Do not chart these "by sandbox": the series look per-sandbox but are N copies of one node number. Prefer kubelet/cAdvisor or a node exporter for node-level data.
- `egress.process.*` is read from the sidecar's own cgroup (v2 `memory.current` and `cpu.stat`, falling back to v1 `memory.usage_in_bytes` and `cpuacct.usage`), so it really is per sandbox.

`egress.process.cpu.time` is a **cumulative counter of consumed seconds**, not a sampled ratio: use `rate()` on it. Both `process` instruments are registered only if their cgroup files can be read — a runtime that does not expose cgroupfs (a sandbox pod under `secure_runtime`, for instance) gets no series at all, rather than a flat zero that reads like an idle sidecar.

### DNS latency histogram boundaries

`egress.dns.query.duration` declares its bucket boundaries explicitly:

```
0.001  0.0025  0.005  0.01  0.025  0.05  0.1  0.25  0.5  1  2.5  5  10  15  30  60  120  300  600
```

The instrument records **seconds**, while the SDK default boundaries are the spec's millisecond ladder (`0, 5, 10, … 10000`), so every realistic latency would fall into the single `le=5` bucket and the quantiles would be meaningless. The coarse tail exists because the recorded duration covers the **whole resolver chain**: forwarding walks the upstreams serially, each with the full timeout (`OPENSANDBOX_EGRESS_DNS_UPSTREAM_TIMEOUT`, 2s by default), so a query can legitimately take `timeout x len(upstreams)`. A late **success** lands in the tail too, not only an exhausted failure. The chain has no finite worst case (`OPENSANDBOX_EGRESS_DNS_UPSTREAM` accepts an unbounded resolver list), so past the last boundary quantile resolution is lost by construction and `_count` is what remains. Both successful and failed lookups feed this histogram, so its tail mixes slow resolutions with exhausted retry chains.

### Failure signals

`egress.dns.query.failed_total` and `egress.policy.denied_total` answer different questions, and confusing them inverts the diagnosis:

- **denied** — the policy did its job. The workload asked for something it is not allowed to reach. Expected traffic in a working system.
- **failed** — the sidecar could not do its job. The workload asked for something allowed and got `SERVFAIL`. Never expected.

`reason` comes from a closed set, so the counter's cardinality is fixed and neither the queried name nor the error text is ever attached:

| `reason` | Meaning |
|---|---|
| `no_upstreams` | No resolvers configured or discovered. |
| `upstream_error` | Every resolver failed to answer (network error, timeout). |
| `empty_response` | A resolver returned a nil message. |
| `rcode` | The last resolver answered with a failover-worthy rcode, e.g. `SERVFAIL`. |

`egress.nftables.updates.failed_total` covers another silent failure class. Its `operation` attribute is one of `static_apply`, `dynamic_add`, `remove`, or — in the fast-sandbox profile (OSEP-0022) — `deny_first`, `reset`. `dynamic_add` is the one to alert on: a failed add means the kernel never learned about IPs the policy allows, so the chain drops traffic that should pass — which looks exactly like a policy denial from inside the sandbox while `egress.policy.denied_total` stays flat.

`egress.dns.reply.failed_total` covers the last silent failure class: a query that was **handled** (decided, maybe forwarded and answered upstream) whose reply write then failed. Its `stage` attribute is one of `malformed`, `unknown_source`, `deny`, `upstream_error`, `answer`, and every failure also emits a `[dns] reply write failed (stage=… remote=… question=…)` warning with the remote address and query name. Alert on any nonzero value: like `dynamic_add`, an `answer`-stage failure means traffic the policy allows is not reaching the client.

A `static_apply` failure happens during startup, where the sidecar logs and exits. Metrics leave through a periodic reader and `os.Exit` skips the deferred shutdown, so that path flushes telemetry explicitly before terminating — otherwise the one sample explaining why the sidecar died would never be exported.

### Automatic egress allow rule

When an OTLP destination is configured — the endpoint env vars, or the exporter fallback node IP (`HOST_IP` / `/etc/hostinfo`) when both are unset — egress automatically injects an always-allow egress rule for that host (domain or IP, any port), so telemetry export works under the default deny-all policy without manually managing allowlist rules. This also covers the sidecar's own metric export, which shares the sandbox network namespace and would otherwise be blocked by its own egress chain.

- Precedence: `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` wins over `OTEL_EXPORTER_OTLP_ENDPOINT`; the fallback node IP applies only when neither is set. A set-but-invalid endpoint never falls back (the exporter does not either), so no rule is injected in that case.
- The endpoint must be a URL (`https://host:4318/v1/metrics`) — the `otlpmetrichttp` env-var form. Bare `host:port` or `host` values are not accepted; a trailing root dot on FQDNs is trimmed to match DNS policy normalization.
- The rule lives in the always-allow layer: it survives user `POST`/`PATCH`/`DELETE` policy updates and always-rule file reloads. Operators can still block the target with `deny.always`, which takes precedence.
- Rules are host-scoped (any port), matching the egress rule model; ports are not enforced per rule.

::: warning
Use a fully-qualified service name or an IP in the endpoint. Single-label names (e.g. `otel-collector`) are subject to resolver search-domain expansion, and the deny-all DNS proxy answers the expanded names (e.g. `otel-collector.<ns>.svc.cluster.local`) with NXDOMAIN without falling back to the bare name, so the auto-generated exact-host allow rule would not be reached.
:::

### Shared attributes

- `sandbox_id` from `OPENSANDBOX_EGRESS_SANDBOX_ID` (when set). Without it the sidecars of different sandboxes export identical attribute sets, so their series collide in the backend — which matters most for the per-sandbox `egress.process.*` gauges.
- extra key/value attributes from `OPENSANDBOX_EGRESS_METRICS_EXTRA_ATTRS` (when set)

### Endpoint configuration

Export is enabled only when at least one OTLP endpoint is set; otherwise egress keeps metrics local (no OTLP export), subject to the node-IP fallback described [above](#export-endpoints).

```bash
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="http://otel-collector.sandbox.svc.cluster.local:4318"
# An IP endpoint works as well:
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="http://10.0.0.5:4318"
```

The server can inject the endpoint into every egress sidecar via `[egress].otlp_endpoint`; see [egress observability](/architecture/network/egress#observability-opentelemetry).

### Structured logs (JSON)

Egress structured logs are emitted by zap (typically to stdout). OTLP log export is not implemented in-tree.

- Common fields: `sandbox_id` (when `OPENSANDBOX_EGRESS_SANDBOX_ID` is set), key/value pairs from `OPENSANDBOX_EGRESS_METRICS_EXTRA_ATTRS` merged into the root logger, and `opensandbox.event` identifying the event family.
- `opensandbox.event=egress.outbound` — emitted on allow-path DNS handling (success or forward error) with `target.host`, `target.ips`, `peer`, and `error`.
- Policy lifecycle: `opensandbox.event=egress.loaded` (initial effective policy loaded), `egress.updated` (policy update applied), `egress.update_failed` (policy update failed), with `egress.default` (`allow`/`deny`), `rules`, and `error` fields.

## Ingress gateway

Meter: `opensandbox/ingress` · Service name: `opensandbox-ingress-<version>`.

### HTTP requests & routing

| Metric | Type | Unit | Attributes | Description |
|---|---|---|---|---|
| `ingress.http.request.count` | Counter | — | `http_method`, `http_status_code`, `proxy_type` | Request count (QPS derivable) |
| `ingress.http.request.duration` | Histogram | `ms` | `http_method`, `http_status_code`, `proxy_type` | Full request duration |
| `ingress.routing.resolutions.count` | Counter | — | `routing_result` | Routing resolution count |
| `ingress.routing.resolution.duration` | Histogram | `ms` | `routing_result` | Routing resolution latency |

### Upstream connectivity

| Metric | Type | Unit | Attributes | Description |
|---|---|---|---|---|
| `ingress.upstream.connect.count` | Counter | — | `connect_result`, `proxy_type` | Upstream TCP dial attempts by result |
| `ingress.upstream.connect.duration` | Histogram | `ms` | `connect_result`, `proxy_type` | Upstream TCP dial duration |

### Network readiness (shadow assessment)

Reported for the most recent complete window; tuned by the `--network-readiness-shadow-*` flags. Shadow gauges never gate traffic.

| Metric | Type | Unit | Description |
|---|---|---|---|
| `ingress.network.shadow.attempts` | Observable Gauge | — | TCP connection attempts in the window |
| `ingress.network.shadow.signal_failures` | Observable Gauge | — | Timeout or unreachable results in the window |
| `ingress.network.shadow.distinct_targets` | Observable Gauge | — | Bounded distinct upstream targets in the window |
| `ingress.network.shadow.distinct_signal_targets` | Observable Gauge | — | Distinct targets with timeout/unreachable results |
| `ingress.network.shadow.qualified` | Observable Gauge | — | 1 when the window has enough samples |
| `ingress.network.shadow.degraded` | Observable Gauge | — | 1 when the qualified window is degraded |

### Wake-on-access (OSEP-0024)

Emitted when wake-on-access is enabled. Restore saturation, parking-lot shedding, policy gate failures, and Redis write drops are the signals to watch.

| Metric | Type | Unit | Attributes | Description |
|---|---|---|---|---|
| `ingress.wake.flights` | Counter | — | `outcome` (`triggered`/`joined`/`none`) | Wake flight count by outcome |
| `ingress.park.active` | UpDownCounter | — | - | Currently parked requests |
| `ingress.park.wait.duration` | Histogram | `s` | `outcome` (`served`/`budget_exhausted`/`canceled`/`error`) | Per-parked-request wait duration; buckets are sized around the default 5s park budget (a ~200ms same-node resume vs a near-exhaustion wait) |
| `ingress.park.shed` | Counter | — | - | Requests shed because the parking lot was full |
| `ingress.wake.restore.active` | UpDownCounter | — | - | Wake flights currently holding a restore permit (the concurrent-restore gauge) |
| `ingress.idlep.policy.reads` | Counter | — | `outcome` (`allowed`/`denied`/`error`) | Idle-policy gate outcomes; sustained `error` means an RBAC or apiserver problem |
| `ingress.activity.writes_dropped` | Counter | — | - | Activity observations dropped before reaching Redis |

### Host metrics (Linux only; 0 elsewhere)

| Metric | Type | Unit | Description |
|---|---|---|---|
| `ingress.system.cpu.usage` | Observable Gauge | `1` | CPU busy ratio `[0,1]` |
| `ingress.system.memory.usage_bytes` | Observable Gauge | `By` | Memory used bytes |
| `ingress.connections.active` | Observable Gauge | — | TCP ESTABLISHED connections (from `/proc/net/tcp`) |

**Attribute values:**

- `proxy_type`: `http` | `websocket`
- `routing_result`: `success` | `not_found` | `not_ready` | `error`
- `connect_result`: `success` | `timeout` | `unreachable` | `refused` | `dns_error` | `canceled` | `other`

### Endpoint configuration

```bash
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="http://otel-collector:4318"
```

Fallback: `OTEL_EXPORTER_OTLP_ENDPOINT`. When both are unset, export falls back to the node IP (`HOST_IP` env, then `/etc/hostinfo`) on port 4318 over plain HTTP; if no node IP resolves, metrics are not exported. Disable export with `OTEL_SDK_DISABLED=true` or `OTEL_METRICS_EXPORTER=none`. Default temporality: delta for synchronous Counter/Histogram, cumulative for observable gauges — override with `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE`.

## Node agent

The node agent's log pipeline exports OTLP metrics with no sandbox or Pod identifiers as labels:

| Metric | Type | Unit | Description |
|---|---|---|---|
| `opensandbox.nodeagent.records` | Counter | - | Records consumed from sources and handed to the sink. |
| `opensandbox.nodeagent.bytes` | Counter | `By` | Record payload bytes consumed. |
| `opensandbox.nodeagent.drops` | Counter | - | Records dropped (only possible under `drop` backpressure). |
| `opensandbox.nodeagent.retries` | Counter | - | Sink delivery retry attempts. |
| `opensandbox.nodeagent.queue.bytes` | UpDownCounter | `By` | Bytes currently queued for delivery (queue depth proxy). |
| `opensandbox.nodeagent.sink.consume.duration` | Histogram | `ms` | Sink consumption (write) latency. |

It requires an explicit endpoint — the node-IP fallback used by other components is disabled (`DisableEndpointFallback`):

```yaml
opensandbox-node-agent:
  extraEnv:
    - name: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
      value: http://otel-collector.observability:4318/v1/metrics
```

See [node agent configuration](/guides/node-agent) for the full agent setup.

## SDK metrics

The SDKs report sandbox creation latency to the lifecycle server (`POST /v1/metrics/events`), which the server exports as `opensandbox.sandbox.create.duration` when `[otel]` is enabled, and can emit pool warmup traces through the application's own OpenTelemetry provider. Client-side enablement and disablement are covered in [SDK observability](/sdks/observability).

---

### Guides Windows Sandbox (`docs/guides/windows-sandbox.md`)

---
title: Windows Sandbox
description: Run Windows guests inside Linux containers via KVM/QEMU, managed through the OpenSandbox lifecycle API.
---

# Windows Sandbox

A Windows sandbox runs a Windows guest in a Linux container via KVM/QEMU and is managed end-to-end through the OpenSandbox lifecycle API (create, expose endpoints, delete). The current Windows profile is based on [`dockur/windows`](https://github.com/dockur/windows), with service integration and capabilities provided by OpenSandbox.

## Scope

This guide covers the OpenSandbox Server Windows profile (`platform.os=windows`):

- Create a Windows sandbox
- Check status and endpoints
- Common resources and volume settings
- Delete and cleanup

## How it works

- **Runtime**: [`dockur/windows`](https://github.com/dockur/windows) runs the Windows guest in a Linux container (requires KVM/QEMU), using mount points such as `/storage` and `/boot.iso` for the system disk and install media.
- **Control plane**: On create, OpenSandbox Server injects the Windows profile configuration (devices, capabilities, `execd`, port mappings) and enforces resource checks, state, and endpoint publication.
- **Client**: You call the Lifecycle API or the Python SDK with `platform.os=windows`, then follow create → poll `Running` → get endpoints → use → delete.

## Prerequisites

- Reachable OpenSandbox Server (e.g. `http://localhost:8080`)
- Host meets the Windows profile requirements:
  - `/dev/kvm` present
  - `/dev/net/tun` present
- Server `storage.allowed_host_paths` configured for allowed host paths
- A host directory to bind to `/storage` (recommended)
- Optional: a local Windows ISO for `/boot.iso` (cuts repeated download time)

## Key settings and constraints

### Platform and image

- Set `platform: {"os":"windows","arch":"amd64"}` (`arm64` depends on the host)
- Suggested image: `dockurr/windows:latest`; see [`dockur/windows` releases](https://github.com/dockur/windows/releases) and [Docker Hub tags](https://hub.docker.com/r/dockurr/windows/tags)

### Minimum resources

The Windows profile enforces:

- `resourceLimits.cpu >= 2`
- `resourceLimits.memory >= 4G`
- `resourceLimits.disk >= 64G`

### `resourceLimits` vs. environment variables

If `resourceLimits` is set, the server maps it to and overwrites these env vars:

- `CPU_CORES`
- `RAM_SIZE`
- `DISK_SIZE`

Prefer `resourceLimits` only; do not set those three env vars manually.

### Default ports and `USER_PORTS`

The Windows profile wires these ports by default:

- `44772` (execd)
- `8080`
- `3389/tcp`, `3389/udp` (RDP)
- `8006/tcp` (typical web console)

It also builds or merges `USER_PORTS` (often `44772,8080,3389,8006`).

### Storage mounts

- Bind a writable host directory to `/storage`
- Optional: bind an ISO file to `/boot.iso` (`readOnly=true`)
- Host paths must fall under `storage.allowed_host_paths` or the request is rejected

## End-to-end example (Python)

This flow:

1. Create a Windows sandbox
2. Wait until `Running`
3. Get the `44772` endpoint
4. Use the Python SDK against the sandbox
5. Delete the sandbox

### Minimal example

```python
import os
from datetime import timedelta

from opensandbox import SandboxSync
from opensandbox.config import ConnectionConfigSync
from opensandbox.models.sandboxes import PlatformSpec

BASE_URL = os.getenv("OPEN_SANDBOX_BASE_URL", "http://localhost:8080")
API_KEY = os.getenv("OPEN_SANDBOX_API_KEY", "")


def main() -> None:
    cfg = ConnectionConfigSync(
        domain=BASE_URL,
        api_key=API_KEY or None,
        use_server_proxy=True,
    )

    sbx = None
    try:
        sbx = SandboxSync.create(
            image="dockurr/windows:latest",
            timeout=timedelta(hours=12),
            ready_timeout=timedelta(minutes=30),
            resource={
                "cpu": "4",
                "memory": "8G",
                "disk": "64G",
            },
            env={"VERSION": "11"},
            entrypoint=["cmd", "/c", "echo OpenSandbox Windows profile"],
            platform=PlatformSpec(os="windows", arch="amd64"),
            connection_config=cfg,
        )
        print("created:", sbx.id)

        endpoint = sbx.get_endpoint(44772)
        print("execd endpoint:", endpoint.endpoint)
        print("sdk health:", sbx.is_healthy())
    finally:
        if sbx is not None:
            try:
                sbx.kill()
                print("deleted:", sbx.id)
            finally:
                sbx.close()


if __name__ == "__main__":
    main()
```

> The sample keeps SDK health waiting (`ready_timeout=30m`). Windows cold start can be slow; increase `ready_timeout` as needed. For custom readiness logic, use `skip_health_check=True` and poll for `Running` in your app.

### Advanced options

#### Persistent `/storage`

Bind a host path to `/storage` for a persistent system/user data directory. The directory **need not exist** ahead of time: it may be created on bind mount (fresh), or reused if it already exists. `host.path` must still be under an allowed `storage.allowed_host_paths` prefix, regardless of whether the directory exists.

```python
from opensandbox.models.sandboxes import Host, Volume

volumes = [
    Volume(
        name="win-storage",
        host=Host(path="/data/opensandbox/windows-storage"),
        mount_path="/storage",
        read_only=False,
    ),
]
# Pass as SandboxSync.create(..., volumes=volumes, ...)
```

#### Existing ISO on the host

Bind a local Windows install ISO read-only to `/boot.iso` to avoid downloading media each time (also requires `allowed_host_paths`; the path must be a **file**):

```python
from opensandbox.models.sandboxes import Host, Volume

volumes = [
    Volume(
        name="win-iso",
        host=Host(path="/data/iso/Win11_23H2.iso"),
        mount_path="/boot.iso",
        read_only=True,
    ),
]
```

#### Customizing `dockur/windows`

[`dockur/windows`](https://github.com/dockur/windows) is configured with **environment variables**. OpenSandbox passes your `env` into the container as documented upstream. **Install / UX** settings often include:

- `VERSION`: Windows variant code (e.g. `11`, `11l`, `10` — full table in the project README) or, in some cases, a custom ISO `https://` URL
- `USERNAME` / `PASSWORD`: first user and password (defaults in the image if omitted — see upstream)
- `LANGUAGE`: install language (e.g. `English`, `Chinese`)
- `REGION` / `KEYBOARD`: locale and layout (e.g. `en-US`, `zh-CN`)
- Advanced cases may use `DHCP`, `ARGUMENTS` (extra QEMU args), etc. — see the [`dockur/windows` FAQ](https://github.com/dockur/windows)

If you already set CPU, memory, and disk via `resourceLimits`, do **not** repeat the same **resource** keys in `env` as in the dockur docs; see [`resourceLimits` vs. environment variables](#resourcelimits-vs-environment-variables) above. Other non-resource keys can be combined freely.

```python
env = {
    "VERSION": "11l",
    "USERNAME": "Docker",
    "PASSWORD": "your-secure-password",
    "LANGUAGE": "Chinese",
    "REGION": "zh-CN",
    "KEYBOARD": "zh-CN",
}
```

For the full list, defaults, and interactions, use the [`dockur/windows` repository](https://github.com/dockur/windows) and image tags.

#### Using your own Windows image

In [`dockur/windows` documentation](https://github.com/dockur/windows), **“your own”** usually means **which Windows install ISO to use** — not a custom recipe unrelated to upstream. The [custom install FAQ](https://github.com/dockur/windows#how-do-i-install-a-custom-image) states:

- For an ISO **not** in the project’s version table, set `VERSION` to the ISO’s **download URL** (`https://...`); the container fetches and installs it.
- If you **already have a local ISO file**, bind it to **`/boot.iso`** as in [Existing ISO on the host](#existing-iso-on-the-host) above; upstream documents that `VERSION` is then ignored.

In OpenSandbox, set `VERSION` in `env` and, when needed, add a volume for the local ISO at `/boot.iso`, matching the upstream flow; keep `image` as `dockurr/windows` or another compatible upstream image, as in the other examples.

Snippet for the **`VERSION` = ISO URL** case (same as passing `env` in the minimal example):

```python
env = {
    "VERSION": "https://example.com/path/to/your.iso",
    # same pattern as the minimal example for resource / platform / etc.
}
```

## FAQ

- `Unsupported platform.os 'windows'` on create — server build has no Windows profile; upgrade to a version that includes it.

- `INVALID_PARAMETER` for `resourceLimits.*` — ensure `cpu >= 2`, `memory >= 4G`, `disk >= 64G`.

- Stays `Pending` a long time — first Windows install is slow; check host resources and `/storage` space, and increase wait timeouts.

- Status `Running` but endpoint unreachable — verify `GET /v1/sandboxes/{id}/endpoints/44772` returns a valid address; set `env.USER_PORTS` if you need more ports forwarded.

---

### Reference Code Interpreter Image Migration (`docs/reference/code-interpreter-image-migration.md`)

---
title: Code Interpreter Image Migration
description: Migration guide and repository separation details for the code-interpreter sandbox environment image.
---

# Code Interpreter Image Migration Guide

Issue: [#1764](https://github.com/opensandbox-group/OpenSandbox/issues/1764)

## Background

Historically, the source Dockerfiles, environment setup scripts, and build tooling for the official `opensandbox/code-interpreter` container image lived inside the OpenSandbox monorepo under `sandboxes/code-interpreter/`.

To separate platform runtime and SDK governance from specific sandbox userland environment implementations, the container image implementation has been extracted into a dedicated repository:

- **New repository**: [opensandbox-group/sandbox-images](https://github.com/opensandbox-group/sandbox-images)

## Scope of Separation

### Extracted to Dedicated Repository

The following components and artifacts are now developed, versioned, and published from [opensandbox-group/sandbox-images](https://github.com/opensandbox-group/sandbox-images):

- Dockerfiles (`Dockerfile`, `Dockerfile_base`)
- Environment bootstrap and lifecycle scripts (`code-interpreter.sh`, `code-interpreter-env.sh`, `jupyter_notebook_config.py`)
- Image build scripts and test suites (`build.sh`, `tests/test_code_interpreter_node_setup.sh`)
- Standalone image publication and release workflows

### Retained in OpenSandbox Monorepo

OpenSandbox continues to maintain full support for the code-interpreter experience in the monorepo:

- **Code Interpreter SDKs**: Multi-language SDKs for Python, JavaScript/TypeScript, C#/.NET, Kotlin/Java, and Go (`sdks/sandbox/go/code_interpreter.go`) remain in `sdks/`
- **Execution Daemon & Contracts**: In-sandbox daemon (`components/execd`) and public API contracts (`specs/execd-api.yaml`, `specs/sandbox-lifecycle.yml`)
- **Runnable Examples & Tests**: End-to-end tests and sample applications (`examples/code-interpreter/`)

## Compatibility and Continuity

This migration is purely structural for monorepo maintenance. Container image coordinates, pinned versions, startup paths, and SDK APIs remain fully compatible:

| Property | Status | Details |
|---|---|---|
| **Image Names** | Unchanged | Published to `docker.io/opensandbox/code-interpreter`, `ghcr.io/opensandbox-group/opensandbox/code-interpreter`, and `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter` |
| **Existing Tags** | Valid | Existing pinned tags (such as `v1.1.0`) continue to resolve and run |
| **Startup Path** | Unchanged | Entrypoint `/opt/code-interpreter/code-interpreter.sh` remains standard |
| **Environment Variables** | Unchanged | Language version selection via `PYTHON_VERSION`, `JAVA_VERSION`, `NODE_VERSION`, `GO_VERSION` remains identical |
| **SDK Usage** | Unchanged | Client code using `CodeInterpreter` classes requires no modifications |

### Signature and Provenance Verification

Operators and users who verify container image signatures or build provenance should note the workflow identity:

- **Existing image releases** (such as `v1.1.0` and earlier) were published by the OpenSandbox monorepo and continue using the OpenSandbox workflow identity (`publish-components.yml`).
- **New releases** from [opensandbox-group/sandbox-images](https://github.com/opensandbox-group/sandbox-images) use that repository's `release.yml` workflow identity.
- Users verifying Cosign signatures or provenance attestations for new releases must follow the verification documentation in [opensandbox-group/sandbox-images](https://github.com/opensandbox-group/sandbox-images).

## Contributing and Reporting Issues

- **Container Image Issues or Requests**: If you need new pre-installed packages, updated language versions, or custom kernels in the `code-interpreter` environment image, please submit an issue or pull request at [opensandbox-group/sandbox-images](https://github.com/opensandbox-group/sandbox-images).
- **SDK or Lifecycle Platform Issues**: For SDK bugs, lifecycle control plane features, or `execd` daemon issues, continue contributing to [opensandbox-group/OpenSandbox](https://github.com/opensandbox-group/OpenSandbox).

---

### Reference Execd Path Migration (`docs/reference/execd-path-migration.md`)

---
title: Execd Path Migration
description: Migration guide for the execd install path change from /opt/opensandbox/bin/ to /opt/opensandbox/.
---

# Execd Path Migration Guide

PR: [#1014](https://github.com/opensandbox-group/OpenSandbox/pull/1014)

## Background

OpenSandbox previously installed execd and bootstrap.sh to `/opt/opensandbox/bin/`, creating unnecessary nesting. The execd image already places these artifacts at the root:

```
/execd
/bootstrap.sh
```

While bootstrap.sh defaults to `EXECD=/opt/opensandbox/execd` (no `bin/`), the Kubernetes provider and Pool CR templates historically injected files into a `bin/` subdirectory. This PR flattens the layout:

| Before | After |
|---|---|
| `/opt/opensandbox/bin/execd` | `/opt/opensandbox/execd` |
| `/opt/opensandbox/bin/bootstrap.sh` | `/opt/opensandbox/bootstrap.sh` |
| `/opt/opensandbox/bin/task-executor` | `/opt/opensandbox/task-executor` |

Additionally, the Docker runtime now uses the **full** `bootstrap.sh` from the execd image instead of a minimal 15-line inline-generated shim. This gives Docker sandboxes the same capabilities as Kubernetes: MITM CA trust setup, SIGTERM forwarding, pre-script sourcing, and chained commands.

## Who is affected

| User profile | Impact |
|---|---|
| **code-interpreter image `<=v1.0.2`** | **Must upgrade to `v1.1.0`** — emptyDir at `/opt/opensandbox` shadows old scripts under `/opt/opensandbox/` |
| **Pool CR with custom templates** | Must update execd init container paths and volume mount |
| **Custom execd images** | Must now include `/bootstrap.sh` alongside `/execd` |
| **Docker runtime users (default)** | No manual action — bootstrap.sh is auto-installed |

## Migration steps

### 1. code-interpreter image

**Must upgrade the image from `v1.0.2` to `v1.1.0`.**

In code-interpreter `v1.0.2`, the entrypoint scripts (`code-interpreter.sh`, `code-interpreter-env.sh`) live directly under `/opt/opensandbox/`. Before this PR, the emptyDir volume was mounted at `/opt/opensandbox/bin/`, shadowing only that subdirectory. Now the emptyDir mounts at `/opt/opensandbox/`, shadowing the entire directory — including those scripts. The pod starts but cannot find its own entrypoint.

[PR #1012](https://github.com/opensandbox-group/OpenSandbox/pull/1012) moved all code-interpreter scripts from `/opt/opensandbox/` to `/opt/code-interpreter/`, avoiding the conflict. Version `v1.1.0` is the first image with that change.

```yaml
# In your Pool CR or sandbox creation request:
image: sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:v1.1.0
```

The Python SDK (`opensandbox-code-interpreter`) does not require an upgrade — it communicates with execd over the network and does not reference filesystem paths.

### 2. Pool CR template

If you maintain a custom Pool CR that defines the execd init container, update it:

```yaml
# Before
initContainers:
  - name: execd-installer
    image: opensandbox/execd:latest
    args:
      - |
        cp ./execd /opt/opensandbox/bin/execd &&
        cp ./bootstrap.sh /opt/opensandbox/bin/bootstrap.sh &&
        chmod +x /opt/opensandbox/bin/execd &&
        chmod +x /opt/opensandbox/bin/bootstrap.sh
    volumeMounts:
      - name: opensandbox-bin
        mountPath: /opt/opensandbox/bin

# After
initContainers:
  - name: execd-installer
    image: opensandbox/execd:latest
    args:
      - |
        cp ./execd /opt/opensandbox/execd &&
        cp ./bootstrap.sh /opt/opensandbox/bootstrap.sh &&
        chmod +x /opt/opensandbox/execd &&
        chmod +x /opt/opensandbox/bootstrap.sh
    volumeMounts:
      - name: opensandbox-bin
        mountPath: /opt/opensandbox
```

The same update applies to the task-executor init container if used:

```yaml
# Before
cp /workspace/server /opt/opensandbox/bin/task-executor && chmod +x /opt/opensandbox/bin/task-executor
# After
cp /workspace/server /opt/opensandbox/task-executor && chmod +x /opt/opensandbox/task-executor
```

And the main container's env vars and volume mount:

```yaml
# Before
- name: EXECD
  value: /opt/opensandbox/bin/execd
volumeMounts:
  - name: opensandbox-bin
    mountPath: /opt/opensandbox/bin

# After
- name: EXECD
  value: /opt/opensandbox/execd
volumeMounts:
  - name: opensandbox-bin
    mountPath: /opt/opensandbox
```

### 3. Custom execd images (Docker)

If you override `[runtime].execd_image` with a custom image, that image must now contain `/bootstrap.sh` **in addition to** `/execd`. Previously only `/execd` was required because the Docker runtime generated a minimal inline bootstrap script.

If your custom execd image is built from a Dockerfile that only copies the execd binary, add bootstrap.sh:

```dockerfile
COPY bootstrap.sh /bootstrap.sh
```

The official `opensandbox/execd` image already includes both files and requires no change.

### 4. Custom entrypoint scripts

If you hardcode `/opt/opensandbox/bin/execd` or `/opt/opensandbox/bin/bootstrap.sh` in entrypoint scripts or build pipelines, update those references to `/opt/opensandbox/`.

## Compatibility

### Old paths are not available

After this change, the paths `/opt/opensandbox/bin/execd` and `/opt/opensandbox/bin/bootstrap.sh` no longer exist. Any script, tool, or template referencing them will fail with "file not found".

### How failures manifest

| Scenario | Failure mode |
|---|---|
| Old code-interpreter image (`<=v1.0.2`) | emptyDir at `/opt/opensandbox` shadows `code-interpreter.sh`; pod starts but entrypoint not found |
| Old Pool CR template | execd init container succeeds (it copies to its own mount), but main container cannot find `/opt/opensandbox/bin/bootstrap.sh` |
| Old custom execd image (no bootstrap.sh) | Docker sandbox creation fails with "bootstrap.sh not found in execd image cache" |

### Verification

After migration, confirm the sandbox starts successfully:

```bash
# In a running sandbox:
ls /opt/opensandbox/execd /opt/opensandbox/bootstrap.sh
# Both should exist. Verify /opt/opensandbox/bin/ does NOT exist:
test -d /opt/opensandbox/bin && echo "STALE PATH EXISTS" || echo "OK"
```

## Release notes

For release note authors: include a summary that links to this document.

```markdown
### Breaking: execd install path flattened

execd and bootstrap.sh are now installed to `/opt/opensandbox/` instead of
`/opt/opensandbox/bin/`. code-interpreter image users must upgrade to `>=v1.1.0`.
Custom Pool CR templates and custom execd images require updates.

See [execd Path Migration Guide](/reference/execd-path-migration) for full details.
```

---

### Reference Helm Chart Restructure Migration (`docs/reference/helm-chart-restructure-migration.md`)

---
title: Helm Chart Restructure Migration
description: Migration notes for the charts moving under manifests/charts, the base chart split, and the standalone ingress-gateway chart.
---

# Helm Chart Restructure Migration Guide

Feature: [#1857](https://github.com/opensandbox-group/OpenSandbox/pull/1857)

All Helm charts moved under `manifests/charts/` and were split by component:

| Chart | Directory | Notes |
|---|---|---|
| base (new) | `manifests/charts/base` | CRDs + user-facing CRD RBAC, cluster-scoped |
| controller | `manifests/charts/controller` | was `kubernetes/charts/opensandbox-controller` |
| server | `manifests/charts/server` | was `kubernetes/charts/opensandbox-server` |
| ingress-gateway (new) | `manifests/charts/ingress-gateway` | extracted from the server chart |
| node-agent | `manifests/charts/node-agent` | was `kubernetes/charts/opensandbox-node-agent` |
| opensandbox (umbrella) | `manifests/charts/opensandbox` | aggregates the above |

## ⚠️ Upgrade risks — read first

### 1. Standalone controller upgrades with `crds.keep=false` delete every custom resource

The CRDs were moved out of the controller chart. On `helm upgrade` of an existing controller release, Helm's three-way merge sees the CRDs disappear from the release manifest and deletes them — and Kubernetes cascade-deletes **all** `BatchSandbox`, `Pool`, and `SandboxSnapshot` objects.

- Default installs are safe: the old chart's default `crds.keep=true` puts `helm.sh/resource-policy: keep` on the CRDs, and Helm refuses to delete resources carrying that annotation.
- **If you explicitly set `crds.keep=false`, do not `helm upgrade` in place.** Instead: install `base` first, then upgrade; or verify the CRDs carry the `helm.sh/resource-policy: keep` annotation before upgrading (`kubectl get crd batchsandboxes.sandbox.opensandbox.io -o jsonpath='{.metadata.annotations}'`).

### 2. Umbrella users: server gateway deployment values are silently ignored

The gateway deployment moved from the server chart to the new `ingress-gateway` chart. Umbrella values under `opensandbox-server.server.gateway.*` that only affected the gateway workload (`image`, `replicaCount`, `port`, `dataplaneNamespace`, `providerType`, `logLevel`, `env`, `resources`, scheduling/security keys) are no longer consumed by any template. After upgrading, the gateway either stays off (previous default) or runs with the `ingress-gateway` chart's defaults — your overrides are **not** carried over.

Move those overrides to the `ingress-gateway.*` (umbrella) or `gateway.*` (standalone chart) values before upgrading. Only `enabled`, `host`, `gatewayRouteMode`, and `secureAccess` remain on `opensandbox-server.server.gateway.*` (they render the server's `[ingress]` config announcement).

## Install flow changes

- The controller chart no longer installs CRDs. Standalone installs need the base chart first:

  ```bash
  helm install base manifests/charts/base
  helm install opensandbox-controller manifests/charts/controller \
    --namespace opensandbox-system --create-namespace
  ```

- Umbrella installs are unchanged: `base` is a default-enabled dependency, so `helm install opensandbox ...` still provisions CRDs in one release.

## Release ordering

The docs reference `helm/base/<version>/base-<version>.tgz` release assets. Publish the first `helm/base` tag right after this change merges; until then those links 404 and standalone installs should use the chart sources.

## Unchanged

- Chart names, resource names, selector labels, and the server config-side `server.gateway.{enabled,host,gatewayRouteMode,secureAccess}` keys are preserved, so upgrades are rename-free.
- The umbrella chart still installs everything in a single release.

---

### Reference Snapshot Store Migration (`docs/reference/snapshot-store-migration.md`)

---
title: Snapshot Store Migration
description: Migration guide for switching the server snapshot store from SQLite to PostgreSQL.
---

# Snapshot Store Migration Guide

Feature: [#1653](https://github.com/opensandbox-group/OpenSandbox/pull/1653)

## Background

The server persists the public snapshot catalog in a configurable store. SQLite is the default backend; the PostgreSQL backend is opt-in for operators that need external persistence. The PostgreSQL backend does **not** read an existing SQLite database, so a server switched from `store.type = "sqlite"` to `store.type = "postgresql"` starts with an empty snapshot catalog.

Use the `migrate-snapshots` command to copy existing snapshot records from SQLite to PostgreSQL before switching the store type.

## Before you start

- Stop the server (or at least stop issuing snapshot requests) so the source SQLite database is not modified while it is read.
- The target PostgreSQL database must be reachable and the configured role must be able to create the `snapshots` table.
- Run the command against the same target database the server will use after the switch.

## Migrate

Dry-run first to see what would be copied:

```bash
opensandbox-server migrate-snapshots \
  --from ~/.opensandbox/opensandbox.db \
  --to postgresql://user:password@localhost:5432/opensandbox \
  --dry-run
```

Then run the migration:

```bash
opensandbox-server migrate-snapshots \
  --from ~/.opensandbox/opensandbox.db \
  --to postgresql://user:password@localhost:5432/opensandbox
```

The command prints a summary:

```text
Snapshots migrated: total=42, migrated=42, skipped=0
```

## Behavior

- Records whose id already exists in PostgreSQL are skipped, so the command can be re-run safely. A repeated run reports `migrated=0` and `skipped=42`.
- `--dry-run` reports the counts without writing anything, including without creating the PostgreSQL `snapshots` table, so it works with read-only target credentials.
- The source SQLite database is opened read-only and is never modified, so a backup on a read-only mount can be migrated.
- The PostgreSQL schema is created only on a real migration run, when the table does not already exist.
- Timestamps stored as naive UTC in SQLite are written as `TIMESTAMPTZ` in UTC.

## Switch the server

After migration, update the server configuration:

```toml
[store]
type = "postgresql"

[store.postgresql]
# In production, inject the DSN with OPENSANDBOX_STORE_POSTGRESQL_DSN instead.
dsn = "postgresql://user:password@localhost:5432/opensandbox"
```

Restart the server. Snapshot lookups and restore requests now read the shared PostgreSQL catalog, including the migrated records.

## Verify

- `GET /v1/sandboxes/{id}/snapshots` returns the migrated snapshot records.
- Restoring a sandbox with an existing `snapshotId` succeeds.

---

### Releases 1.1.0 Rc.1 (`docs/releases/1.1.0-rc.1.md`)

Release candidate for the 1.1.0 line and the first end-to-end exercise of the unified umbrella release pipeline (OSEP-0016). It carries the version bump to `1.1.0-rc.1` across charts and image references; no functional changes are included. rc releases ship no SDK artifacts — SDK/CLI packages, dependency ranges, and identity constants move at the 1.1.0 stable bump. Please test the release mechanics — staging image builds, BOM assembly, and held package fan-out — and report issues.

## Highlights

- First umbrella release candidate on the 1.1.0 line
- Platform version aligned to `1.1.0-rc.1` (charts and image references)

## Misc

- release: bump platform version to 1.1.0-rc.1 (#1896)

## Upgrade & Compatibility

- Images: `opensandbox/<component>:release-1.1.0-rc.1` (all three registries)
- Packages: unchanged — SDK/CLI artifacts publish at the 1.1.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

## Artifacts

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

## Installation

```bash
# Platform (Kubernetes) — render the chart at this tag and apply
git clone https://github.com/opensandbox-group/OpenSandbox
git checkout release-1.1.0-rc.1
helm dependency build manifests/charts/opensandbox  # package file:// sub-charts (not committed)
helm template ./manifests/charts/opensandbox | kubectl apply -f -
```

Verify what you installed against the BOM: see [Release Verification](/community/release-verification).

---

### Releases 1.1.0 (`docs/releases/1.1.0.md`)

First stable release of the unified umbrella release line: one platform version — `1.1.0` — covers server, component images, charts, CLI, and every published SDK, cut from one commit and pinned in a signed BOM (versioning starts at `1.1.0`; `1.0.0` was already consumed by legacy releases, so the line skips it by design per the umbrella governance OSEP). Everyone on a legacy per-component version should upgrade; legacy tag namespaces are frozen. Because this is the first umbrella cut, sections carry different baselines — the controller jumps from its May `v0.2.0` release, ingress from July, the JavaScript/C#/Go SDKs from their July releases — so those sections list several months of changes at once.

## Highlights

- First unified umbrella release: every artifact ships at `1.1.0` from tag `release-1.1.0`, with a Sigstore-signed BOM as the digest authority
- First release of the Fast Sandbox integration: a Firecracker microVM runtime behind the FastPath fleets backend, with pause/resume, ingress FastPath routing, and fsb template management in all five SDKs
- Snapshot persistence gains an opt-in PostgreSQL store with multi-process HA and a SQLite migration command, and sandboxes gain preStart/periodic lifecycle hooks

## Server

- Add the FastPath fleets lifecycle backend: create, renew, delete, and status mapping served through FastPath v2, plus end-to-end fsb pause/resume with async 202 semantics and UID fencing ([#1703](https://github.com/opensandbox-group/OpenSandbox/pull/1703) [#1844](https://github.com/opensandbox-group/OpenSandbox/pull/1844))
- Converge snapshot status via a per-namespace watch instead of blocking polls, and add fast-sandbox snapshot support ([#1846](https://github.com/opensandbox-group/OpenSandbox/pull/1846))
- Add an opt-in PostgreSQL snapshot store with pooled connections and CAS semantics, a one-shot `migrate-snapshots` command for moving SQLite records, and multi-process HA coordination for snapshot create/recovery/delete on Kubernetes ([#1653](https://github.com/opensandbox-group/OpenSandbox/pull/1653) [#1670](https://github.com/opensandbox-group/OpenSandbox/pull/1670) [#1724](https://github.com/opensandbox-group/OpenSandbox/pull/1724))
- Transport sandbox lifecycle hooks (OSEP-0020): create requests accept `preStart` and `periodic` hooks, validated and serialized into a reserved environment variable for Kubernetes workloads; the Docker provider rejects lifecycle requests for now ([#1589](https://github.com/opensandbox-group/OpenSandbox/pull/1589) [#1542](https://github.com/opensandbox-group/OpenSandbox/pull/1542))
- Implement a stable, scoped diagnostics API over the existing log/event collectors with `logs:container|all` and `events:runtime|lifecycle|all` scopes, explicit warnings on partial scopes, `400 DIAGNOSTICS_SCOPE_UNSUPPORTED` for unknown scopes, and backward-compatible plain-text responses ([#1553](https://github.com/opensandbox-group/OpenSandbox/pull/1553))
- Export generic HTTP request metrics (request rate, error rate, latency) from the server over OTLP, complementing the SDK-reported create-duration histogram ([#1561](https://github.com/opensandbox-group/OpenSandbox/pull/1561))
- Reject unsupported gVisor (`runsc`) snapshots before persistence with `409 SNAPSHOT::UNSUPPORTED_RUNTIME` ([#1720](https://github.com/opensandbox-group/OpenSandbox/pull/1720))
- Support independent resource requests/limits for the Kubernetes egress sidecar via `[egress.resources]` ([#1620](https://github.com/opensandbox-group/OpenSandbox/pull/1620)), add `[egress] otlp_endpoint` so the sidecar exports metrics to an OpenTelemetry collector on both runtimes ([#1753](https://github.com/opensandbox-group/OpenSandbox/pull/1753)), pause and resume the sidecar together with the sandbox ([#1441](https://github.com/opensandbox-group/OpenSandbox/pull/1441)), make the sidecar readiness wait configurable via `[egress] timeout_seconds` ([#1522](https://github.com/opensandbox-group/OpenSandbox/pull/1522)), and inject `OPENSANDBOX_EGRESS_SANDBOX_ID` server-side so sidecar telemetry can no longer be spoofed by request env ([#1381](https://github.com/opensandbox-group/OpenSandbox/pull/1381))
- Honor `image.auth` in create requests for the agent-sandbox provider with per-sandbox pull secrets that are garbage-collected with the Sandbox CR ([#1789](https://github.com/opensandbox-group/OpenSandbox/pull/1789))
- Source OSEP-0011 secure-access signing keys from the `OPENSANDBOX_SECURE_ACCESS_KEYS` environment variables or a Kubernetes Secret instead of plaintext chart values ([#1445](https://github.com/opensandbox-group/OpenSandbox/pull/1445))
- Make the reverse proxy's internal resolution configurable with `[proxy] resolve_internal` for deployments that cannot route to Docker bridge IPs ([#1631](https://github.com/opensandbox-group/OpenSandbox/pull/1631))
- Raise the `preStart.timeoutSeconds` limit from 300 to 10800 seconds across the server API, execd, and SDKs ([#1637](https://github.com/opensandbox-group/OpenSandbox/pull/1637))
- Add Docker-runtime-wide `sandbox_env` and `sandbox_binds` configuration, giving Docker parity with the Kubernetes pod template for fleet-wide CA bundles, proxies, and mounts ([#1303](https://github.com/opensandbox-group/OpenSandbox/pull/1303))
- Breaking: migrate the agent-sandbox provider and ingress informer to `agents.x-k8s.io/v1beta1` required by agent-sandbox v1.0.0, creating workloads with `operatingMode: Running` and `service: true`; see Upgrade & Compatibility ([#1763](https://github.com/opensandbox-group/OpenSandbox/pull/1763))
- Support pool-only sandbox creation (`poolRef` without entrypoint) instead of rejecting it with HTTP 400, scheduling the execd-start task per allocation ([#1729](https://github.com/opensandbox-group/OpenSandbox/pull/1729)), fail fast on exhausted namespace ResourceQuotas by surfacing the Sandbox CR's admission rejection ([#1752](https://github.com/opensandbox-group/OpenSandbox/pull/1752)), and surface pool back-pressure as HTTP 429 `KUBERNETES::POOL_CAPACITY_EXHAUSTED` with `Retry-After` instead of a generic readiness timeout ([#1581](https://github.com/opensandbox-group/OpenSandbox/pull/1581))
- Propagate template container `securityContext` into generated pods ([#1564](https://github.com/opensandbox-group/OpenSandbox/pull/1564)), honor the PVC `readOnly` flag on the volume source and not just the mount ([#1246](https://github.com/opensandbox-group/OpenSandbox/pull/1246)), allow mixed read-only `subPath` mounts from a shared PVC ([#1440](https://github.com/opensandbox-group/OpenSandbox/pull/1440)), reject `networkPolicy` combined with `extensions.poolRef` ([#1319](https://github.com/opensandbox-group/OpenSandbox/pull/1319)), and patch PVC owner references through the supported strategic-merge path ([#1538](https://github.com/opensandbox-group/OpenSandbox/pull/1538))
- Make the informer-backed cache the only Kubernetes read path by removing the `informer_enabled` flag, and expire stale informer entries when a watch stops delivering ([#1926](https://github.com/opensandbox-group/OpenSandbox/pull/1926) [#1534](https://github.com/opensandbox-group/OpenSandbox/pull/1534))
- Reserve Docker host ports in-process between probing and container start, and retry container creation when the probed port conflicts at Docker bind time ([#1666](https://github.com/opensandbox-group/OpenSandbox/pull/1666) [#1715](https://github.com/opensandbox-group/OpenSandbox/pull/1715))
- Map completed agent-sandbox pods to terminal states ([#1643](https://github.com/opensandbox-group/OpenSandbox/pull/1643)), report `Failed` when a pooled sandbox task fails ([#1663](https://github.com/opensandbox-group/OpenSandbox/pull/1663)), keep direct responses out of the informer cache ([#1674](https://github.com/opensandbox-group/OpenSandbox/pull/1674)), resolve agent-sandbox server-proxy endpoints from `status.podIPs` with IPv6 bracketing ([#1424](https://github.com/opensandbox-group/OpenSandbox/pull/1424)), and use the public state in invalid-resume errors ([#1586](https://github.com/opensandbox-group/OpenSandbox/pull/1586))
- Handle websocket close semantics: map abnormal upstream closures and relay client closes with a legal status code ([#1642](https://github.com/opensandbox-group/OpenSandbox/pull/1642) [#1731](https://github.com/opensandbox-group/OpenSandbox/pull/1731))
- Fix HTTP proxy fidelity: preserve repeated response headers without duplicating Uvicorn's own ([#1858](https://github.com/opensandbox-group/OpenSandbox/pull/1858) [#1471](https://github.com/opensandbox-group/OpenSandbox/pull/1471)), forward the query string unchanged ([#1902](https://github.com/opensandbox-group/OpenSandbox/pull/1902)), rewrite root-relative redirects to stay on the proxy path ([#1516](https://github.com/opensandbox-group/OpenSandbox/pull/1516)), release backend responses when clients abort mid-stream ([#1488](https://github.com/opensandbox-group/OpenSandbox/pull/1488)), stop leaking the ingress routing header on server-proxied endpoints ([#1503](https://github.com/opensandbox-group/OpenSandbox/pull/1503)), and generate unique OpenAPI operation IDs ([#1634](https://github.com/opensandbox-group/OpenSandbox/pull/1634))
- Improve proxy diagnostics: identify backend connection failures ([#1675](https://github.com/opensandbox-group/OpenSandbox/pull/1675)) and describe transparent proxy responses in the spec ([#1701](https://github.com/opensandbox-group/OpenSandbox/pull/1701))
- Preserve the egress auth token on internal proxy requests to port 18080 across Docker and Kubernetes ([#1880](https://github.com/opensandbox-group/OpenSandbox/pull/1880))
- Route Compose proxying through host port mappings ([#1698](https://github.com/opensandbox-group/OpenSandbox/pull/1698))
- Fix Docker runtime edges: skip a missing IPv6 sysctl in Kubernetes init ([#1721](https://github.com/opensandbox-group/OpenSandbox/pull/1721)), use the supported `no-new-privileges` separator ([#1383](https://github.com/opensandbox-group/OpenSandbox/pull/1383)), detect Podman when resolving the Docker host IP ([#1806](https://github.com/opensandbox-group/OpenSandbox/pull/1806)), reject invalid ([#1742](https://github.com/opensandbox-group/OpenSandbox/pull/1742)) and non-finite ([#1568](https://github.com/opensandbox-group/OpenSandbox/pull/1568)) resource limits, and honor `[docker].host_ip` when the server itself runs in a container bound to loopback ([#1918](https://github.com/opensandbox-group/OpenSandbox/pull/1918))
- Define the missing `SandboxErrorCodes.INTERNAL_ERROR` so PVC auto-create failures return a proper 500 ([#1447](https://github.com/opensandbox-group/OpenSandbox/pull/1447)), and report the deployed version consistently across the API, package metadata, and chart ([#1439](https://github.com/opensandbox-group/OpenSandbox/pull/1439))

## SDKs

- Default code-interpreter creation to a strict health check in every language: `CodeInterpreter` now blocks until both execd and the Jupyter kernel gateway are verified ready ([#1754](https://github.com/opensandbox-group/OpenSandbox/pull/1754))
- Add sandbox lifecycle hook models (OSEP-0020) to the API spec and every SDK, with consistent validation aligned to the server's authoritative ranges ([#1588](https://github.com/opensandbox-group/OpenSandbox/pull/1588) [#1605](https://github.com/opensandbox-group/OpenSandbox/pull/1605))
- Wait for endpoint publication when connecting or creating, fixing connections that raced endpoint propagation ([#1751](https://github.com/opensandbox-group/OpenSandbox/pull/1751) [#1830](https://github.com/opensandbox-group/OpenSandbox/pull/1830))
- Resolve the execd and egress endpoints in parallel during create/connect/resume, roughly halving startup endpoint latency ([#1531](https://github.com/opensandbox-group/OpenSandbox/pull/1531))
- Protect the tenant API key: JavaScript and C# no longer forward `OPEN-SANDBOX-API-KEY` to execd/egress in direct data-plane mode ([#1691](https://github.com/opensandbox-group/OpenSandbox/pull/1691))
- Bound readiness polling sleeps across all SDKs ([#1734](https://github.com/opensandbox-group/OpenSandbox/pull/1734) [#1747](https://github.com/opensandbox-group/OpenSandbox/pull/1747) [#1745](https://github.com/opensandbox-group/OpenSandbox/pull/1745) [#1748](https://github.com/opensandbox-group/OpenSandbox/pull/1748) [#1744](https://github.com/opensandbox-group/OpenSandbox/pull/1744))
- Reject invalid inputs: negative readiness polling intervals in Python and JavaScript ([#1826](https://github.com/opensandbox-group/OpenSandbox/pull/1826)), negative session and command timeouts in Python ([#1726](https://github.com/opensandbox-group/OpenSandbox/pull/1726) [#1733](https://github.com/opensandbox-group/OpenSandbox/pull/1733)), negative log cursors in Go and JavaScript ([#1835](https://github.com/opensandbox-group/OpenSandbox/pull/1835) [#1836](https://github.com/opensandbox-group/OpenSandbox/pull/1836)), and a negative code-interpreter polling interval in Go ([#1885](https://github.com/opensandbox-group/OpenSandbox/pull/1885))
- Bring JavaScript, C#, and Go to parity with the Python and Kotlin SDKs: background runs in isolated sessions ([#1456](https://github.com/opensandbox-group/OpenSandbox/pull/1456)), execd-as-init request support ([#1474](https://github.com/opensandbox-group/OpenSandbox/pull/1474)), and pool allocation summaries ([#1481](https://github.com/opensandbox-group/OpenSandbox/pull/1481))
- Add fsb (fast-sandbox) template management to every sandbox SDK ([#1877](https://github.com/opensandbox-group/OpenSandbox/pull/1877) [#1895](https://github.com/opensandbox-group/OpenSandbox/pull/1895) [#1894](https://github.com/opensandbox-group/OpenSandbox/pull/1894) [#1898](https://github.com/opensandbox-group/OpenSandbox/pull/1898) [#1897](https://github.com/opensandbox-group/OpenSandbox/pull/1897))
- Fail fast on readiness auth errors in Python ([#1782](https://github.com/opensandbox-group/OpenSandbox/pull/1782)) and handle an unset `volume.host` safely ([#1659](https://github.com/opensandbox-group/OpenSandbox/pull/1659))
- Document snapshots and isolated sessions in the Python, JavaScript, Kotlin, C#, and Go SDK references, and fix non-compiling README snippets in Kotlin and Go ([#1863](https://github.com/opensandbox-group/OpenSandbox/pull/1863) [#1854](https://github.com/opensandbox-group/OpenSandbox/pull/1854) [#1870](https://github.com/opensandbox-group/OpenSandbox/pull/1870) [#1856](https://github.com/opensandbox-group/OpenSandbox/pull/1856) [#1876](https://github.com/opensandbox-group/OpenSandbox/pull/1876) [#1888](https://github.com/opensandbox-group/OpenSandbox/pull/1888))

### Python

- Align the client pool with Kotlin: `warmup_create_qps`, staged readiness, leader-epoch fencing, and opt-in OpenTelemetry warmup tracing ([#1889](https://github.com/opensandbox-group/OpenSandbox/pull/1889) [#1697](https://github.com/opensandbox-group/OpenSandbox/pull/1697))
- Preserve replacement endpoint fetches when an earlier cache fetch completes out of order ([#1567](https://github.com/opensandbox-group/OpenSandbox/pull/1567)), and keep pool cleanup running when `acquire()` is cancelled mid-renewal ([#1938](https://github.com/opensandbox-group/OpenSandbox/pull/1938))
- Preserve Unicode separators in SSE data ([#1444](https://github.com/opensandbox-group/OpenSandbox/pull/1444))
- Carry the API key on execd requests per `use_server_proxy` ([#1687](https://github.com/opensandbox-group/OpenSandbox/pull/1687))
- MCP: constrain the SDK to MCP 1.x with a guard and migrate the sandbox server to the MCP 2.x API ([#1725](https://github.com/opensandbox-group/OpenSandbox/pull/1725) [#1737](https://github.com/opensandbox-group/OpenSandbox/pull/1737)), add a `--use-server-proxy` option ([#1509](https://github.com/opensandbox-group/OpenSandbox/pull/1509)), and expose streamable HTTP host/port options ([#1929](https://github.com/opensandbox-group/OpenSandbox/pull/1929))

### JavaScript

- Add a client-side `SandboxPool` aligned with Kotlin: staged warmup, FIFO idle acquisition, lifecycle fencing, and optional Redis-backed distributed state ([#1716](https://github.com/opensandbox-group/OpenSandbox/pull/1716) [#1875](https://github.com/opensandbox-group/OpenSandbox/pull/1875))
- Bound pool readiness checks and align pool warmup resources with Python ([#1793](https://github.com/opensandbox-group/OpenSandbox/pull/1793) [#1909](https://github.com/opensandbox-group/OpenSandbox/pull/1909)), and always make the final warmup attempt when the initial delay is capped by the readiness deadline ([#1927](https://github.com/opensandbox-group/OpenSandbox/pull/1927))
- Allow aborting `Sandbox.create()` and `Sandbox.connect()` via `AbortSignal`, with background cleanup after cancellation ([#1685](https://github.com/opensandbox-group/OpenSandbox/pull/1685))
- Stop background SSE streams on the complete event ([#1532](https://github.com/opensandbox-group/OpenSandbox/pull/1532)), include response bodies in client error messages ([#1496](https://github.com/opensandbox-group/OpenSandbox/pull/1496)), drop readiness network hints ([#1493](https://github.com/opensandbox-group/OpenSandbox/pull/1493)), and escape the multipart filename on streamed uploads ([#1862](https://github.com/opensandbox-group/OpenSandbox/pull/1862))

### Kotlin

- Add structured exceptions for sandbox-not-found and capacity-exceeded outcomes plus a typed exception for proxy backend-connection failures ([#1667](https://github.com/opensandbox-group/OpenSandbox/pull/1667) [#1683](https://github.com/opensandbox-group/OpenSandbox/pull/1683))
- Send an empty egress array instead of null ([#1630](https://github.com/opensandbox-group/OpenSandbox/pull/1630))

### C#

- Add fsb (fast-sandbox) template CRUD to the sandbox SDK ([#1898](https://github.com/opensandbox-group/OpenSandbox/pull/1898))

### Go

- Merge `poolredis` into the parent Go module; the import path is unchanged ([#1903](https://github.com/opensandbox-group/OpenSandbox/pull/1903))
- Add a `SandboxPoolManager` with fence/tombstone parity ([#1525](https://github.com/opensandbox-group/OpenSandbox/pull/1525))
- Release idle sandboxes with bounded concurrency and bound completion-driven pool reconcile ([#1475](https://github.com/opensandbox-group/OpenSandbox/pull/1475) [#1494](https://github.com/opensandbox-group/OpenSandbox/pull/1494))
- Stop capping SSE event lines at 4 MiB ([#1881](https://github.com/opensandbox-group/OpenSandbox/pull/1881))

## Controller

The controller section carries everything since the last standalone controller release (`k8s/controller/v0.2.0`, May 2026).

- Stabilize pool scheduling: a capacity predicate in pool-assign, a fix for pool scale expectations getting stuck, and scaling stabilization ([#1143](https://github.com/opensandbox-group/OpenSandbox/pull/1143) [#906](https://github.com/opensandbox-group/OpenSandbox/pull/906) [#1425](https://github.com/opensandbox-group/OpenSandbox/pull/1425))
- Add lifecycle hooks to the task schedule ([#420](https://github.com/opensandbox-group/OpenSandbox/pull/420)) and emit Kubernetes events for BatchSandbox and Pool lifecycle ([#1099](https://github.com/opensandbox-group/OpenSandbox/pull/1099))
- Support opt-in PVC deletion together with sandboxes via the Pool recycle strategy ([#880](https://github.com/opensandbox-group/OpenSandbox/pull/880))
- Add a containerd socket path flag to the controller ([#859](https://github.com/opensandbox-group/OpenSandbox/pull/859))
- Rework the image committer: a reusable `pkg/imagecommitter` interface with a containerd-native default implementation, source-registry authentication, self-signed/plain-HTTP transport policy, and an operator-controlled Job template overlay ([#1431](https://github.com/opensandbox-group/OpenSandbox/pull/1431)), plus pull secrets for authenticated registry proxies ([#1300](https://github.com/opensandbox-group/OpenSandbox/pull/1300))
- Add QEMU VMState pause/resume support in the Kubernetes runtime path ([#1584](https://github.com/opensandbox-group/OpenSandbox/pull/1584))
- Sync Kata guests before snapshot commit and fall back to stopped snapshot containers ([#1356](https://github.com/opensandbox-group/OpenSandbox/pull/1356))
- Sync SandboxSnapshot RBAC ([#1088](https://github.com/opensandbox-group/OpenSandbox/pull/1088)) and add the missing `pods/log` permission to the server ClusterRole ([#1211](https://github.com/opensandbox-group/OpenSandbox/pull/1211))
- Export low-cardinality Pool and BatchSandbox capacity gauges over OTLP/HTTP from the elected controller, computed from actual Pod specs ([#1651](https://github.com/opensandbox-group/OpenSandbox/pull/1651))
- Helm: default the server deployment to one active replica ([#1723](https://github.com/opensandbox-group/OpenSandbox/pull/1723)), allow configuring the server Service type ([#1628](https://github.com/opensandbox-group/OpenSandbox/pull/1628)), expose the controller-runtime metrics endpoint in the controller chart ([#1331](https://github.com/opensandbox-group/OpenSandbox/pull/1331)), add scheduling/tolerations fields for the server and ingress-gateway Deployments ([#1245](https://github.com/opensandbox-group/OpenSandbox/pull/1245) [#1235](https://github.com/opensandbox-group/OpenSandbox/pull/1235)), roll the server Deployment on `config.toml` changes via a checksum annotation ([#1473](https://github.com/opensandbox-group/OpenSandbox/pull/1473)), render `imagePullSecrets` on the ingress-gateway Deployment ([#1236](https://github.com/opensandbox-group/OpenSandbox/pull/1236)), allow extra env vars on the ingress-gateway container ([#1333](https://github.com/opensandbox-group/OpenSandbox/pull/1333)), sync the missing Pool CRD strategy fields into the chart CRD ([#1337](https://github.com/opensandbox-group/OpenSandbox/pull/1337)), remove the dead `networkPolicy` block ([#1243](https://github.com/opensandbox-group/OpenSandbox/pull/1243)), guard empty `extraVolumes`/`extraVolumeMounts` rendering ([#1244](https://github.com/opensandbox-group/OpenSandbox/pull/1244)), fix the `containerdSocketPath` CrashLoopBackOff against controller v0.2.x and bump the umbrella chart to 0.2.2 ([#1557](https://github.com/opensandbox-group/OpenSandbox/pull/1557)), and source secure-access keys from a Secret ([#1445](https://github.com/opensandbox-group/OpenSandbox/pull/1445))
- Restructure all charts under `manifests/charts/` ([#1857](https://github.com/opensandbox-group/OpenSandbox/pull/1857))
- Release stranded BatchSandbox finalizers when the referenced Pool is missing or terminating, and stop recreating an idle task scheduler after cleanup ([#1657](https://github.com/opensandbox-group/OpenSandbox/pull/1657))
- Fail sandbox recovery when the executor status is unknown, fail the sandbox when the main container terminates without restart while the Pod stays `Running`, and propagate terminal Pod failures to sandbox status ([#1599](https://github.com/opensandbox-group/OpenSandbox/pull/1599) [#1741](https://github.com/opensandbox-group/OpenSandbox/pull/1741) [#1722](https://github.com/opensandbox-group/OpenSandbox/pull/1722))
- Fix pool reconciliation: compare replica counts by value, trigger reconcile when `poolRef` changes, and stop attributing failures of already-deleting Pods ([#1779](https://github.com/opensandbox-group/OpenSandbox/pull/1779) [#1794](https://github.com/opensandbox-group/OpenSandbox/pull/1794) [#1828](https://github.com/opensandbox-group/OpenSandbox/pull/1828))
- Improve controller logging and performance ([#919](https://github.com/opensandbox-group/OpenSandbox/pull/919))

## Execd

- Support native `argv` command execution in `/command` (literal arguments without shell parsing, environment-aware executable resolution), wired through all five SDKs ([#1760](https://github.com/opensandbox-group/OpenSandbox/pull/1760))
- Add a runtime-init handshake: `POST /init` applies a one-shot RuntimeBinding (authoritative sandbox id, token hash, envs, lifecycle config, telemetry attributes) with `GET /ready` reporting readiness, moving identity out of image build time ([#1871](https://github.com/opensandbox-group/OpenSandbox/pull/1871)); fast-sandbox restores can rebind a restored runtime via `preserveRuntimeState` ([#1946](https://github.com/opensandbox-group/OpenSandbox/pull/1946))
- Execute the runtime side of sandbox lifecycle hooks (OSEP-0020): `preStart` gates the user entrypoint, non-overlapping `periodic` hooks run through the init-reaper-aware process path, with fail-closed TERM/KILL watchdogs and atomic lifecycle config persistence ([#1587](https://github.com/opensandbox-group/OpenSandbox/pull/1587))
- Fix `EXECD_ENVS` handling with lossless dotenv-style round-tripping and apply the env file to bash session env and cwd expansion ([#1893](https://github.com/opensandbox-group/OpenSandbox/pull/1893) [#1868](https://github.com/opensandbox-group/OpenSandbox/pull/1868))
- Drop the `fsync` from the task-create critical path that could stall sandbox startup for seconds on congested block devices ([#1614](https://github.com/opensandbox-group/OpenSandbox/pull/1614))
- Bound command output retention ([#1710](https://github.com/opensandbox-group/OpenSandbox/pull/1710)), clamp background log cursors to the end of the log ([#1708](https://github.com/opensandbox-group/OpenSandbox/pull/1708)), and avoid re-reading unfinished command output ([#1746](https://github.com/opensandbox-group/OpenSandbox/pull/1746))
- Validate command `cwd` against request-scoped environment variables ([#1611](https://github.com/opensandbox-group/OpenSandbox/pull/1611)), make `bootstrap.sh` readable by non-root workload users ([#1619](https://github.com/opensandbox-group/OpenSandbox/pull/1619)), and stop PTY creation from appending a success body after an error ([#1603](https://github.com/opensandbox-group/OpenSandbox/pull/1603))
- Reclaim stale overlay upper dirs at startup ([#1784](https://github.com/opensandbox-group/OpenSandbox/pull/1784))
- Skip credential switches for same-identity uid/gid requests ([#1809](https://github.com/opensandbox-group/OpenSandbox/pull/1809))
- Surface NSS trust prerequisites ([#1712](https://github.com/opensandbox-group/OpenSandbox/pull/1712))
- Shorten the default SSE drain timeout ([#1840](https://github.com/opensandbox-group/OpenSandbox/pull/1840))

## Networking

### egress

- Fleet profile **[EXPERIMENTAL]**: multi-sandbox egress control plane with per-subject credential vaults, shared MITM data plane with per-subject DNAT and subject-aware active vault, DoH-443 blocking with a blocklist, per-sandbox netns OUTPUT rules with connection refresh, and a fastlet Sandbox Actions handler protocol replacing file observation ([#1595](https://github.com/opensandbox-group/OpenSandbox/pull/1595) [#1608](https://github.com/opensandbox-group/OpenSandbox/pull/1608) [#1633](https://github.com/opensandbox-group/OpenSandbox/pull/1633) [#1678](https://github.com/opensandbox-group/OpenSandbox/pull/1678))
- Credential-bound TLS interception foundation **[EXPERIMENTAL]**: host selector, request-weighted TLS shadow observations, and the revision snapshot pipeline (transaction receiver, coordinator, Unix transport, receiver endpoint, decision snapshots, validation, process-session provisioning, bootstrap, and outcome reconciliation, lifecycle wiring) — groundwork only, not enabled by default ([#1775](https://github.com/opensandbox-group/OpenSandbox/pull/1775) [#1777](https://github.com/opensandbox-group/OpenSandbox/pull/1777) [#1791](https://github.com/opensandbox-group/OpenSandbox/pull/1791) [#1800](https://github.com/opensandbox-group/OpenSandbox/pull/1800) [#1812](https://github.com/opensandbox-group/OpenSandbox/pull/1812) [#1834](https://github.com/opensandbox-group/OpenSandbox/pull/1834) [#1839](https://github.com/opensandbox-group/OpenSandbox/pull/1839) [#1845](https://github.com/opensandbox-group/OpenSandbox/pull/1845) [#1850](https://github.com/opensandbox-group/OpenSandbox/pull/1850) [#1867](https://github.com/opensandbox-group/OpenSandbox/pull/1867) [#1892](https://github.com/opensandbox-group/OpenSandbox/pull/1892) [#1915](https://github.com/opensandbox-group/OpenSandbox/pull/1915))
- Make Credential Vault create/patch/delete immediately visible to the next intercepted flow (replacing the 0.5s cache window), and add opt-in strict scope enforcement rejecting bindings without method/path matches or with host-wide `/*` paths ([#1755](https://github.com/opensandbox-group/OpenSandbox/pull/1755) [#1762](https://github.com/opensandbox-group/OpenSandbox/pull/1762))
- Support kernels without xtables compat and IPv6 transports with a native nftables redirect backend (`OPENSANDBOX_EGRESS_REDIRECT_BACKEND`), and renew timed nft elements atomically ([#1861](https://github.com/opensandbox-group/OpenSandbox/pull/1861) [#1786](https://github.com/opensandbox-group/OpenSandbox/pull/1786))
- Cut DNS read amplification in domain revalidation ([#1808](https://github.com/opensandbox-group/OpenSandbox/pull/1808)), revalidate observed domain addresses ([#1804](https://github.com/opensandbox-group/OpenSandbox/pull/1804)), log a warning on DNS denies ([#1807](https://github.com/opensandbox-group/OpenSandbox/pull/1807)), and shrink the DNS upstream ejection window so unhealthy resolvers recover faster ([#1943](https://github.com/opensandbox-group/OpenSandbox/pull/1943))
- Drain webhooks during bounded Docker deletion so egress shutdown cannot hang sandbox deletion ([#1827](https://github.com/opensandbox-group/OpenSandbox/pull/1827))
- Report the sidecar's own resource usage from its cgroup ([#1411](https://github.com/opensandbox-group/OpenSandbox/pull/1411))

### ingress

- Add fleets FastPath routing with tenant-scoped `(namespace, sandbox_id, port)` resolution, HMAC-bound route scopes, and short-lived route caching ([#1577](https://github.com/opensandbox-group/OpenSandbox/pull/1577))
- Observe upstream network readiness with a bounded shadow assessment exposed at `/status.ok/network-readiness` ([#1693](https://github.com/opensandbox-group/OpenSandbox/pull/1693))
- Add OpenTelemetry metrics instrumentation (OSEP-0010): request count/duration, routing resolution, proxy-type counters, and system CPU/memory plus active-connection gauges ([#1181](https://github.com/opensandbox-group/OpenSandbox/pull/1181))
- Migrate the WebSocket proxy off the archived `gorilla/websocket` to `coder/websocket` and fix `Connection` header token-list detection ([#1822](https://github.com/opensandbox-group/OpenSandbox/pull/1822))
- Verify route signatures with a constant-time comparison ([#1417](https://github.com/opensandbox-group/OpenSandbox/pull/1417))
- Eliminate an SSE ping goroutine race and a proxy error leak ([#1216](https://github.com/opensandbox-group/OpenSandbox/pull/1216))
- Serve only intentionally registered routes by replacing `http.DefaultServeMux` with a dedicated mux ([#1224](https://github.com/opensandbox-group/OpenSandbox/pull/1224))
- Shared egress/ingress telemetry: honor `OTEL_SDK_DISABLED=true` and `OTEL_METRICS_EXPORTER=none` and respect caller-chosen metric temporality ([#1783](https://github.com/opensandbox-group/OpenSandbox/pull/1783))

## Fast Sandbox

First release with [fast-sandbox](https://github.com/opensandbox-group/fast-sandbox): an external, template-backed sandbox platform that provisions sandboxes as Firecracker microVMs from golden-image templates, exposed to clients through a FastPath v2 gRPC control plane ([#1558](https://github.com/opensandbox-group/OpenSandbox/pull/1558) [#1559](https://github.com/opensandbox-group/OpenSandbox/pull/1559)). OpenSandbox's server, ingress, execd, and SDKs integrate with it as the `fast-sandbox` runtime provider. The upstream source this release builds against is pinned at `manifests/third-party/fast-sandbox.commit` (`d51fa69`, first pinned `1a420a22`, [#1899](https://github.com/opensandbox-group/OpenSandbox/pull/1899)); from that commit the release builds the Firecracker-scope image family only — `fsb-controller`, `fsb-fastlet`, `fsb-fastlet-proxy`, `fsb-janitor`, `fsb-firecracker-runtime`, `fsb-sandboxtemplate-builder` — and vendors its CRDs, deployable with the new `fast-sandbox` Helm chart ([#1869](https://github.com/opensandbox-group/OpenSandbox/pull/1869))

## Misc

- CLI: add Credential Vault management commands ([#1027](https://github.com/opensandbox-group/OpenSandbox/pull/1027)), report pause request acceptance ([#1537](https://github.com/opensandbox-group/OpenSandbox/pull/1537)), pass native argv to `command run` without shell quoting ([#1829](https://github.com/opensandbox-group/OpenSandbox/pull/1829)), preserve local files when downloads fail, and restore downloads to devices and named pipes ([#1820](https://github.com/opensandbox-group/OpenSandbox/pull/1820) [#1847](https://github.com/opensandbox-group/OpenSandbox/pull/1847))
- node-agent: add durable sandbox log collection and a compile-time multi-Source architecture (isolated Source state, event multiplexing, acknowledgement routing) ([#1435](https://github.com/opensandbox-group/OpenSandbox/pull/1435) [#1656](https://github.com/opensandbox-group/OpenSandbox/pull/1656)), and add an opt-in eBPF `syscalls` Source emitting ordered NDJSON syscall records to file/OSS sinks with least-privilege Helm capabilities ([#1682](https://github.com/opensandbox-group/OpenSandbox/pull/1682))
- Images: publish component images to GHCR alongside Docker Hub and ACR ([#1161](https://github.com/opensandbox-group/OpenSandbox/pull/1161)) and document registry provenance with digest-pinning verification guidance ([#1339](https://github.com/opensandbox-group/OpenSandbox/pull/1339))
- Move the code-interpreter sandbox image to the standalone opensandbox-group/sandbox-images repository; the image keeps versioning independently of the umbrella ([#1796](https://github.com/opensandbox-group/OpenSandbox/pull/1796))
- Resolve Dependabot security alerts and bump vulnerable dependencies: grpc, containerd, ebpf-go, cel-go, x/crypto, x/net, vitest, js-yaml, fast-uri, @humanfs/node, selinux, postcss, anyio, and otel-go ([#1545](https://github.com/opensandbox-group/OpenSandbox/pull/1545) [#1736](https://github.com/opensandbox-group/OpenSandbox/pull/1736) [#1838](https://github.com/opensandbox-group/OpenSandbox/pull/1838) [#1390](https://github.com/opensandbox-group/OpenSandbox/pull/1390) [#1250](https://github.com/opensandbox-group/OpenSandbox/pull/1250) [#1385](https://github.com/opensandbox-group/OpenSandbox/pull/1385) [#1467](https://github.com/opensandbox-group/OpenSandbox/pull/1467) [#1921](https://github.com/opensandbox-group/OpenSandbox/pull/1921))
- Docs: refresh the architecture overview, add the contributor advancement process, publish the OSEP set (pause/resume, resilient SDK transport, lifecycle hooks, multi-sandbox egress, TLS interception, execd-as-init, node-agent collection), add isolation-session, client-pool, and secure-access guides, extend the Claude Code and Codex CLI examples and add Deep Agents and OpenCode examples, use pip/uv install tabs, clarify pooled sandbox readiness, refresh SDK capability pages, and trim the README ([#1848](https://github.com/opensandbox-group/OpenSandbox/pull/1848) [#1761](https://github.com/opensandbox-group/OpenSandbox/pull/1761) [#1896](https://github.com/opensandbox-group/OpenSandbox/pull/1896) [#1149](https://github.com/opensandbox-group/OpenSandbox/pull/1149) [#1368](https://github.com/opensandbox-group/OpenSandbox/pull/1368) [#1542](https://github.com/opensandbox-group/OpenSandbox/pull/1542) [#1592](https://github.com/opensandbox-group/OpenSandbox/pull/1592) [#1714](https://github.com/opensandbox-group/OpenSandbox/pull/1714) [#1394](https://github.com/opensandbox-group/OpenSandbox/pull/1394) [#1428](https://github.com/opensandbox-group/OpenSandbox/pull/1428) [#1346](https://github.com/opensandbox-group/OpenSandbox/pull/1346) [#1352](https://github.com/opensandbox-group/OpenSandbox/pull/1352) [#1349](https://github.com/opensandbox-group/OpenSandbox/pull/1349) [#1774](https://github.com/opensandbox-group/OpenSandbox/pull/1774) [#1803](https://github.com/opensandbox-group/OpenSandbox/pull/1803) [#1797](https://github.com/opensandbox-group/OpenSandbox/pull/1797) [#1535](https://github.com/opensandbox-group/OpenSandbox/pull/1535) [#1544](https://github.com/opensandbox-group/OpenSandbox/pull/1544) [#1727](https://github.com/opensandbox-group/OpenSandbox/pull/1727) [#1821](https://github.com/opensandbox-group/OpenSandbox/pull/1821) [#1696](https://github.com/opensandbox-group/OpenSandbox/pull/1696) [#1728](https://github.com/opensandbox-group/OpenSandbox/pull/1728) [#1852](https://github.com/opensandbox-group/OpenSandbox/pull/1852) [#1912](https://github.com/opensandbox-group/OpenSandbox/pull/1912))
- Scripts: bootstrap a source-built fast-sandbox integration environment with source-built server and ingress ([#1756](https://github.com/opensandbox-group/OpenSandbox/pull/1756) [#1769](https://github.com/opensandbox-group/OpenSandbox/pull/1769))
- CI/release: enforce repository and release safeguards (tag immutability, protected release environment, scoped required checks) ([#1274](https://github.com/opensandbox-group/OpenSandbox/pull/1274)), gate release approval on the consistency scan and make the umbrella release idempotent ([#1911](https://github.com/opensandbox-group/OpenSandbox/pull/1911)), render the BOM from channel templates and publish images directly to release tags ([#1920](https://github.com/opensandbox-group/OpenSandbox/pull/1920) [#1925](https://github.com/opensandbox-group/OpenSandbox/pull/1925)), stop auto-bumping the server chart on release ([#1646](https://github.com/opensandbox-group/OpenSandbox/pull/1646)), pass the gateway route mode in e2e chart values ([#1874](https://github.com/opensandbox-group/OpenSandbox/pull/1874)), and remove the verify-license workflow ([#1813](https://github.com/opensandbox-group/OpenSandbox/pull/1813))
- Update code copyright to The OpenSandbox Authors across remaining components ([#1878](https://github.com/opensandbox-group/OpenSandbox/pull/1878) [#1945](https://github.com/opensandbox-group/OpenSandbox/pull/1945) [#1942](https://github.com/opensandbox-group/OpenSandbox/pull/1942) [#1890](https://github.com/opensandbox-group/OpenSandbox/pull/1890))

## Upgrade & Compatibility

- Breaking: the agent-sandbox provider and ingress require `agents.x-k8s.io/v1beta1` (agent-sandbox v1.0.0); install the beta CRDs/controller before upgrading — see the migration guide in [#1763](https://github.com/opensandbox-group/OpenSandbox/pull/1763)
- Breaking for chart users: charts moved to `manifests/charts/` ([#1857](https://github.com/opensandbox-group/OpenSandbox/pull/1857))
- Config: the beta `informer_enabled` flag was removed; the informer-backed cache is now the only Kubernetes read path (it was already the default, so remove the key when upgrading) ([#1926](https://github.com/opensandbox-group/OpenSandbox/pull/1926))
- Behavior change: code-interpreter creation blocks until strictly ready by default ([#1754](https://github.com/opensandbox-group/OpenSandbox/pull/1754)); set the previous lenient behavior explicitly if you depend on it
- Behavior change: lifecycle hooks are validated at create time and rejected on the Docker provider in this release ([#1589](https://github.com/opensandbox-group/OpenSandbox/pull/1589))
- Go module: `poolredis` is merged into the parent module `github.com/alibaba/OpenSandbox/sdks/sandbox/go` with the import path unchanged; a single companion tag now versions both ([#1903](https://github.com/opensandbox-group/OpenSandbox/pull/1903))
- Images: `opensandbox/<component>:release-1.1.0` (all three registries)
- Packages: server / CLI / SDKs all at `1.1.0`
- 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 ❤️

- @7487
- @aGallea
- @akshar27
- @alahaiyo
- @AyushSonuu
- @bcfre
- @bcho
- @BigVan
- @bleakbelladonnals
- @Borda
- @changren-wcr
- @cwj2001
- @DanielXuuuuu
- @Dasen-CAI
- @dcaminos
- @Dev-next-gen
- @ekrasnoyarov
- @Elioooon
- @FAUST-BENCHOU
- @Federaffo
- @fengcone
- @ferponse
- @gegemeimingzi
- @GodBlf
- @Gujiassh
- @hi-neason
- @hittyt
- @hpliStartAgain
- @HUAN2022A
- @jianpingpei
- @jiawen7777
- @Joey777210
- @Junaid-PK
- @jwx0925
- @khandar-william
- @kittimzhe
- @korcan-kai
- @Kuang-xianxin
- @KXHXK
- @L4XB
- @lizhengfeng101
- @LorenzoRD2003
- @luffy0x
- @Luzhanpeng23
- @lyydsheep
- @mameikagou
- @MannXo
- @MaxFreedomPollard
- @mengdehong
- @MGuardia10
- @mpu
- @Mubelotix
- @mythic-p
- @ninan-nn
- @Pangjiping
- @ruirui6946
- @ryanzhang-oss
- @shenshouer
- @Spground
- @tea-artist
- @TuGou-a
- @vedmaka
- @vinkiYu
- @wakqasahmed
- @wangzhiyuan-pixel
- @wlmvp
- @wuzhihui1123
- @y4ney
- @yanpgwang
- @Yeaury
- @Zeeshan-Chaudhry
- @zhangfane
- @zpzjzj

## Artifacts

- BOM: [docs/releases/1.1.0.yaml](./1.1.0.yaml)
- Images: `opensandbox/<component>:release-1.1.0` (Docker Hub / GHCR / ACR)
- Packages: PyPI ×5, npm ×2, Maven Central ×5, NuGet ×2 — all at `1.1.0`
- Go module: `github.com/alibaba/OpenSandbox/sdks/sandbox/go@v1.1.0`

## Installation

```bash
# Platform (Kubernetes) — render the chart at this tag and apply
git clone https://github.com/opensandbox-group/OpenSandbox
git checkout release-1.1.0
helm dependency build manifests/charts/opensandbox  # package file:// sub-charts (not committed)
helm template ./manifests/charts/opensandbox | kubectl apply -f -
# or point your GitOps platform (Argo / Flux) at the repo path + tag

# SDKs
pip install opensandbox==1.1.0            # Python
npm install @alibaba-group/opensandbox@1.1.0   # JavaScript
# Kotlin/JVM: implementation("com.alibaba.opensandbox:sandbox:1.1.0")
dotnet add package Alibaba.OpenSandbox --version 1.1.0
go get github.com/alibaba/OpenSandbox/sdks/sandbox/go@v1.1.0

# Run the server locally
uvx opensandbox-server==1.1.0
```

---

### Releases 1.1.1 Rc.1 (`docs/releases/1.1.1-rc.1.md`)

First release candidate on the 1.1.1 patch line, cut to fix the broken `1.1.0` server wheel: hatchling's VCS-exclude filtering silently dropped the committed FastPath gRPC stubs from the PyPI package, so every `1.1.0` install of `opensandbox-server` failed at import time — PyPI is immutable, so the patched line is `1.1.1` ([#1959](https://github.com/opensandbox-group/OpenSandbox/pull/1959)). Beyond that fix, this rc carries everything merged since `release-1.1.0`: agent-sandbox pause/resume, shared-namespace tenant isolation, watch-driven create latency, `commands.setEnv` and a systematic audit cleanup across all five SDKs, upstream-proxy chaining for egress, and two fast-sandbox pin bumps. Baseline for every section is `release-1.1.0`. rc releases publish images but hold packages — SDK/CLI artifacts ship at the `1.1.1` stable bump. Please test and report issues.

## Highlights

- Fixes the `1.1.0` server wheel: FastPath stubs now ship in the package and installs import cleanly ([#1959](https://github.com/opensandbox-group/OpenSandbox/pull/1959))
- Server: pause/resume on the agent-sandbox provider, tenant isolation for sandboxes sharing a Kubernetes namespace, and pool creates woken by watch events instead of 1-second polling ([#1975](https://github.com/opensandbox-group/OpenSandbox/pull/1975) [#1973](https://github.com/opensandbox-group/OpenSandbox/pull/1973) [#1996](https://github.com/opensandbox-group/OpenSandbox/pull/1996))
- SDKs: `commands.setEnv` runtime env injection in all five languages, plus a systematic cross-language audit cleanup fixing dozens of correctness bugs ([#2013](https://github.com/opensandbox-group/OpenSandbox/pull/2013) [#2020](https://github.com/opensandbox-group/OpenSandbox/pull/2020))

## Server

- Implement pause/resume for the agent-sandbox provider by bridging to `spec.operatingMode` (`Running`/`Suspended`) on `agents.x-k8s.io/v1beta1`, with state derivation (`Paused`/`Pausing`/`Resuming`/`Terminated`), resourceVersion preconditions, and 409 on invalid transitions; both endpoints previously returned "not supported" ([#1975](https://github.com/opensandbox-group/OpenSandbox/pull/1975))
- Isolate tenants that share one Kubernetes namespace: creates stamp `opensandbox.io/tenant`, and list/get/delete/pause/resume/proxy hide other tenants' labeled sandboxes as 404 while unlabeled legacy sandboxes stay visible until expiry; also adds `kubernetes.insecure_skip_tls_verify` for ServiceAccount CA mismatches ([#1973](https://github.com/opensandbox-group/OpenSandbox/pull/1973))
- Heal the silent renew-intent drop after a server restart in multi-tenant mode: the resolver now falls back to a cluster-wide label lookup with a 403 circuit breaker and structured warnings, trusts an ingress-observed namespace from the intent payload when verifiable, and single-tenant deployments never resolve beyond the configured namespace ([#1819](https://github.com/opensandbox-group/OpenSandbox/pull/1819))
- Add `[docker] publish_host` to publish bridge-mode sandbox ports on one host address (default `0.0.0.0` unchanged) — with `127.0.0.1` or the Docker bridge gateway, the tokenless execd port no longer lands on public interfaces ([#1954](https://github.com/opensandbox-group/OpenSandbox/pull/1954))
- Add a typed, admin-only `[egress.upstream_proxy]` config section (URL + optional authorization, validated at load) that injects the sidecar's chained upstream CONNECT proxy on both Docker and Kubernetes, with fail-closed 400s when the requested `networkPolicy` is incompatible; secret values are no longer echoed in config-validation errors ([#1999](https://github.com/opensandbox-group/OpenSandbox/pull/1999))
- Accept `env` on template create and bake it into the fast-sandbox golden image (`/etc/sandbox-init.env`, inheriting the source image's OCI `Config.Env`), closing the gap where `SandboxTemplate.spec.envs` existed but the lifecycle API rejected it; supported across all SDKs ([#1984](https://github.com/opensandbox-group/OpenSandbox/pull/1984))
- Wake Kubernetes creation waits on informer watch events instead of fixed 1-second polling, cutting prewarmed-pool create latency from ~1 s to tens of milliseconds while keeping the polling fallback ([#1996](https://github.com/opensandbox-group/OpenSandbox/pull/1996))
- Ship the committed FastPath gRPC stubs in the server wheel by force-including them past hatchling's VCS excludes, and smoke-import the built wheel in the release workflow before publishing — this is the `1.1.0` install-time breakage fix ([#1959](https://github.com/opensandbox-group/OpenSandbox/pull/1959))
- Fix the Fast Sandbox snapshot CR plural (`sandboxsnapshots`) in Helm RBAC and the fsb snapshot runtime, where `sandboxesnapshots` could fail list/watch with missing RBAC ([#1972](https://github.com/opensandbox-group/OpenSandbox/pull/1972))
- Propagate the underlying FastPath error message through the 429 capacity mapping (keeping status and `Retry-After`, adding a `cause` field) so admission/runtime failures like a broken KVM device are no longer masked as pool exhaustion, and warn-log the raw error before mapping ([#2001](https://github.com/opensandbox-group/OpenSandbox/pull/2001))
- Fix Docker edges: reject a repeated pause of an already-paused sandbox with the documented 409 instead of surfacing Docker's "already paused" as 500 ([#2023](https://github.com/opensandbox-group/OpenSandbox/pull/2023)), and clear Docker masked/read-only system paths only for sandboxes that opt into the isolation extension so nested Bubblewrap can mount a private procfs ([#1982](https://github.com/opensandbox-group/OpenSandbox/pull/1982))

## SDKs

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

- Add `commands.setEnv` for persistent runtime env injection in all five sandbox SDKs (Python async/sync, JavaScript, Kotlin/Java, Go, C#): appends `KEY=VALUE` to the sandbox env file with runtime-exact serialization, local key/NUL validation, and a loud failure when `EXECD_ENVS` is unset ([#2013](https://github.com/opensandbox-group/OpenSandbox/pull/2013))
- Systematic cross-language audit cleanup across sandbox, code-interpreter, and MCP SDKs: Go retry backoff collapsing to zero delay and dropped session `cwd`, Python isolated-session ignoring `encoding` and readiness budgets reporting stale errors, JS resume sharing and killing the source sandbox's transport, Kotlin codes runs dropping every SSE-framed event and never setting `exitCode`, C# create failing on fresh-sandbox 404 and NREs on empty bodies, and MCP per-call closes tearing down the server-wide shared transport — each fixed with regression tests ([#2020](https://github.com/opensandbox-group/OpenSandbox/pull/2020))
- Support `env` on template create across all five SDKs, matching the server-side golden-image baking ([#1984](https://github.com/opensandbox-group/OpenSandbox/pull/1984))
- Align background command stream completion across languages: with execd's fixed completion delay removed, background result collection in Go, Kotlin, and C# now returns on startup acknowledgement like Python and JavaScript instead of waiting for EOF and inferring exit code 0 — query command status for the eventual outcome; foreground calls still read to EOF for older servers ([#1825](https://github.com/opensandbox-group/OpenSandbox/pull/1825))

## Controller

- Fix an image-committer deadlock where pausing the QEMU container via cgroup v2 freeze never completes (KVM worker kernel threads never reach `frozen`), by skipping the redundant freeze for the QEMU container already paused over QMP and committing with `--pause=false` ([#1986](https://github.com/opensandbox-group/OpenSandbox/pull/1986))

## Execd

- Remove the fixed one-second delay after command completion, cutting median terminal-event-to-return latency from ~1000 ms to sub-millisecond while retaining output flushing and heartbeat cleanup ([#1825](https://github.com/opensandbox-group/OpenSandbox/pull/1825))
- Support multiple independent overlay mounts per session in the bwrap isolation layer (separate uppers for workspace vs system paths, nested shadowing, ephemeral tmpfs uppers); internal foundation with no public API change — the request-level `overlays` field follows ([#2021](https://github.com/opensandbox-group/OpenSandbox/pull/2021))
- Stop logging every stdout/stderr chunk at info level, which accounted for 99.8% of execd log volume on output-heavy workloads ([#1906](https://github.com/opensandbox-group/OpenSandbox/pull/1906))

## Networking

### egress

- Add an opt-in chained upstream CONNECT proxy to the transparent mitmproxy path (`OPENSANDBOX_EGRESS_UPSTREAM_PROXY`/`_AUTH`), routing sandbox egress through corporate forward proxies with SNI/Host-consistent CONNECT authority, fail-closed startup validation, and no behavior change when unset ([#1816](https://github.com/opensandbox-group/OpenSandbox/pull/1816))
- Continue the credential-bound TLS interception foundation (OSEP-0023) **[EXPERIMENTAL]**: bootstrap authenticated revision sessions on sidecar launch and restart behind an internal gate ([#1948](https://github.com/opensandbox-group/OpenSandbox/pull/1948)), stage unpublished Credential Vault mutation candidates with acknowledge-before-publish commit semantics ([#1980](https://github.com/opensandbox-group/OpenSandbox/pull/1980)), serialize `/policy` and Vault writes behind a shared mutation barrier ([#1991](https://github.com/opensandbox-group/OpenSandbox/pull/1991)), stage always-rule reloads before publishing ([#1997](https://github.com/opensandbox-group/OpenSandbox/pull/1997)), retain exact attempt identity across indeterminate coordinator outcomes ([#2007](https://github.com/opensandbox-group/OpenSandbox/pull/2007)), and add post-bootstrap revision updates with exact-attempt reconciliation ([#2017](https://github.com/opensandbox-group/OpenSandbox/pull/2017)) — groundwork only; public Vault writes still 503 under the gate and selective TLS stays disabled

### ingress

- Support per-request opt-out of access renew intents via an `OpenSandbox-Access-Renew: skip` header, suppressed before throttle state is touched and stripped on both the gateway and the server proxy paths — for health probes and other heartbeat-style traffic ([#1966](https://github.com/opensandbox-group/OpenSandbox/pull/1966))

## Fast Sandbox

- Chart: add controller tolerations, a `sandboxtemplateBuilderPodSpec` PodSpec-fragment ConfigMap merged into golden-image build pods, a `fastletProxyImage` override so the injected sidecar no longer has to come from docker.io, and agent/janitor runtime resources for tainted or registry-restricted clusters ([#1968](https://github.com/opensandbox-group/OpenSandbox/pull/1968))
- Bump the pinned fast-sandbox source twice: `f45bc60` drops CRD CEL validations for older API servers, makes P2P optional behind a peer-gateway seam, and lets template guests inherit the source image's OCI env ([#1989](https://github.com/opensandbox-group/OpenSandbox/pull/1989)); `2b5e840` drops the node-side guest kernel asset (`kernelPath` becomes optional), adds snapshot CPU-template compatibility admission, and rebinds jailed `/dev/kvm` to host device numbers before restore ([#2028](https://github.com/opensandbox-group/OpenSandbox/pull/2028)); the intermediate `fb87a53` pin bumps the builder's default guest kernel to 6.18.36 ([#1992](https://github.com/opensandbox-group/OpenSandbox/pull/1992))

## Misc

- CLI: add template and snapshot management (`osb template create|get|list|delete`, `osb snapshot ...`, plus `--template`/`--snapshot-id` on `sandbox create`) ([#2002](https://github.com/opensandbox-group/OpenSandbox/pull/2002)), kubectl-style `-f/--file` request files for `sandbox create` and `template create` covering the full wire format with strict unknown-field rejection ([#2019](https://github.com/opensandbox-group/OpenSandbox/pull/2019)), and fix `sandbox metrics --watch` failing on execd's raw SSE field names ([#1970](https://github.com/opensandbox-group/OpenSandbox/pull/1970))
- Charts: correct chart metadata, unify image tag defaults on `release-<appVersion>` fallbacks, make `imagePullPolicy` configurable, keep the dataplane Namespace on `helm.sh/resource-policy: keep`, and rewrite the deployment guide around the umbrella release flow with an explicit install order ([#2006](https://github.com/opensandbox-group/OpenSandbox/pull/2006))
- Fast-sandbox tooling: add a self-hosted KVM integration-test workflow ([#1978](https://github.com/opensandbox-group/OpenSandbox/pull/1978) [#1985](https://github.com/opensandbox-group/OpenSandbox/pull/1985)), a helper that provisions preallocated reflink-XFS state disks for Firecracker ([#1995](https://github.com/opensandbox-group/OpenSandbox/pull/1995)), a realistic golden-image test workload ([#1983](https://github.com/opensandbox-group/OpenSandbox/pull/1983)), and replace MinIO with RustFS in the integration environment ([#2009](https://github.com/opensandbox-group/OpenSandbox/pull/2009))
- Docs: publish fast-sandbox performance pages with measured create/snapshot/restore figures ([#1951](https://github.com/opensandbox-group/OpenSandbox/pull/1951) [#1992](https://github.com/opensandbox-group/OpenSandbox/pull/1992)), sharpen the README intro and Features ([#1960](https://github.com/opensandbox-group/OpenSandbox/pull/1960)), repair links broken by the docs restructure ([#1957](https://github.com/opensandbox-group/OpenSandbox/pull/1957)), mark OSEP-0016 as implemented ([#1955](https://github.com/opensandbox-group/OpenSandbox/pull/1955)), add a DeerFlow integration example ([#2011](https://github.com/opensandbox-group/OpenSandbox/pull/2011)), and remove the unmaintained NullClaw example ([#2016](https://github.com/opensandbox-group/OpenSandbox/pull/2016))
- release: bump platform version to `1.1.1-rc.1` across charts and image references

## Upgrade & Compatibility

- The `1.1.0` server package on PyPI is broken at import time and cannot be replaced (PyPI is immutable) — upgrade to this line; see [#1959](https://github.com/opensandbox-group/OpenSandbox/pull/1959)
- Behavior change: background command result collection in Go, Kotlin, and C# returns on startup acknowledgement and no longer infers exit code 0 — query command status for the eventual outcome ([#1825](https://github.com/opensandbox-group/OpenSandbox/pull/1825))
- Behavior change: in multi-tenant mode, another tenant's labeled sandboxes in a shared namespace now resolve as 404; unlabeled legacy sandboxes stay visible until expiry ([#1973](https://github.com/opensandbox-group/OpenSandbox/pull/1973))
- Fast-sandbox operators: the `2b5e840` pin removes the bundled node-side guest kernel — restores never boot a kernel and direct boot requires an explicit operator `kernelPath` pin ([#2028](https://github.com/opensandbox-group/OpenSandbox/pull/2028)); the integration environment reads `RUSTFS_*`/`RC_IMAGE` instead of `MINIO_*`/`MC_IMAGE` with no legacy alias ([#2009](https://github.com/opensandbox-group/OpenSandbox/pull/2009))
- Images: `opensandbox/<component>:release-1.1.1-rc.1` (all three registries)
- Packages: unchanged — SDK/CLI artifacts publish at the `1.1.1` 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 ❤️

- @alahaiyo
- @comqx
- @cwj2001
- @dcaminos
- @gau1991
- @hittyt
- @hpliStartAgain
- @jianpingpei
- @jiawen7777
- @kittimzhe
- @mengdehong
- @mpu
- @Pangjiping
- @ruirui6946
- @TuGou-a
- @YxinMiracle
- @y4ney

## Artifacts

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

## Installation

```bash
# Platform (Kubernetes) — render the chart at this tag and apply
git clone https://github.com/opensandbox-group/OpenSandbox
git checkout release-1.1.1-rc.1
helm dependency build manifests/charts/opensandbox  # package file:// sub-charts (not committed)
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](/community/release-verification).

---

### Releases 1.1.1 (`docs/releases/1.1.1.md`)

Stable cut of the `1.1.1` patch line: fixes the broken `1.1.0` server wheel — hatchling's VCS-exclude filtering silently dropped the committed FastPath gRPC stubs from the PyPI package, so every `1.1.0` install of `opensandbox-server` failed at import time; PyPI is immutable, so the fix ships on this line ([#1959](https://github.com/opensandbox-group/OpenSandbox/pull/1959)). SDK/CLI/server packages publish at `1.1.1` (`1.1.1-rc.1` shipped images only). Everyone on `1.1.0` should upgrade. Baseline for every section below: `release-1.1.0`.

## Highlights

- Fixes the `1.1.0` server wheel: FastPath stubs now ship in the package and installs import cleanly; SDK/CLI/server packages publish at `1.1.1` ([#1959](https://github.com/opensandbox-group/OpenSandbox/pull/1959))
- Identity-bound filesystem operations: execd executes file APIs as an explicit Linux UID/GID, with `FilesWithIdentity`/`files_with_identity` in all five SDKs; isolated sessions gain multiple independent overlay mounts ([#2070](https://github.com/opensandbox-group/OpenSandbox/pull/2070) [#2093](https://github.com/opensandbox-group/OpenSandbox/pull/2093) [#2094](https://github.com/opensandbox-group/OpenSandbox/pull/2094) [#2095](https://github.com/opensandbox-group/OpenSandbox/pull/2095) [#2096](https://github.com/opensandbox-group/OpenSandbox/pull/2096) [#2097](https://github.com/opensandbox-group/OpenSandbox/pull/2097) [#2032](https://github.com/opensandbox-group/OpenSandbox/pull/2032))
- Server hardening: snapshot creation submits the runtime object before persisting so early reads can't map to `Failed`, proxied backend failures map to 502/504 instead of a generic 500, and diagnostics work for pooled sandboxes ([#2098](https://github.com/opensandbox-group/OpenSandbox/pull/2098) [#2077](https://github.com/opensandbox-group/OpenSandbox/pull/2077) [#2084](https://github.com/opensandbox-group/OpenSandbox/pull/2084) [#2104](https://github.com/opensandbox-group/OpenSandbox/pull/2104))

## Server

- Implement pause/resume for the agent-sandbox provider by bridging to `spec.operatingMode` (`Running`/`Suspended`) on `agents.x-k8s.io/v1beta1`, with state derivation (`Paused`/`Pausing`/`Resuming`/`Terminated`) and 409 on invalid transitions; both endpoints previously returned "not supported" ([#1975](https://github.com/opensandbox-group/OpenSandbox/pull/1975))
- Isolate tenants that share one Kubernetes namespace: creates stamp `opensandbox.io/tenant`, and list/get/delete/pause/resume/proxy hide other tenants' labeled sandboxes as 404 while unlabeled legacy sandboxes stay visible until expiry; also adds `kubernetes.insecure_skip_tls_verify` for ServiceAccount CA mismatches ([#1973](https://github.com/opensandbox-group/OpenSandbox/pull/1973))
- Heal the silent renew-intent drop after a server restart in multi-tenant mode: the resolver now falls back to a cluster-wide label lookup with a 403 circuit breaker and structured warnings, trusts an ingress-observed namespace from the intent payload when verifiable, and single-tenant deployments never resolve beyond the configured namespace ([#1819](https://github.com/opensandbox-group/OpenSandbox/pull/1819))
- Add `[docker] publish_host` to publish bridge-mode sandbox ports on one host address (default `0.0.0.0` unchanged) — with `127.0.0.1` or the Docker bridge gateway, the tokenless execd port no longer lands on public interfaces ([#1954](https://github.com/opensandbox-group/OpenSandbox/pull/1954))
- Add a typed, admin-only `[egress.upstream_proxy]` config section (URL + optional authorization, validated at load) that injects the sidecar's chained upstream CONNECT proxy on both Docker and Kubernetes, with fail-closed 400s when the requested `networkPolicy` is incompatible; secret values are no longer echoed in config-validation errors ([#1999](https://github.com/opensandbox-group/OpenSandbox/pull/1999)), plus `ca_cert_path` (Docker) and `ca_secret_name` (Kubernetes) so `https://` proxies signed by a private CA are trusted without replacing the system trust store — the PEM augments `/etc/ssl/certs` in the sidecar ([#2030](https://github.com/opensandbox-group/OpenSandbox/pull/2030))
- Make snapshot creation race-free for early readers on k8s/fsb: `POST /sandboxes/{id}/snapshots` now creates the runtime object (SandboxSnapshot CR / fsb submit) before persisting the `Creating` row and returns without queueing a worker, so read-time sync can no longer map a not-yet-created object to `Failed` — including on other HA replicas; recovery resubmits stuck rows inline ([#2098](https://github.com/opensandbox-group/OpenSandbox/pull/2098)), fixing the window where a client polling right after create flipped the row to a permanent `Failed` ([#2077](https://github.com/opensandbox-group/OpenSandbox/pull/2077))
- Map proxied backend failures to meaningful statuses: read/write timeouts return 504 `BACKEND_TIMEOUT`, mid-response resets and protocol errors return 502 `BACKEND_RESPONSE_FAILED`, keeping 502 `BACKEND_CONNECTION_FAILED` for connect failures — a sandbox that dies mid-request no longer looks like a server bug ([#2084](https://github.com/opensandbox-group/OpenSandbox/pull/2084))
- Find pool-allocated pods for k8s diagnostics: pooled BatchSandboxes don't carry the `opensandbox.io/id` pod label, so every logs/events/inspect call returned 404; the lookup now falls back to the pods listed in the workload's `alloc-status` annotation ([#2104](https://github.com/opensandbox-group/OpenSandbox/pull/2104))
- Remove auto-created Docker volumes when create fails before the provisioning block runs (image pull, credential/upstream-proxy checks, Windows prerequisites, OSSFS prep) — a mistyped image name no longer leaks the PVC it triggered ([#2103](https://github.com/opensandbox-group/OpenSandbox/pull/2103))
- Report deleting pooled/task-mode BatchSandboxes as `Stopping`/`DELETING`: `metadata.deletionTimestamp` now wins over runtime status publication, so a sandbox whose pods go not-ready during finalization no longer flips back to `Pending`/`CREATING` ([#2122](https://github.com/opensandbox-group/OpenSandbox/pull/2122))
- Stop Docker metadata PATCHes from resurrecting deleted keys: `patch()` now starts from the override file (as `get()` does) instead of re-merging container labels, so `{"env": null}` stays deleted and an emptied override file doesn't restore the original labels ([#2065](https://github.com/opensandbox-group/OpenSandbox/pull/2065))
- Accept `env` on template create and bake it into the fast-sandbox golden image (`/etc/sandbox-init.env`, inheriting the source image's OCI `Config.Env`), closing the gap where `SandboxTemplate.spec.envs` existed but the lifecycle API rejected it; supported across all SDKs ([#1984](https://github.com/opensandbox-group/OpenSandbox/pull/1984))
- Wake Kubernetes creation waits on informer watch events instead of fixed 1-second polling, cutting prewarmed-pool create latency from ~1 s to tens of milliseconds while keeping the polling fallback ([#1996](https://github.com/opensandbox-group/OpenSandbox/pull/1996))
- Ship the committed FastPath gRPC stubs in the server wheel by force-including them past hatchling's VCS excludes, and smoke-import the built wheel in the release workflow before publishing — this is the `1.1.0` install-time breakage fix ([#1959](https://github.com/opensandbox-group/OpenSandbox/pull/1959))
- Fix the Fast Sandbox snapshot CR plural (`sandboxsnapshots`) in Helm RBAC and the fsb snapshot runtime, where `sandboxesnapshots` could fail list/watch with missing RBAC ([#1972](https://github.com/opensandbox-group/OpenSandbox/pull/1972))
- Propagate the underlying FastPath error message through the 429 capacity mapping (keeping status and `Retry-After`, adding a `cause` field) so admission/runtime failures like a broken KVM device are no longer masked as pool exhaustion, and warn-log the raw error before mapping ([#2001](https://github.com/opensandbox-group/OpenSandbox/pull/2001))
- Clarify the renewal contract: `renew-expiration` may shorten, preserve, or extend expiration (any future target is valid) and `max_sandbox_timeout_seconds` caps only creation, not renewal or total lifetime — documentation and SDK references aligned, with regression coverage across Docker, Kubernetes, and FastSandbox ([#2087](https://github.com/opensandbox-group/OpenSandbox/pull/2087))
- Fix Docker edges: reject a repeated pause of an already-paused sandbox with the documented 409 instead of surfacing Docker's "already paused" as 500 ([#2023](https://github.com/opensandbox-group/OpenSandbox/pull/2023)), and clear Docker masked/read-only system paths only for sandboxes that opt into the isolation extension so nested Bubblewrap can mount a private procfs ([#1982](https://github.com/opensandbox-group/OpenSandbox/pull/1982))

## SDKs

- Add identity-bound filesystem operations end to end: execd gains additive `/v1/filesystem/{uid}/{gid}` routes backed by a per-request worker process running with explicit credentials and account-derived supplementary groups (fail-closed, existing routes unchanged), and every sandbox SDK exposes it — Python `files_with_identity`, JavaScript `filesWithIdentity`, Kotlin `filesWithIdentity`, C# `FilesWithIdentity`, Go `FilesWithIdentity` — so Linux checks the selected user's permissions during actual access; identity selection belongs in the trusted backend, this enforces filesystem permissions and is not a tenant boundary ([#2070](https://github.com/opensandbox-group/OpenSandbox/pull/2070) [#2093](https://github.com/opensandbox-group/OpenSandbox/pull/2093) [#2094](https://github.com/opensandbox-group/OpenSandbox/pull/2094) [#2095](https://github.com/opensandbox-group/OpenSandbox/pull/2095) [#2096](https://github.com/opensandbox-group/OpenSandbox/pull/2096) [#2097](https://github.com/opensandbox-group/OpenSandbox/pull/2097))
- Expose multiple independent overlay mounts per isolated session across the API and all five SDKs: `CreateIsolatedSessionRequest.overlays` (`{path, mode, persist}`) with separate host-tracked or ephemeral tmpfs uppers, `workspace` kept as sugar, and `SessionState` echoing the canonical list — completing the internal foundation from [#2021](https://github.com/opensandbox-group/OpenSandbox/pull/2021) ([#2032](https://github.com/opensandbox-group/OpenSandbox/pull/2032))
- Add `commands.setEnv` for persistent runtime env injection in all five sandbox SDKs (Python async/sync, JavaScript, Kotlin/Java, Go, C#): appends `KEY=VALUE` to the sandbox env file with runtime-exact serialization, local key/NUL validation, and a loud failure when `EXECD_ENVS` is unset ([#2013](https://github.com/opensandbox-group/OpenSandbox/pull/2013))
- Systematic cross-language audit cleanup across sandbox, code-interpreter, and MCP SDKs: Go retry backoff collapsing to zero delay and dropped session `cwd`, Python isolated-session ignoring `encoding` and readiness budgets reporting stale errors, JS resume sharing and killing the source sandbox's transport, Kotlin codes runs dropping every SSE-framed event and never setting `exitCode`, C# create failing on fresh-sandbox 404 and NREs on empty bodies, and MCP per-call closes tearing down the server-wide shared transport — each fixed with regression tests ([#2020](https://github.com/opensandbox-group/OpenSandbox/pull/2020))
- Support `env` on template create across all five SDKs, matching the server-side golden-image baking ([#1984](https://github.com/opensandbox-group/OpenSandbox/pull/1984))
- Align background command stream completion across languages: with execd's fixed completion delay removed, background result collection in Go, Kotlin, and C# now returns on startup acknowledgement like Python and JavaScript instead of waiting for EOF and inferring exit code 0 — query command status for the eventual outcome; foreground calls still read to EOF for older servers ([#1825](https://github.com/opensandbox-group/OpenSandbox/pull/1825))

### Python

- Raise on error responses from `create_session` and `delete_session`: both passed the parsed model to the error handler (which needs the raw status), so 400/404/500 returned `None` as success and unknown statuses raised with a bogus `status_code=200` — the CLI could print "Deleted session" for a session that never existed ([#2066](https://github.com/opensandbox-group/OpenSandbox/pull/2066))
- Expose `follow_redirects` on `ConnectionConfig`/`ConnectionConfigSync`, propagated to all handwritten async/sync httpx clients including SSE clients and generated-client wrappers ([#1123](https://github.com/opensandbox-group/OpenSandbox/pull/1123))
- Keep the last real endpoint error when the readiness deadline expires mid-request: the budget no longer records its own `SandboxReadyTimeoutException` as `last_error`, so causes like `KUBERNETES::POD_IP_NOT_AVAILABLE` aren't masked as a plain timeout ([#2121](https://github.com/opensandbox-group/OpenSandbox/pull/2121))
- Authenticate Code Interpreter requests in server-proxy mode: context creation and streamed execution now attach the API key like sandbox file requests, fixing 401s from the same proxy ([#2125](https://github.com/opensandbox-group/OpenSandbox/pull/2125))
- Serialize `SandboxPoolSync.shutdown()` teardown: concurrent shutdown (or shutdown racing `start()`) could close the warmup event loop twice; teardown is now idempotent under a dedicated lock ([#2041](https://github.com/opensandbox-group/OpenSandbox/pull/2041))
- Orphan the shared warmup event loop instead of closing it over an in-flight warmup: when the bounded drain wait ends with warmups still running, shutdown leaves the loop alive so the in-flight warmup finishes and cleans up its sandbox, instead of destroying the pending task and skipping cleanup ([#2142](https://github.com/opensandbox-group/OpenSandbox/pull/2142))
- MCP: map expected OpenSandbox validations to MCP 2.x `ToolError` so clients get the actionable message instead of a generic "Error executing tool" ([#2108](https://github.com/opensandbox-group/OpenSandbox/pull/2108))

### JavaScript

- Treat empty-body non-2xx responses as errors: openapi-fetch sets no `error` for `Content-Length: 0`/204/HEAD responses, so an empty 404/502/503 from a gateway made `deleteSandbox`, `pauseSandbox`, and file deletes look successful in both the sandbox and code-interpreter packages ([#2101](https://github.com/opensandbox-group/OpenSandbox/pull/2101))

### Kotlin

- Identity-bound filesystem operations via `sandbox.filesWithIdentity(uid, gid)` — see the cross-SDK entry above ([#2095](https://github.com/opensandbox-group/OpenSandbox/pull/2095))

### C#

- Identity-bound filesystem operations via `sandbox.FilesWithIdentity(uid, gid)` — see the cross-SDK entry above ([#2096](https://github.com/opensandbox-group/OpenSandbox/pull/2096))

### Go

- Identity-bound filesystem operations via `sandbox.FilesWithIdentity(uid, gid)` — see the cross-SDK entry above ([#2097](https://github.com/opensandbox-group/OpenSandbox/pull/2097))
- Document previously uncovered surfaces: file operations, volume mounts (`host`/`pvc`/`ossfs`), the full template workflow, `WaitUntilReady` with custom health checks, and `SandboxManager` ([#2024](https://github.com/opensandbox-group/OpenSandbox/pull/2024))

## Controller

- Add OpenTelemetry allocator-path metrics on the `opensandbox/controller` meter: pool-allocation schedule, alloc-state persist, and alloc-result sync duration histograms with namespace/pool/success attributes, exported over OTLP ([#2014](https://github.com/opensandbox-group/OpenSandbox/pull/2014))
- Prepare the pool-assign surface for custom extension: the `Predicate` interface and predicate methods under `poolassign` are now exported — the package remains under `kubernetes/internal/`, so external Go modules cannot import it yet ([#2037](https://github.com/opensandbox-group/OpenSandbox/pull/2037))
- Auto-replace BatchSandbox pods stuck in provisioning failures during initial startup: never-Ready pods waiting in `ImagePullBackOff`/`ErrImagePull` are deleted after a threshold so the scheduler can escape a broken node (a full disk had left one production fleet with 114 stuck sandboxes), with a per-sandbox, per-generation replacement budget and a `PodRecoveryLimitReached` warning; permanently broken images are skipped ([#1961](https://github.com/opensandbox-group/OpenSandbox/pull/1961))
- Support custom snapshot image URI templates: `--snapshot-image-uri-template` / `controller.snapshot.imageURITemplate` name rootfs and QEMU VM-state targets with snapshot/sandbox/container/namespace metadata and `date`/`dateInZone` helpers, validated (invalid refs, duplicate targets) before Job creation; empty keeps existing naming ([#2004](https://github.com/opensandbox-group/OpenSandbox/pull/2004))
- Fix the free-pod predicate admitting pods on a non-empty IP alone: the task scheduler bound tasks to pods Kubernetes marked `NotReady` (still starting or restarting), the assignment POST failed with no retry, and a healthy sibling was skipped; readiness is now required like every other predicate ([#2138](https://github.com/opensandbox-group/OpenSandbox/pull/2138))
- Keep the last-known task status when a status query fails during release: a single timed-out `/getTasks` no longer marks a `releasing` task as gone, which skipped `Set(nil)`, skipped the postStop hook, and let the controller drop the cleanup finalizer or return the pod to the pool while the task still ran ([#2067](https://github.com/opensandbox-group/OpenSandbox/pull/2067))
- Skip pause dispatch for an already-`Paused` BatchSandbox: renewing a paused sandbox bumped its generation and re-triggered `handlePause`, which needs a running source pod and drove the sandbox to `Failed`/`PauseFailed` ([#2082](https://github.com/opensandbox-group/OpenSandbox/pull/2082))
- Fix an image-committer deadlock where pausing the QEMU container via cgroup v2 freeze never completes (KVM worker kernel threads never reach `frozen`), by skipping the redundant freeze for the QEMU container already paused over QMP and committing with `--pause=false` ([#1986](https://github.com/opensandbox-group/OpenSandbox/pull/1986))

## Execd

- Execute ordinary filesystem operations under an explicit Linux identity: additive `/v1/filesystem/{uid}/{gid}` routes spawn a per-request worker process with the selected uid/gid and account-derived supplementary groups, fail-closed on unsupported platforms; existing routes keep their current identity — the SDK-facing follow-ups ship in the same release ([#2070](https://github.com/opensandbox-group/OpenSandbox/pull/2070))
- Expose the public isolated-session overlays API (validation, canonical merge with legacy `workspace`, per-overlay allocator assignment) — details and SDK support under SDKs ([#2032](https://github.com/opensandbox-group/OpenSandbox/pull/2032))
- Give overlay sessions a durable cleanup identity outside their removable subtree (`<upper_root>/.execd-cleanup/<session-id>`): startup recovery removes only exact matching directories, and partial deletion or lost records are retried within the process or across restarts instead of orphaning upper dirs ([#2107](https://github.com/opensandbox-group/OpenSandbox/pull/2107))
- Let children of a finished background command keep running: the post-`Wait` context cancel no longer SIGKILLs the whole process group, so a backgrounded `nohup ./server &` survives its script — cancellation and timeouts still kill the group ([#2074](https://github.com/opensandbox-group/OpenSandbox/pull/2074))
- Kill the whole process group when a bash session run times out: only bash itself was killed, so `(sleep 2; …)` children kept the run hanging past its deadline ([#2079](https://github.com/opensandbox-group/OpenSandbox/pull/2079))
- Preserve `\n`/`\r` bytes in foreground `/command` stdout/stderr event text, so concatenated events reconstruct the command's line endings including blank lines and a trailing unterminated fragment; the Python SDK's display-oriented `Execution.text` and stderr summary no longer insert extra blank lines when CRLF arrives across events, while raw `logs.stdout`/`logs.stderr` keep their original text ([#2033](https://github.com/opensandbox-group/OpenSandbox/pull/2033))
- Remove the per-run bash session script file (`execd_bash_*.sh`), which retained a copy of every session environment variable — secrets included — readable by any later command as the same user ([#2076](https://github.com/opensandbox-group/OpenSandbox/pull/2076))
- Clamp the HTTP `Range` end before computing length: `bytes=0-9223372036854775807` overflowed into a negative length and answered 206 with a broken `Content-Range`/`Content-Length` and no body, on both regular and isolated-session downloads ([#2089](https://github.com/opensandbox-group/OpenSandbox/pull/2089))
- Remove the fixed one-second delay after command completion, cutting median terminal-event-to-return latency from ~1000 ms to sub-millisecond while retaining output flushing and heartbeat cleanup ([#1825](https://github.com/opensandbox-group/OpenSandbox/pull/1825))
- Stop logging every stdout/stderr chunk at info level, which accounted for 99.8% of execd log volume on output-heavy workloads ([#1906](https://github.com/opensandbox-group/OpenSandbox/pull/1906))

## Networking

### egress

- Continue the credential-bound TLS interception foundation (OSEP-0023) **[EXPERIMENTAL]** across the line: generation-pinned live revision-session callbacks held under the mitmproxy lifecycle lock ([#2036](https://github.com/opensandbox-group/OpenSandbox/pull/2036)), credential-free ClientHello classification of ECH/SNI/static pass-through with `needs_registry` admission requests ([#2046](https://github.com/opensandbox-group/OpenSandbox/pull/2046)), a bounded connection-admission registry that denies on exhaustion instead of downgrading to opaque pass-through ([#2062](https://github.com/opensandbox-group/OpenSandbox/pull/2062)) and flags admissions uncovered by selector cutovers ([#2072](https://github.com/opensandbox-group/OpenSandbox/pull/2072)), request admission pinned to one immutable snapshot with no old-credential fallback and permanent fences for removed hosts ([#2088](https://github.com/opensandbox-group/OpenSandbox/pull/2088)), bounded request-handle lifecycles with a global budget and paginated inspection ([#2091](https://github.com/opensandbox-group/OpenSandbox/pull/2091)), monotonic retirement deadlines for uncovered connections ([#2100](https://github.com/opensandbox-group/OpenSandbox/pull/2100)), joint Receiver/TLS-Registry publication with prepare/commit/abort retry semantics ([#2106](https://github.com/opensandbox-group/OpenSandbox/pull/2106)) wired into the authenticated revision IPC via an internal InstallationReceiver ([#2120](https://github.com/opensandbox-group/OpenSandbox/pull/2120)), revision-backed Vault mutation transactions with exact-attempt reconciliation and readiness fencing ([#2124](https://github.com/opensandbox-group/OpenSandbox/pull/2124)), frozen effective-policy revision candidates revalidating every binding including HTTP-only ones ([#2139](https://github.com/opensandbox-group/OpenSandbox/pull/2139)), guarded revision recovery and readiness publication with exact bootstrap identity and quarantined uncertain outcomes ([#2151](https://github.com/opensandbox-group/OpenSandbox/pull/2151)), and cross-language IPC recovery coverage against the production endpoint ([#2136](https://github.com/opensandbox-group/OpenSandbox/pull/2136)) — groundwork only; public Vault writes still 503 under the gate and selective TLS stays disabled
- Extend the chained upstream CONNECT proxy: trust `https://` proxies signed by a private CA via an append-style `OPENSANDBOX_EGRESS_MITMPROXY_UPSTREAM_EXTRA_CA` bundle that augments the system trust store for all upstream TLS verification, configured from the server's `ca_cert_path`/`ca_secret_name` ([#2030](https://github.com/opensandbox-group/OpenSandbox/pull/2030)); support the chained proxy under the fast-sandbox profile with profile-wide endpoint drops so sandbox workloads cannot reach the upstream proxy directly, infrastructure DNS seeding resolved concurrently through dnsproxy upstreams and the Pod resolver, and atomic nft table seeding with drop elements permanent across daemon downtime ([#2003](https://github.com/opensandbox-group/OpenSandbox/pull/2003))
- Add an opt-in chained upstream CONNECT proxy to the transparent mitmproxy path (`OPENSANDBOX_EGRESS_UPSTREAM_PROXY`/`_AUTH`), routing sandbox egress through corporate forward proxies with SNI/Host-consistent CONNECT authority, fail-closed startup validation, and no behavior change when unset ([#1816](https://github.com/opensandbox-group/OpenSandbox/pull/1816))
- Roll back the persisted policy file when an nft apply fails, so a restart never loads a policy the caller was told had failed — potentially looser than the one actually running ([#2075](https://github.com/opensandbox-group/OpenSandbox/pull/2075))
- Tolerate nested percent-encoded slashes in the credential proxy path guard: the single-layer `%2f` relaxation now applies at any decode depth, so `pip` downloads from internal PyPI proxies no longer fail with 403 `request path contains ambiguous segments` while Credential Vault is active; dot-segments, backslashes at every depth, and non-converging decodes are still rejected ([#2153](https://github.com/opensandbox-group/OpenSandbox/pull/2153))
- Continue the earlier credential-bound groundwork: bootstrap authenticated revision sessions on sidecar launch and restart behind an internal gate ([#1948](https://github.com/opensandbox-group/OpenSandbox/pull/1948)), stage unpublished Credential Vault mutation candidates with acknowledge-before-publish commit semantics ([#1980](https://github.com/opensandbox-group/OpenSandbox/pull/1980)), serialize `/policy` and Vault writes behind a shared mutation barrier ([#1991](https://github.com/opensandbox-group/OpenSandbox/pull/1991)), stage always-rule reloads before publishing ([#1997](https://github.com/opensandbox-group/OpenSandbox/pull/1997)), retain exact attempt identity across indeterminate coordinator outcomes ([#2007](https://github.com/opensandbox-group/OpenSandbox/pull/2007)), and add post-bootstrap revision updates with exact-attempt reconciliation ([#2017](https://github.com/opensandbox-group/OpenSandbox/pull/2017))

### ingress

- Fail at chart render time when `providerType=fast-sandbox` is set without a secure-access key ring: the gateway previously passed `helm template` and then CrashLoopBackOff'd on `FastPath routing requires --secure-access-keys`; the deployment guide now also documents the gateway as required on Kubernetes (sandbox Pods are ClusterIP-only) and installed before the server ([#2047](https://github.com/opensandbox-group/OpenSandbox/pull/2047))
- Support per-request opt-out of access renew intents via an `OpenSandbox-Access-Renew: skip` header, suppressed before throttle state is touched and stripped on both the gateway and the server proxy paths — for health probes and other heartbeat-style traffic ([#1966](https://github.com/opensandbox-group/OpenSandbox/pull/1966))

## Fast Sandbox

- Bump the pinned fast-sandbox source twice more: `83139cc` makes the Firecracker runtime require XFS with reflink at startup ([#2123](https://github.com/opensandbox-group/OpenSandbox/pull/2123)); `b702fbe` preserves the guest gateway MAC across snapshot restores with a committed-cache recheck before delivery retry and ARP-cache regression coverage, and the vendored CRD bundle drops the `boxlite` runtime enum that upstream removed ([#2145](https://github.com/opensandbox-group/OpenSandbox/pull/2145)); the earlier `2b5e840` pin drops the node-side guest kernel asset (`kernelPath` becomes optional), adds snapshot CPU-template compatibility admission, and rebinds jailed `/dev/kvm` to host device numbers before restore ([#2028](https://github.com/opensandbox-group/OpenSandbox/pull/2028)), with the intermediate `fb87a53` bumping the builder's default guest kernel to 6.18.36 ([#1992](https://github.com/opensandbox-group/OpenSandbox/pull/1992)) and `f45bc60` dropping CRD CEL validations for older API servers, making P2P optional, and letting template guests inherit the source image's OCI env ([#1989](https://github.com/opensandbox-group/OpenSandbox/pull/1989))
- Create the shared system namespace in the `base` chart (`fastSandbox.namespaces.createSystem`, default `true`, `helm.sh/resource-policy: keep`): the fast-sandbox control-plane ServiceAccounts previously assumed the namespace was bootstrapped later, breaking the documented base-first install order and every rendered-manifest/GitOps flow ([#2044](https://github.com/opensandbox-group/OpenSandbox/pull/2044))
- Chart: add controller tolerations, a `sandboxtemplateBuilderPodSpec` PodSpec-fragment ConfigMap merged into golden-image build pods, a `fastletProxyImage` override so the injected sidecar no longer has to come from docker.io, and agent/janitor runtime resources for tainted or registry-restricted clusters ([#1968](https://github.com/opensandbox-group/OpenSandbox/pull/1968))
- Replace MinIO with RustFS in the integration environment ([#2009](https://github.com/opensandbox-group/OpenSandbox/pull/2009))

## Misc

- CLI: add template and snapshot management (`osb template create|get|list|delete`, `osb snapshot ...`, plus `--template`/`--snapshot-id` on `sandbox create`) ([#2002](https://github.com/opensandbox-group/OpenSandbox/pull/2002)), kubectl-style `-f/--file` request files for `sandbox create` and `template create` covering the full wire format with strict unknown-field rejection ([#2019](https://github.com/opensandbox-group/OpenSandbox/pull/2019)), fix `sandbox metrics --watch` failing on execd's raw SSE field names ([#1970](https://github.com/opensandbox-group/OpenSandbox/pull/1970)), exit non-zero when a foreground command's SSE stream ends without a completion or error event instead of reporting success ([#2099](https://github.com/opensandbox-group/OpenSandbox/pull/2099)), and reject failed or unconfirmed background starts before printing success metadata ([#2109](https://github.com/opensandbox-group/OpenSandbox/pull/2109)), and edit `osb config set` values through a real TOML parser instead of string concatenation, preserving comments and layout with correct escaping so quoted or multiline values no longer corrupt the config ([#2146](https://github.com/opensandbox-group/OpenSandbox/pull/2146))
- Charts: correct chart metadata, unify image tag defaults on `release-<appVersion>` fallbacks, make `imagePullPolicy` configurable, keep the dataplane Namespace on `helm.sh/resource-policy: keep`, and rewrite the deployment guide around the umbrella release flow with an explicit install order ([#2006](https://github.com/opensandbox-group/OpenSandbox/pull/2006))
- Docs: publish fast-sandbox performance pages with measured create/snapshot/restore figures ([#1951](https://github.com/opensandbox-group/OpenSandbox/pull/1951) [#1992](https://github.com/opensandbox-group/OpenSandbox/pull/1992)) and refresh the README performance claims with ACK measurements ([#2086](https://github.com/opensandbox-group/OpenSandbox/pull/2086) [#2115](https://github.com/opensandbox-group/OpenSandbox/pull/2115)), add an ACK deployment guide and an upstream-proxy operations guide ([#2048](https://github.com/opensandbox-group/OpenSandbox/pull/2048) [#2045](https://github.com/opensandbox-group/OpenSandbox/pull/2045)), restructure the server, operator, execd, and node-agent architecture pages around verified design decisions with redrawn diagrams ([#2058](https://github.com/opensandbox-group/OpenSandbox/pull/2058) [#2064](https://github.com/opensandbox-group/OpenSandbox/pull/2064) [#2069](https://github.com/opensandbox-group/OpenSandbox/pull/2069) [#2137](https://github.com/opensandbox-group/OpenSandbox/pull/2137)), add an interactive fast-sandbox integration architecture diagram ([#2034](https://github.com/opensandbox-group/OpenSandbox/pull/2034) [#2127](https://github.com/opensandbox-group/OpenSandbox/pull/2127)), document execd implementation-compatibility boundaries ([#2050](https://github.com/opensandbox-group/OpenSandbox/pull/2050)), multi-runtime Kubernetes deployments ([#1684](https://github.com/opensandbox-group/OpenSandbox/pull/1684)), rclone volume mounts ([#2071](https://github.com/opensandbox-group/OpenSandbox/pull/2071)), snapshot durability after abnormal termination ([#2111](https://github.com/opensandbox-group/OpenSandbox/pull/2111)), sandbox expiration semantics ([#2087](https://github.com/opensandbox-group/OpenSandbox/pull/2087)), and `apply_patch` in sandboxes ([#2110](https://github.com/opensandbox-group/OpenSandbox/pull/2110)), add GitHub Copilot CLI, DeerFlow, and scheduled pool-resize examples ([#2053](https://github.com/opensandbox-group/OpenSandbox/pull/2053) [#2011](https://github.com/opensandbox-group/OpenSandbox/pull/2011) [#2141](https://github.com/opensandbox-group/OpenSandbox/pull/2141)), sharpen the README intro ([#1960](https://github.com/opensandbox-group/OpenSandbox/pull/1960)), repair links broken by the docs restructure ([#1957](https://github.com/opensandbox-group/OpenSandbox/pull/1957)), mark OSEP-0016 as implemented ([#1955](https://github.com/opensandbox-group/OpenSandbox/pull/1955)), remove the unmaintained NullClaw example ([#2016](https://github.com/opensandbox-group/OpenSandbox/pull/2016)), pin the Compose example to a release image with the proxy fixes ([#2073](https://github.com/opensandbox-group/OpenSandbox/pull/2073)), and refresh the roadmap ([#2135](https://github.com/opensandbox-group/OpenSandbox/pull/2135))
- Security/dependency bumps: fast-uri, undici, and containerd CVEs ([#2057](https://github.com/opensandbox-group/OpenSandbox/pull/2057)), PyJWT (critical), oauthlib, brace-expansion, and fast-uri ([#2102](https://github.com/opensandbox-group/OpenSandbox/pull/2102)), and urllib3, source-map-js, and vue ([#2117](https://github.com/opensandbox-group/OpenSandbox/pull/2117))
- node-agent/CI: add a nightly full-stack log-collection e2e on Kind proving real sandbox stdout reaches the file sink and finalizes on deletion ([#2143](https://github.com/opensandbox-group/OpenSandbox/pull/2143)), narrow the fast-sandbox integration trigger scope ([#2054](https://github.com/opensandbox-group/OpenSandbox/pull/2054)), and harden the integration scripts' error handling so subshell failures can't tear down the shared environment ([#2068](https://github.com/opensandbox-group/OpenSandbox/pull/2068))
- Fast-sandbox tooling: add a self-hosted KVM integration-test workflow ([#1978](https://github.com/opensandbox-group/OpenSandbox/pull/1978) [#1985](https://github.com/opensandbox-group/OpenSandbox/pull/1985)), a helper that provisions preallocated reflink-XFS state disks for Firecracker ([#1995](https://github.com/opensandbox-group/OpenSandbox/pull/1995)), a realistic golden-image test workload ([#1983](https://github.com/opensandbox-group/OpenSandbox/pull/1983)), and a reproducible Kubernetes rootfs resume benchmark ([#2113](https://github.com/opensandbox-group/OpenSandbox/pull/2113))
- Tests and housekeeping: cover upstream-proxy compatibility against real mitmdump with Vault/proxy credential isolation ([#2043](https://github.com/opensandbox-group/OpenSandbox/pull/2043)) and Authorization on streamed uploads ([#1940](https://github.com/opensandbox-group/OpenSandbox/pull/1940)); harden the Python pool-lifecycle stress tests ([#2026](https://github.com/opensandbox-group/OpenSandbox/pull/2026) [#2144](https://github.com/opensandbox-group/OpenSandbox/pull/2144)) and fix task-manager test races ([#2060](https://github.com/opensandbox-group/OpenSandbox/pull/2060) [#2061](https://github.com/opensandbox-group/OpenSandbox/pull/2061)); make the PauseResume e2e status tamper work on pre-1.24 kubectl ([#2085](https://github.com/opensandbox-group/OpenSandbox/pull/2085)); sweep low-value and duplicate tests across the Go and Python suites with no production changes ([#2126](https://github.com/opensandbox-group/OpenSandbox/pull/2126)); apply a zero-behavior-change node-agent cleanup ([#2140](https://github.com/opensandbox-group/OpenSandbox/pull/2140)) and remove dead CLI code ([#2018](https://github.com/opensandbox-group/OpenSandbox/pull/2018)); drop the unused `.pre-commit-config.yaml` ([#2049](https://github.com/opensandbox-group/OpenSandbox/pull/2049)); fix a dead docs link ([#2118](https://github.com/opensandbox-group/OpenSandbox/pull/2118)) and a test license-header owner ([#1976](https://github.com/opensandbox-group/OpenSandbox/pull/1976)); and prepare and pin the `1.1.1-rc.1` pre-release ([#2035](https://github.com/opensandbox-group/OpenSandbox/pull/2035) [#2042](https://github.com/opensandbox-group/OpenSandbox/pull/2042))
- release: bump platform version to `1.1.1` across charts and image references

## Upgrade & Compatibility

- The `1.1.0` server package on PyPI is broken at import time and cannot be replaced (PyPI is immutable) — upgrade to this line; see [#1959](https://github.com/opensandbox-group/OpenSandbox/pull/1959)
- Behavior change: the HTTP proxy maps backend read/write timeouts to 504 and mid-response failures to 502 with typed error codes instead of a generic 500; SDK default retry configs differ for the new 504 — the Go SDK's `DefaultRetryConfig` retries 504 like 502/503, while Python's default retries 502/503 only ([#2084](https://github.com/opensandbox-group/OpenSandbox/pull/2084))
- Behavior change: children left behind by a finished background command keep running after it exits, matching the foreground path; cancellation and timeouts still kill the group ([#2074](https://github.com/opensandbox-group/OpenSandbox/pull/2074))
- Behavior change: snapshot creation on k8s/fsb submits the runtime object before persisting and recovery resubmits inline; polling clients no longer observe a spurious permanent `Failed` ([#2098](https://github.com/opensandbox-group/OpenSandbox/pull/2098))
- Behavior change: background command result collection in Go, Kotlin, and C# returns on startup acknowledgement and no longer infers exit code 0 — query command status for the eventual outcome ([#1825](https://github.com/opensandbox-group/OpenSandbox/pull/1825))
- Behavior change: in multi-tenant mode, another tenant's labeled sandboxes in a shared namespace now resolve as 404; unlabeled legacy sandboxes stay visible until expiry ([#1973](https://github.com/opensandbox-group/OpenSandbox/pull/1973))
- Chart: the `base` chart now creates the shared `fastSandbox.namespaces.system` namespace by default — set `fastSandbox.namespaces.createSystem=false` if you manage it externally ([#2044](https://github.com/opensandbox-group/OpenSandbox/pull/2044)); the ingress gateway is required on Kubernetes deployments and must be installed before the server, and fast-sandbox mode requires a secure-access key ring at render time ([#2047](https://github.com/opensandbox-group/OpenSandbox/pull/2047) [#2048](https://github.com/opensandbox-group/OpenSandbox/pull/2048))
- Fast-sandbox operators: restores never boot a kernel and direct boot requires an explicit operator `kernelPath` pin ([#2028](https://github.com/opensandbox-group/OpenSandbox/pull/2028)); the Firecracker runtime now requires XFS with reflink at startup — use the provided reflink-XFS state-disk helper on nodes that lack it ([#2123](https://github.com/opensandbox-group/OpenSandbox/pull/2123)); the vendored CRD drops the removed `boxlite` runtime enum ([#2145](https://github.com/opensandbox-group/OpenSandbox/pull/2145)); the integration environment reads `RUSTFS_*`/`RC_IMAGE` instead of `MINIO_*`/`MC_IMAGE` with no legacy alias ([#2009](https://github.com/opensandbox-group/OpenSandbox/pull/2009))
- Images: `opensandbox/<component>:release-1.1.1` (all three registries)
- Packages: server / CLI / SDKs all at `1.1.1`
- 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 ❤️

- @Coder-Sang
- @GodBlf
- @Gujiassh
- @Harbor404
- @Pangjiping
- @Spground
- @SuperSgdk
- @TuGou-a
- @YxinMiracle
- @acodercat
- @alahaiyo
- @comqx
- @cwj2001
- @dcaminos
- @drakeo338
- @gau1991
- @hittyt
- @hpliStartAgain
- @JasonBuildAI
- @jianpingpei
- @jiawen7777
- @jwx0925
- @ju123123
- @kinfey
- @kittimzhe
- @lorenzozanee
- @lux-liang
- @mengdehong
- @MincongZhou
- @mpu
- @ninan-nn
- @riversiderig-serves
- @ruirui6946
- @skyser2003
- @y4ney

## Artifacts

- BOM: [docs/releases/1.1.1.yaml](./1.1.1.yaml)
- Images: `opensandbox/<component>:release-1.1.1` (Docker Hub / GHCR / ACR)
- Packages: PyPI ×5, npm ×2, Maven Central ×5, NuGet ×2 — all at `1.1.1`
- Go module: `github.com/alibaba/OpenSandbox/sdks/sandbox/go@v1.1.1`

## Installation

```bash
# Platform (Kubernetes) — render the chart at this tag and apply
git clone https://github.com/opensandbox-group/OpenSandbox
cd OpenSandbox
git checkout release-1.1.1
helm dependency build manifests/charts/opensandbox  # package file:// sub-charts (not committed)
# Before first install, configure the server API key and enable the ingress
# gateway (required on Kubernetes; the bare render disables it, and the server
# refuses to start with an empty api_key) — see the deployment guide:
# https://open-sandbox.ai/deployment/
helm template ./manifests/charts/opensandbox | kubectl apply -f -
# or point your GitOps platform (Argo / Flux) at the repo path + tag

# SDKs
pip install opensandbox==1.1.1            # Python
npm install @alibaba-group/opensandbox@1.1.1   # JavaScript
# Kotlin/JVM: implementation("com.alibaba.opensandbox:sandbox:1.1.1")
dotnet add package Alibaba.OpenSandbox --version 1.1.1
go get github.com/alibaba/OpenSandbox/sdks/sandbox/go@v1.1.1

# Run the server locally
uvx opensandbox-server==1.1.1
```

Verify what you installed against the BOM: see [Release Verification](https://open-sandbox.ai/community/release-verification).

---

### Releases 1.2.0 Rc.1 (`docs/releases/1.2.0-rc.1.md`)

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](https://github.com/opensandbox-group/OpenSandbox/pull/2156) [#2180](https://github.com/opensandbox-group/OpenSandbox/pull/2180) [#2192](https://github.com/opensandbox-group/OpenSandbox/pull/2192) [#2198](https://github.com/opensandbox-group/OpenSandbox/pull/2198) [#2218](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/2176) [#2201](https://github.com/opensandbox-group/OpenSandbox/pull/2201) [#2182](https://github.com/opensandbox-group/OpenSandbox/pull/2182) [#2162](https://github.com/opensandbox-group/OpenSandbox/pull/2162) [#2172](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/2165) [#2186](https://github.com/opensandbox-group/OpenSandbox/pull/2186) [#2196](https://github.com/opensandbox-group/OpenSandbox/pull/2196) [#2206](https://github.com/opensandbox-group/OpenSandbox/pull/2206) [#2191](https://github.com/opensandbox-group/OpenSandbox/pull/2191) [#2200](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/2192) [#2181](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/2198) [#2218](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/2165) [#2186](https://github.com/opensandbox-group/OpenSandbox/pull/2186) [#2196](https://github.com/opensandbox-group/OpenSandbox/pull/2196) [#2206](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/2194) [#2208](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/2202))
- Docs: accept and document OSEP-0024 — the proposal itself ([#2152](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/2217)); unify component telemetry metrics from seven scattered locations into one reference at `docs/guides/telemetry.md` ([#2205](https://github.com/opensandbox-group/OpenSandbox/pull/2205)); refresh the network architecture pages with restyled SVG diagrams, a condensed egress page, and the network-isolation guide relocation ([#2210](https://github.com/opensandbox-group/OpenSandbox/pull/2210)); align CONTRIBUTING with the code (Go 1.25, Gin — not Beego) ([#2175](https://github.com/opensandbox-group/OpenSandbox/pull/2175)), dedupe `AGENTS.md` rules and state the architecture-vs-guides placement rule once ([#2211](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/2225))
- Tests/CI: align the node-agent log-collection e2e assertion with execd's preserved trailing newline ([#2164](https://github.com/opensandbox-group/OpenSandbox/pull/2164)); make the umbrella release workflow dispatch-only by dropping the weekly scheduled dry-run ([#2223](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/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](https://github.com/opensandbox-group/OpenSandbox/pull/2200))
- Behavior change: `networkPolicy` is rejected for Docker network modes `none`/`pasta`/`slirp4netns` — previously accepted silently for `none` ([#2203](https://github.com/opensandbox-group/OpenSandbox/pull/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 ❤️

- @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

```bash
# 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](https://open-sandbox.ai/community/release-verification).

---

### Releases 1.2.0 Rc.2 (`docs/releases/1.2.0-rc.2.md`)

Second release candidate on the `1.2.0` line, carrying everything merged since `release-1.2.0-rc.1` — the headline is the Credential Vault (OSEP-0022) landing end to end for fast sandboxes: an egress-held vault with durable per-subject persistence and pause/resume transfer, the user-reachable server-routed vault API, and a pool-pinned MITM CA. Network policy follows the same egress-held pattern, and `NetworkRule` gains port-scoped rules in the classic profile — fast sandboxes reject them fail-closed until the fastlet nft path enforces them. Images publish now; SDK/CLI packages ship at the `1.2.0` stable bump. Please test and report issues.

## Highlights

- Credential Vault (OSEP-0022) lands end to end for fast sandboxes: the egress sidecar holds the vault with durable per-subject files, sealed snapshot transfer for pause/resume, and stale-subject takeover on slot reuse ([#2249](https://github.com/opensandbox-group/OpenSandbox/pull/2249) [#2252](https://github.com/opensandbox-group/OpenSandbox/pull/2252) [#2255](https://github.com/opensandbox-group/OpenSandbox/pull/2255) [#2250](https://github.com/opensandbox-group/OpenSandbox/pull/2250)); the server exposes the user-reachable `POST|GET|PATCH|DELETE /v1/sandboxes/{id}/credential-vault` API over the FastPath handler relay with credential values never appearing on any public read ([#2263](https://github.com/opensandbox-group/OpenSandbox/pull/2263)), and the MITM CA is pinned per pool so an egress restart no longer rotates the trust anchor under live sandboxes ([#2251](https://github.com/opensandbox-group/OpenSandbox/pull/2251))
- Network policy mirrors the vault: the recorded fast-sandbox policy becomes egress-held state with relay CRUD, durable persistence, and pause/resume snapshot transfer ([#2309](https://github.com/opensandbox-group/OpenSandbox/pull/2309)), and `NetworkRule` gains port-scoped rules end to end in the classic profile — spec, sidecar nftables, server validation, and all five SDKs, while fast sandboxes reject `ports` fail-closed for now — plus an operator-side TCP port floor that no user policy can open ([#2212](https://github.com/opensandbox-group/OpenSandbox/pull/2212))
- Lifecycle and diagnostics polish: `lifecycle.renewalPolicy` replaces the opaque renew extensions key (OSEP-0009) ([#2296](https://github.com/opensandbox-group/OpenSandbox/pull/2296)); the diagnostics context API reaches server, all five SDKs, and the CLI ([#2292](https://github.com/opensandbox-group/OpenSandbox/pull/2292)); execd fixes make start failures distinguishable, preserve session output byte-for-byte, and stop silent disk growth ([#2303](https://github.com/opensandbox-group/OpenSandbox/pull/2303) [#2318](https://github.com/opensandbox-group/OpenSandbox/pull/2318) [#2295](https://github.com/opensandbox-group/OpenSandbox/pull/2295))

## Server

- Implement the server side of the Credential Vault (OSEP-0022, fast-sandbox profile): the user-reachable `POST|GET|PATCH|DELETE /v1/sandboxes/{id}/credential-vault` API pushes complete vault revisions over the FastPath `InvokeHandlerAction` handler relay and treats the relay response as the synchronous egress-validated ack — the CRD never carries vault material, and every public read is sanitized server-side; PATCH composes in memory (`delete → replace → add`, each name resolving at most once) with `expectedRevision` as the cross-replica backstop, non-fsb backends proxy the sidecar's `/credential-vault` surface, and the fsb `credentialProxy` create-time rejection is lifted ([#2263](https://github.com/opensandbox-group/OpenSandbox/pull/2263))
- Bake the pinned MITM CA into server-created SandboxTemplates (OSEP-0022): new `[kubernetes] template_mitm_ca_secret` / `template_mitm_ca_secret_key` config (default off) emits `spec.egressMitmCertificate` — the certificate entry only, never the private key — on every server-created template; the Secret must exist in each template namespace (ops-runbook distribution), and a missed copy fails the build observably ([#2288](https://github.com/opensandbox-group/OpenSandbox/pull/2288))
- Add `lifecycle.renewalPolicy` (OSEP-0009): the renew-on-access opt-in moves from the opaque `extensions["access.renew.extend.seconds"]` create key to a structured `renewalPolicy.extendSeconds` (300–86400), mirroring the `idlePolicy` pattern; the legacy key is deprecated (supported throughout 1.x, removed in 2.0), setting both is accepted only when the parsed values match, the value normalizes into the existing Docker label / Kubernetes annotation so pre-upgrade sandboxes keep renewing, and fast sandbox rejects it with 400 rather than silently accepting a no-op ([#2296](https://github.com/opensandbox-group/OpenSandbox/pull/2296))
- Implement the diagnostics context endpoint (OSEP contract from rc.1): `GET /v1/sandboxes/{sandboxId}/diagnostics/context` returns the sandbox ID, lifecycle state, backend type, and backend-defined identifiers for Docker, Kubernetes, and Fast Sandbox without connecting to execd — Kubernetes context stays available before a Pod is allocated, and extension backends that don't implement it return 501 ([#2292](https://github.com/opensandbox-group/OpenSandbox/pull/2292))
- Forward the still-encoded path through the sandbox proxy: Starlette bound the `{full_path:path}` route from the percent-decoded path and the proxy pasted that value back into the upstream URL, so a decoded `?` started a query, `#` a fragment, `%2F` became a separator, `%25` and non-UTF-8 escapes were rewritten, and `%2E%2E` segments were resolved — the proxy now takes the client's still-encoded tail from the raw path (validating it decodes to the routed path, with a re-encode fallback) and reads the query from the raw query string; backends receive request targets byte-for-byte, matching the ingress gateway, and the `%2E%2E` prefix-escape on Docker host-mapped endpoints is closed ([#2228](https://github.com/opensandbox-group/OpenSandbox/pull/2228))
- Report BatchSandbox expiry while the pod is still Running: a past `spec.expireTime` was still reported as `Running` (Ready pod with IP), so wall-clock billing kept growing after the configured timeout; expiry now checks before the ready states and returns `Terminated` / `SANDBOX_EXPIRED` with `last_transition_at` set to the `expireTime` itself ([#2238](https://github.com/opensandbox-group/OpenSandbox/pull/2238))
- Reject gVisor snapshots that would silently drop writes: `docker commit` under gVisor's default `--overlay2=root:self` can report `READY` while restoring none of the sandbox's writes (only the opaque `/.gvisor.filestore.*` entry is visible); snapshot preflight now resolves the container's runtime and refuses gVisor unless the effective args include `--overlay2=none`, and create fails closed when the filesystem diff is filestore-only ([#2241](https://github.com/opensandbox-group/OpenSandbox/pull/2241))
- Apply `[docker]` security profiles and `secure_runtime` to the execd cache container: it ignored `apparmor_profile` / `seccomp_profile` / `docker_runtime`, so on a host whose default seccomp profile blocks the execd image, every `POST /sandboxes` failed 503 with no configuration escape ([#2254](https://github.com/opensandbox-group/OpenSandbox/pull/2254))
- Make concurrent Docker metadata patches safe: the store's read-modify-write was unlocked and every write shared one temporary name, so two concurrent `PATCH /metadata` requests for a sandbox raced into a 500 (`FileNotFoundError` on rename) or silently dropped the other's key; a store-wide lock plus per-write temp names serialize patches (last write wins per key), a failed write no longer leaves its temp file behind, and the endpoint contract is now documented per runtime — Kubernetes patches write back the whole label set it read, fsb patches send only their own keys fenced by UID and generation ([#2267](https://github.com/opensandbox-group/OpenSandbox/pull/2267) [#2281](https://github.com/opensandbox-group/OpenSandbox/pull/2281))
- Resolve proxy endpoints off the event loop: HTTP/WebSocket proxy handlers called the synchronous `get_endpoint` directly — a Docker socket call per proxied request on Docker, an apiserver read on Kubernetes cache misses — blocking the loop for every other request and stream; a slow Docker daemon (e.g. during an image pull) was enough to freeze the server; the lookup now runs via `asyncio.to_thread` with tenant context preserved ([#2268](https://github.com/opensandbox-group/OpenSandbox/pull/2268))
- Apply the renew cooldown when the Redis consumer is enabled: proxy renews skipped `min_interval_seconds` entirely (every proxied request ran a `get_sandbox` + expiration renew) and queue intents were only throttled per ingress replica, so N replicas renewed up to N times per interval; the cooldown now applies to every renew source and starts only after a successful renew ([#2276](https://github.com/opensandbox-group/OpenSandbox/pull/2276) [#2279](https://github.com/opensandbox-group/OpenSandbox/pull/2279))

## SDKs

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

- Add the Credential Vault client contract to all five SDKs (OSEP-0022): template-backed sandboxes route vault calls through the server on the same template-origin signal as network policy (`OPEN-SANDBOX-ORIGIN: template` or the local `createFromTemplate` flag), while sidecar-direct adapters stay untouched; Python's `Sandbox.credential_vault` now returns the vault service for both profiles, and Go gains `Sandbox`-level vault methods branching on the same signal ([#2263](https://github.com/opensandbox-group/OpenSandbox/pull/2263))
- Add `lifecycle.renewalPolicy` to all five SDKs (OSEP-0009): a typed create field with client-side fail-fast validation (`extendSeconds` 300–86400) wired through the create converters, replacing the opaque extensions key ([#2296](https://github.com/opensandbox-group/OpenSandbox/pull/2296))
- Implement diagnostics across the SDK surface (with [#2292](https://github.com/opensandbox-group/OpenSandbox/pull/2292)): `context` is exposed through both the manager and sandbox facades in all five languages, and JavaScript, Go, and C# gain public logs/events APIs matching the existing Python/JVM surface (custom factories keep working — the new diagnostics service is optional)
- Support port-scoped egress rules in all five SDKs ([#2212](https://github.com/opensandbox-group/OpenSandbox/pull/2212)): `NetworkRule.ports` (TCP 1–65535, at most 256 per rule, `target` now optional) with client-side validation mirroring the server (range, duplicates, cap, domain+ports rejected, netmask-CIDR+ports rejected); JavaScript encodes the target-or-ports invariant as a union type, Python normalizes a blank wire target on read-back

### Python

- Stop propagating a cancelled endpoint fetch to waiters: the shared-fetch endpoint cache set the fetching caller's `CancelledError` on the shared future, so concurrent waiters — e.g. `TaskGroup` siblings — raised `CancelledError` though nothing cancelled them; waiters now re-resolve (cached endpoint, newer in-flight fetch, or their own), while the cancelled caller still raises as before ([#2277](https://github.com/opensandbox-group/OpenSandbox/pull/2277))
- Forward `credential_proxy` through sandbox pools: `PoolCreationSpec.credential_proxy` was dropped by both sync and async creation paths, so warmup and direct-create sandboxes silently lost the requested credential-proxy configuration; custom factories stay compatible (the keyword is omitted when unset) ([#2283](https://github.com/opensandbox-group/OpenSandbox/pull/2283))
- Regenerate the lifecycle client to drop stale hardcoded defaults (`timeout_seconds: 2`, `wake_on_access: true` → UNSET) left behind by the spec change in [#2212](https://github.com/opensandbox-group/OpenSandbox/pull/2212); wire behavior is unchanged — the server applies the same defaults ([#2280](https://github.com/opensandbox-group/OpenSandbox/pull/2280))

### Go

- Authenticate proxied data-plane requests with the API key: proxy mode sent the tenant API key — a credential that can create, pause, and delete every sandbox of the tenant — into the untrusted sandbox as `X-EXECD-ACCESS-TOKEN` (and as egress auth) while omitting the `OPEN-SANDBOX-API-KEY` header the server middleware reads, so multi-tenant proxy requests failed 401 and surfaced as readiness timeouts; the tenant key now rides the server header and is never smuggled into the sandbox or egress sidecar, endpoint-supplied and caller-supplied proxy keys win regardless of casing, and direct mode stays key-free ([#2274](https://github.com/opensandbox-group/OpenSandbox/pull/2274))

## Controller

- Fix task-executor treating an empty exit file as exit code 0: the shim creates the exit file before writing it, and `Inspect` ignored the parse error — a shim killed in between (or a failed write, e.g. full disk) reported a failed or interrupted task as `Succeeded`; an invalid or empty file now falls through to the pid check (`Running` while alive, `ProcessCrashed` once gone) ([#2270](https://github.com/opensandbox-group/OpenSandbox/pull/2270))

## Execd

- Preserve bash session output line endings: `run_in_session` stdout events dropped `\n`/`\r` bytes, so blank lines were unrecoverable and concatenating a run's events did not reconstruct its output; event text now carries its original line ending (blank lines surface as events containing only the ending), aligning the session stream with the foreground `/command` contract — consumers should concatenate event text per run instead of treating each event as a newline-free line ([#2318](https://github.com/opensandbox-group/OpenSandbox/pull/2318))
- Report `exit_code` on command errors: a command that ran and exited non-zero and a command whose process never started (e.g. `fork/exec: argument list too long`) reported the same error event, so clients reading the exit status from `evalue` could report a start failure as success; the new optional `error.exit_code` is present only when the process ran and finished non-zero (`-1` on signal termination) ([#2303](https://github.com/opensandbox-group/OpenSandbox/pull/2303))
- Serve `binds` mount points through the isolated session files API: the merged-view router only registered overlay mounts, so `session.files` failed with `ErrPathOutsideOverlays` on the same bind destinations that `session.run` could read; each bind now registers a routed view (`dest → source`, read-only binds reject writes, bind views shadow overlays on prefix ties) ([#2299](https://github.com/opensandbox-group/OpenSandbox/pull/2299))
- Truncate command output files before unlink: forked children (daemons) legitimately outlive commands while holding the output fd, so an unlinked file kept consuming disk as invisible `(deleted)` growth until the last holder exited; every existing deletion point now truncates to zero first, and background output is only touched at janitor deletion time so the retention window stays readable ([#2295](https://github.com/opensandbox-group/OpenSandbox/pull/2295))
- Omit invalid upper-usage metrics on scan errors: a failed filesystem scan emitted a misleading `execd.isolation.upper.usage_bytes` of 0 while sessions and data still existed; invalid readings are now omitted (a valid empty upper still reports 0), and quota admission fails closed when `upper_max_bytes > 0` and a scan fails instead of silently bypassing accounting ([#2257](https://github.com/opensandbox-group/OpenSandbox/pull/2257))
- Avoid redundant JDK MITM CA imports: sandboxes with several installed JDKs often share one trust store, but each JDK re-ran the keytool import (~6.2 s for four JDKs); initialization is now deduplicated by resolved trust-store path with certificate-content comparison, cutting the stage to ~0.6 s while still replacing rotated certificates ([#2310](https://github.com/opensandbox-group/OpenSandbox/pull/2310))
- Default PTY sessions to `TERM=xterm-256color` when the resolved user environment has no usable `TERM`, so interactive programs get colors; an explicit `TERM` always wins ([#2313](https://github.com/opensandbox-group/OpenSandbox/pull/2313))

## Networking

### egress

The OSEP-0022 vault items concern the `fast-sandbox` profile's credential-vault surface; the OSEP-0023 follow-ups remain scoped to the default-off experimental revision runtime.

- Add the egress-held vault for fast sandboxes (OSEP-0022), landing in stages: the internal `/vault/{sandboxUid}` relay surface with create-only POST (409 on duplicate, atomic across server replicas), full-revision PUT with `expectedRevision` and an `expectedIncarnation` precondition so a stale writer can never land on a reset's successor, value-including GET, and DELETE that keeps the policy ([#2249](https://github.com/opensandbox-group/OpenSandbox/pull/2249)); durable per-subject persistence — one file per subject under a Pod-scoped volume (optional tmpfs), the same commit protocol as policy files so **acked means durable and applied**, incarnation-fenced reload, and purge on unbind ([#2252](https://github.com/opensandbox-group/OpenSandbox/pull/2252)); and the superseded fastlet-proxy vault route removed with its pending-push cache ([#2247](https://github.com/opensandbox-group/OpenSandbox/pull/2247))
- Carry the vault across pause/resume (OSEP-0022 "Vault Lifecycle"): `VAULT_EXPORT` copies the vault file into the snapshot bundle strictly before runtime teardown and seals the subject (all vault ops answer 409 while paused; enforcement continues), and `VAULT_IMPORT` is the only authorized incarnation transfer — the bundle re-stamps with the restored fence before `data-plane-ready`, validated against the current policy before any durable effect; a genuine export failure leaves the subject unsealed and fails the pause checkpoint, so teardown never runs without a bundle and the egress-local vault survives for the retry ([#2255](https://github.com/opensandbox-group/OpenSandbox/pull/2255)); a `vaultTransfer` capability signal lets an egress without vault persistence skip the transfer entirely — the checkpoint proceeds without the vault (pre-transfer semantics) instead of failing ([#2290](https://github.com/opensandbox-group/OpenSandbox/pull/2290))
- Define stale-subject takeover on slot reuse: the fast-sandbox dispatch key is the slot IP and slots are reused, so a `SET_BINDING` can claim a key still held by a dead-but-registered subject — egress now treats that as authoritative, tearing down the stale subject's nft rules, DNS refcount, MITM redirect, and vault slice under the registry lock before registering the successor deny-first; traffic is never dispatched to stale rules, late unloads stay fence-guarded no-ops, and every failure path leaves each subject either fully registered or fully evicted ([#2250](https://github.com/opensandbox-group/OpenSandbox/pull/2250))
- Pin the fast-sandbox MITM CA (OSEP-0022 "MITM Trust Anchor"): the pool-pinned, versioned CA is seeded from a read-only secret mount into the mitmdump confdir (idempotent, fail-closed on an unloadable pair), replacing generate-on-start — an egress restart no longer rotates the CA and silently invalidates every live sandbox's TLS trust; a missing or mismatched CA refuses only the MITM addon (policy enforcement unaffected) with `mitmCaFingerprint` / `mitmCaDown` surfaced on `/healthz` and the actions status, so a template↔pool Secret mispairing is visible at a glance ([#2251](https://github.com/opensandbox-group/OpenSandbox/pull/2251))
- Align the fast-sandbox network policy with the vault pattern: the recorded policy becomes egress-held state — a `/policy/{sandboxUid}` relay surface with revision preconditions (unknown subject 404, incarnation mismatch 412, sealed 409), durable per-subject files using the same commit protocol, `POLICY_EXPORT`/`POLICY_IMPORT` snapshot transfer mirroring the vault, and, after an egress restart, the committed file superseding the replayed create-time binding input ([#2309](https://github.com/opensandbox-group/OpenSandbox/pull/2309))
- Support port-scoped egress rules end to end (classic profile): `NetworkRule.ports` (TCP 1–65535, ≤256 per rule) makes `target` optional — port-only rules, or ports combined with an IP/CIDR; domain+`ports` is rejected since domain rules evaluate at the DNS proxy layer; deny-before-allow precedence holds across scoping, and a PATCH merge-key bug that let a second port-only rule evict the first is fixed; an operator-shipped `reachable.ports.cap` whitelist is enforced as an always-deny ahead of every user rule so even `defaultAction: allow` cannot open a non-whitelisted port; the fast-sandbox route rejects `ports` with 400 until the fastlet nft path can enforce them (fail-closed, documented) ([#2212](https://github.com/opensandbox-group/OpenSandbox/pull/2212))
- Add static TLS passthrough selectors to the experimental live-revision runtime: `OPENSANDBOX_EGRESS_MITMPROXY_PASSTHROUGH_HOSTS` is a bounded exact/wildcard host array applied at ClientHello with 443-overlap rejection; existing nonempty native `ignore_hosts`/`allow_hosts` now **reject startup in live mode** — migrate static TLS bypass entries to the array (legacy mode unchanged, no public interceptionMode added) ([#2229](https://github.com/opensandbox-group/OpenSandbox/pull/2229))
- Emit TLS admission counters: one best-effort event per final ClientHello admission feeds `egress.mitm.tls.connections_total` with closed `mode`/`decision`/`reason` labels — it measures admission decisions, not handshakes or requests, and is explicitly not security evidence ([#2265](https://github.com/opensandbox-group/OpenSandbox/pull/2265))
- Add the installation-only subject revision IPC foundation (OSEP-0023 Phase 4): an authenticated v2 Unix-socket channel routes target-bound revision transactions to independent, admission-disabled installation receivers with frozen request bytes and lost-reply reconciliation; no production caller yet ([#2284](https://github.com/opensandbox-group/OpenSandbox/pull/2284))
- Ship `egress-trace`, a read-only admission viewer inside the egress image: live policy state, nft sets and DNS leases with TTLs, real outbound connections annotated with their admission verdict, and a `probe <domain>` subcommand separating policy denial from resolver failure; no sidecar behavior change, and decrypted L7 contents are deliberately never shown ([#2230](https://github.com/opensandbox-group/OpenSandbox/pull/2230))

## Misc

- Console: land the third of five planned console PRs — admin and platform ops, with `/api/admin/sandboxes` aggregation, per-tenant detail proxying, a pools API (documented as not tenant-isolated), and admin global sandbox list / pools / overview KPIs in the web UI ([#2131](https://github.com/opensandbox-group/OpenSandbox/pull/2131)); preserve empty successful upstream responses in the BFF proxy instead of failing JSON decode on 200/201/205 ([#2271](https://github.com/opensandbox-group/OpenSandbox/pull/2271))
- CLI: add `osb terminal <sandbox-id>`, attaching the local terminal to a real PTY inside the sandbox over the execd PTY WebSocket — raw mode with `SIGWINCH`-driven resize, replay on reconnect, remote exit code propagation, and support for both direct and server-proxy connection modes (interactive TTY, POSIX) ([#2313](https://github.com/opensandbox-group/OpenSandbox/pull/2313)); report out-of-range duration inputs as Click validation errors instead of uncaught `OverflowError`/`ValueError` ([#2272](https://github.com/opensandbox-group/OpenSandbox/pull/2272))
- OSEPs: OSEP-0022 revised — the vault rides the action binding as an AES-256-GCM-sealed revision on one control channel, with the per-pool pinned CA as the MITM trust anchor ([#2174](https://github.com/opensandbox-group/OpenSandbox/pull/2174)); the planned OpenSandbox runtime driver ask is withdrawn (the firecracker driver ships as-is; deny-first timing replaces the pre-create healthz probe) ([#2259](https://github.com/opensandbox-group/OpenSandbox/pull/2259)); MITM CA namespace distribution becomes an ops-runbook deployment prerequisite instead of a resident sync controller ([#2282](https://github.com/opensandbox-group/OpenSandbox/pull/2282))
- Examples: add an all-in-one Docker image for the DeerFlow example bundling the OpenSandbox server (host Docker runtime) and the DeerFlow backend, for one-command workshop starts without a local Python toolchain ([#2262](https://github.com/opensandbox-group/OpenSandbox/pull/2262))
- Docs: reflow hard-wrapped markdown prose into single-line paragraphs across 148 files (~7.4k collapsed fragments, rendering unchanged) and codify the prose style rule in `AGENTS.md` ([#2285](https://github.com/opensandbox-group/OpenSandbox/pull/2285))
- Tests/CI: cache Go modules for the five Kubernetes nightly e2e jobs (`setup-go` silently skipped its cache without a root `go.sum`, re-downloading the full module graph nightly) ([#2306](https://github.com/opensandbox-group/OpenSandbox/pull/2306)); close the credential-vault happy path in the fast-sandbox smoke — relay CRUD, injection through the real mitm for every auth type, rotation, restart durability, and purge ([#2256](https://github.com/opensandbox-group/OpenSandbox/pull/2256)); dump every fastlet Pod container's logs (including egress) on pause/resume verify failure ([#2290](https://github.com/opensandbox-group/OpenSandbox/pull/2290))
- release: bump platform version to `1.2.0-rc.2` across charts and image references

## Upgrade & Compatibility

- Breaking: bash-session stdout event text now includes its original line endings, and blank lines appear as events — consumers must concatenate event text per run to recover exact output instead of treating each event as a newline-free line ([#2318](https://github.com/opensandbox-group/OpenSandbox/pull/2318))
- Behavior change: a BatchSandbox past its `spec.expireTime` now reports `Terminated` / `SANDBOX_EXPIRED` instead of `Running`, even while the pod is still Ready — callers that treat `Running` as alive should check expiry ([#2238](https://github.com/opensandbox-group/OpenSandbox/pull/2238))
- Behavior change: Docker-backed snapshots of gVisor containers without `--overlay2=none` are rejected with `SNAPSHOT::UNSUPPORTED_RUNTIME` (409) instead of publishing a silently incomplete image ([#2241](https://github.com/opensandbox-group/OpenSandbox/pull/2241))
- Behavior change (experimental egress live mode only): nonempty native `ignore_hosts`/`allow_hosts` now reject startup — migrate static TLS bypass entries to `OPENSANDBOX_EGRESS_MITMPROXY_PASSTHROUGH_HOSTS` ([#2229](https://github.com/opensandbox-group/OpenSandbox/pull/2229))
- Deprecation: the `extensions["access.renew.extend.seconds"]` create key is deprecated in favor of `lifecycle.renewalPolicy.extendSeconds` — supported throughout 1.x, removed in 2.0; setting both is accepted only when values match ([#2296](https://github.com/opensandbox-group/OpenSandbox/pull/2296))
- Behavior change: execd upper-directory quota admission fails closed when `upper_max_bytes > 0` and a usage scan fails (previously the quota check was silently bypassed) ([#2257](https://github.com/opensandbox-group/OpenSandbox/pull/2257))
- Images: `opensandbox/<component>:release-1.2.0-rc.2` (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 ❤️

- @Aling33
- @BootstrapperSBL
- @JasonBuildAI
- @Jun516
- @Pangjiping
- @acodercat
- @alex-feel
- @comqx
- @hardness1020
- @hpliStartAgain
- @ninan-nn
- @pingkui

## Artifacts

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

## Installation

```bash
# 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.2
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](https://open-sandbox.ai/community/release-verification).

---

### Releases TEMPLATE (`docs/releases/TEMPLATE.md`)

## Highlights



-

## Server



-

## SDKs



-

## Controller



-

## Execd



-

## Networking



-

## Fast Sandbox



-

## Misc



-

## Upgrade & Compatibility



- Images: `opensandbox/<component>:release-X.Y.Z` (all three registries)
- Packages: server / CLI / SDKs all at `X.Y.Z`
- Charts: render from this tag (`helm template ./manifests/charts/opensandbox`)
- Kubernetes: supported range vX.Y – vW.Z
- Skew: server ↔ CLI/SDK same line supported; ±1 minor warns

## 👥 Contributors

Thanks to these contributors ❤️

-

## Artifacts



- BOM: [docs/releases/X.Y.Z.yaml](./X.Y.Z.yaml)
- Images: `opensandbox/<component>:release-X.Y.Z` (Docker Hub / GHCR / ACR)
- Packages: PyPI ×5, npm ×2, Maven Central ×5, NuGet ×2 — all at `X.Y.Z`
- Go module: `github.com/alibaba/OpenSandbox/sdks/sandbox/go@vX.Y.Z`

## Installation

```bash
# Platform (Kubernetes) — render the chart at this tag and apply
git clone https://github.com/opensandbox-group/OpenSandbox
cd OpenSandbox
git checkout release-X.Y.Z
helm dependency build manifests/charts/opensandbox  # package file:// sub-charts (not committed)
helm template ./manifests/charts/opensandbox | kubectl apply -f -
# or point your GitOps platform (Argo / Flux) at the repo path + tag

# SDKs
pip install opensandbox==X.Y.Z            # Python
npm install @alibaba-group/opensandbox@X.Y.Z   # JavaScript
# Kotlin/JVM: implementation("com.alibaba.opensandbox:sandbox:X.Y.Z")
dotnet add package Alibaba.OpenSandbox --version X.Y.Z
go get github.com/alibaba/OpenSandbox/sdks/sandbox/go@vX.Y.Z

# Run the server locally
uvx opensandbox-server==X.Y.Z
```

---

### Sdks Csharp (`docs/sdks/csharp.md`)

---
title: C# SDK
description: C# SDK for creating, managing, and interacting with secure OpenSandbox environments.
---

# OpenSandbox SDK for C\#

A C# SDK for low-level interaction with OpenSandbox. It provides the ability to create, manage, and interact with secure sandbox environments, including executing shell commands, managing files, and reading resource metrics.

## Installation

### NuGet

```bash
dotnet add package Alibaba.OpenSandbox
```

### Package Manager

```powershell
Install-Package Alibaba.OpenSandbox
```

## Quick Start

Run this example in a .NET 8+ console project with implicit usings enabled. It creates a sandbox, runs a shell command, and releases remote and local resources.

::: tip
Before running this example, ensure the OpenSandbox service is running. See the [Getting Started](/getting-started/) guide for startup instructions.
:::

```csharp
using OpenSandbox;
using OpenSandbox.Config;
using OpenSandbox.Core;

var config = new ConnectionConfig(new ConnectionConfigOptions
{
    Domain = "api.opensandbox.io",
    ApiKey = "your-api-key",
    // Protocol = ConnectionProtocol.Https,
    // RequestTimeoutSeconds = 60,
});

try
{
    await using var sandbox = await Sandbox.CreateAsync(new SandboxCreateOptions
    {
        ConnectionConfig = config,
        Image = "ubuntu",
        TimeoutSeconds = 10 * 60,
    });

    try
    {
        var execution = await sandbox.Commands.RunAsync("echo 'Hello Sandbox!'");
        Console.WriteLine(execution.Logs.Stdout.FirstOrDefault()?.Text);
    }
    finally
    {
        await sandbox.KillAsync();
    } // await using releases the client even if KillAsync fails.
}
catch (SandboxException ex)
{
    Console.Error.WriteLine($"Sandbox Error: [{ex.Error.Code}] {ex.Error.Message}");
    Console.Error.WriteLine($"Request ID: {ex.RequestId}");
}
```

## Lifecycle Hooks

Set `Lifecycle` in `SandboxCreateOptions`. `PreStart` completes before the entrypoint starts, while `Periodic` hooks run on their schedules after startup.

```csharp
using OpenSandbox.Models;

await using var sandbox = await Sandbox.CreateAsync(new SandboxCreateOptions
{
    ConnectionConfig = config,
    Image = "ubuntu:24.04",
    Lifecycle = new SandboxLifecycle
    {
        PreStart = new LifecycleHook
        {
            Command = new[] { "sh", "-c", "echo ready > /tmp/prestart.done" },
            TimeoutSeconds = 120,
        },
        Periodic = new[]
        {
            new PeriodicLifecycleHook
            {
                Name = "checkpoint",
                Schedule = "@every 5m",
                Command = new[] { "sh", "-c", "date -u >> /tmp/checkpoints.log" },
                TimeoutSeconds = 120,
            },
        },
    },
});
```

The Server validates `TimeoutSeconds`; `PreStart` accepts 1–10800 seconds, while `Periodic` accepts 1–300 seconds. Both default to 60 seconds when omitted. See [Lifecycle Hooks](/guides/lifecycle-hooks) for timing, failure behavior, and provider limitations.

### Idle policy (auto-pause and wake-on-access)

`SandboxLifecycle.IdlePolicy` (OSEP-0024, fast-sandbox only) tells the control plane to auto-pause the sandbox after `IdleTimeoutSeconds` (30–86400) of continuously idle ingress-observed traffic, and to wake it on the next request. It is control-plane state: it never reaches the guest, is accepted in template mode (`SandboxCreateFromTemplateOptions.IdlePolicy`), and is rejected by non-fast-sandbox backends. Omitting it preserves non-automated behavior exactly.

```csharp
var sandbox = await Sandbox.CreateAsync(new SandboxCreateOptions
{
    ConnectionConfig = config,
    Image = "ubuntu:24.04",
    Lifecycle = new SandboxLifecycle
    {
        IdlePolicy = new IdlePolicy
        {
            IdleTimeoutSeconds = 300,
            OnIdle = OnIdleAction.Pause,
            WakeOnAccess = true
        }
    }
});
```

`ActiveProbe` lets the application veto a pause while real work runs without producing ingress traffic: declare an endpoint answering `{"active": bool, "message"?: string}` and the server polls it when the sandbox is otherwise idle; a busy answer resets the idle clock. Invalid policies fail fast client-side through `IdlePolicy.Validate()`.

### Renewal policy (renew-on-access)

`SandboxLifecycle.RenewalPolicy.ExtendSeconds` (OSEP-0009) opts the sandbox into automatic expiration extension on observed access: each proxied or gateway-observed request moves the expiration to `max(now + ExtendSeconds, current expiresAt)`. It is control-plane state and never reaches the guest. Requires the server operator to enable `[renew_intent]` — see [Renew-on-Access](/guides/renew-on-access). The deprecated `extensions["access.renew.extend.seconds"]` key remains supported through 1.x and is removed in 2.0.

```csharp
var sandbox = await Sandbox.CreateAsync(new SandboxCreateOptions
{
    ConnectionConfig = config,
    Image = "ubuntu:24.04",
    Lifecycle = new SandboxLifecycle
    {
        RenewalPolicy = new RenewalPolicy { ExtendSeconds = 3600 }
    }
});
```

### Wake retry

A request to a paused sandbox may arrive while its restore is still in progress. The gateway answers `503` with the `OpenSandbox-Wake: expired` header; the HTTP transport recognizes it and resends the original request after the `Retry-After` delay (safe for any method — the marker proves the request was never forwarded). The wake budget defaults to 5 attempts / 60 seconds across all wake cycles; when it is exhausted the SDK throws `SandboxWakeTimeoutException` (carrying `Attempts` and `Waited`) while the server-side resume continues in the background. Bare `503` responses follow the existing rules untouched. Tune it on the connection config: `WakeRetryEnabled` (default true), `WakeMaxAttempts` (5), `WakeOverallDeadlineSeconds` (60, non-positive for unbounded). SSE and file stream transports bypass wake retry.

## Usage Examples

The snippets below use `config` and a live `sandbox` from the quick start. Run them before its cleanup block, with `using OpenSandbox.Models;` at the top of the file. Creation examples are alternatives. `await using` releases local clients; call `KillAsync()` to terminate sandboxes that are no longer needed.

### 1. Lifecycle Management

Manage the sandbox lifecycle, including renewal, pausing, and resuming.

```csharp
var info = await sandbox.GetInfoAsync();
Console.WriteLine($"State: {info.Status.State}");
Console.WriteLine($"Created: {info.CreatedAt}");
Console.WriteLine($"Expires: {info.ExpiresAt}"); // null when manual cleanup mode is used

await sandbox.PauseAsync();

using var pauseBudget = new CancellationTokenSource(TimeSpan.FromMinutes(2));
while (true)
{
    var current = await sandbox.GetInfoAsync(pauseBudget.Token);
    if (current.Status.State == SandboxStates.Paused) break;
    if (current.Status.State == "Failed")
        throw new InvalidOperationException(current.Status.Message);
    await Task.Delay(1000, pauseBudget.Token);
}

// Resume returns a fresh local handle.
await using var resumed = await sandbox.ResumeAsync();

// Renew: expiresAt = now + timeoutSeconds
await resumed.RenewAsync(30 * 60);
```

Pause is asynchronous and runtime-dependent. See [Pause and Resume](/guides/pause-resume).

Create a non-expiring sandbox by setting `ManualCleanup = true`:

```csharp
var manual = await Sandbox.CreateAsync(new SandboxCreateOptions
{
    ConnectionConfig = config,
    Image = "ubuntu",
    ManualCleanup = true,
});
```

::: info
`TimeoutSeconds = null` uses the default TTL. Set `ManualCleanup = true` to disable automatic expiration.
:::

### Connect to an Existing Sandbox

Use `ConnectAsync` when you already have a sandbox ID and need a new SDK instance bound to it.

```csharp
await using var connected = await Sandbox.ConnectAsync(new SandboxConnectOptions
{
    SandboxId = "existing-sandbox-id",
    ConnectionConfig = config
});
```

### 2. Custom Health Check

Resolving an endpoint confirms that a route exists; it does not confirm that the application on that port is healthy. For service readiness, make a bounded request to the application's health endpoint and include the returned endpoint headers.

Define custom logic to determine whether the sandbox is ready/healthy.

```csharp
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(2) };
await using var sandbox = await Sandbox.CreateAsync(new SandboxCreateOptions
{
    ConnectionConfig = config,
    Image = "nginx:latest",
    Entrypoint = new[] { "nginx", "-g", "daemon off;" },
    HealthCheck = async (sbx) =>
    {
        var ep = await sbx.GetEndpointAsync(80);
        using var request = new HttpRequestMessage(HttpMethod.Get, await sbx.GetEndpointUrlAsync(80));
        foreach (var header in ep.Headers)
            request.Headers.TryAddWithoutValidation(header.Key, header.Value);
        try
        {
            using var response = await http.SendAsync(request);
            return response.StatusCode == System.Net.HttpStatusCode.OK;
        }
        catch (HttpRequestException) { return false; }
        catch (TaskCanceledException) { return false; }
    },
});
```

### 3. Command Execution & Streaming

Execute commands and handle output streams in real-time.

```csharp
using OpenSandbox.Models;

var handlers = new ExecutionHandlers
{
    OnStdout = msg => { Console.WriteLine($"STDOUT: {msg.Text}"); return Task.CompletedTask; },
    OnStderr = msg => { Console.Error.WriteLine($"STDERR: {msg.Text}"); return Task.CompletedTask; },
    OnExecutionComplete = c => { Console.WriteLine($"Finished in {c.ExecutionTimeMs}ms"); return Task.CompletedTask; },
};

await sandbox.Commands.RunAsync(
    "for i in 1 2 3; do echo \"Count $i\"; sleep 0.2; done",
    handlers: handlers
);
```

To execute a native program without shell parsing, pass an argument list. On Linux, this example prints literal `$HOME` and keeps `hello world` as one argument:

```csharp
await sandbox.Commands.RunAsync(new[] { "printf", "%s\n", "$HOME", "hello world" });
```

Native argv execution requires an updated execd. See [command execution modes](/architecture/data-plane/execd#command-execution) for executable lookup and platform behavior.

#### Background commands

Poll status and incremental logs. Command timeout is separate from sandbox TTL.

```csharp
var execution = await sandbox.Commands.RunAsync(
    "for i in 1 2 3; do echo step-$i; sleep 1; done",
    options: new RunCommandOptions
    {
        Background = true,
        TimeoutSeconds = 30,
    });

var commandId = execution.Id ?? throw new InvalidOperationException("No command ID returned");
long cursor = 0;
using var budget = new CancellationTokenSource(TimeSpan.FromSeconds(45));
try
{
    while (true)
    {
        var status = await sandbox.Commands.GetCommandStatusAsync(commandId, budget.Token);
        var logs = await sandbox.Commands.GetBackgroundCommandLogsAsync(commandId, cursor, budget.Token);
        Console.Write(logs.Content);
        cursor = logs.Cursor ?? cursor;
        if (status.Running == false)
        {
            if (status.ExitCode != 0)
                throw new InvalidOperationException($"Command failed: {status.ExitCode}, {status.Error}");
            break;
        }
        await Task.Delay(500, budget.Token);
    }
}
catch (OperationCanceledException) when (budget.IsCancellationRequested)
{
    await sandbox.Commands.InterruptAsync(commandId);
    throw;
}
```

#### Persistent shell sessions

A Bash session preserves shell variables and the working directory across commands.

```csharp
var sessionId = await sandbox.Commands.CreateSessionAsync(
    new CreateSessionOptions { WorkingDirectory = "/tmp" });
try
{
    await sandbox.Commands.RunInSessionAsync(sessionId, "export DEMO=hello");
    var result = await sandbox.Commands.RunInSessionAsync(sessionId, "echo \"$DEMO\"; pwd");
    foreach (var message in result.Logs.Stdout) Console.Write(message.Text);
}
finally
{
    await sandbox.Commands.DeleteSessionAsync(sessionId);
}
```

#### Persistent environment variables

Set environment variables that the runtime injects into every subsequent command and session — without hand-writing shell escaping against the sandbox env file.

```csharp
await sandbox.Commands.SetEnvAsync("MY_TOKEN", "it's a safe value");
```

Keys must match `[A-Za-z_][A-Za-z0-9_]*`. Values without a single quote are stored verbatim; values containing a single quote use the env file's double-quoted form, in which shell-style `$NAME` sequences may be expanded when the runtime loads the file. The env file is append-only: the last write for a key wins. Throws `SandboxException` if the sandbox fails to persist the variable.

For filesystem/process isolation within a sandbox, see [Isolation Sessions](/guides/isolation-sessions). These are separate from Bash sessions.

### 4. File Operations

Manage files and directories, including read, write, list/search, and delete.

```csharp
await sandbox.Files.CreateDirectoriesAsync(new[]
{
    new CreateDirectoryEntry { Path = "/tmp/demo", Mode = 755 }
});

await sandbox.Files.WriteFilesAsync(new[]
{
    new WriteEntry { Path = "/tmp/demo/hello.txt", Data = "Hello World", Mode = 644 }
});

var content = await sandbox.Files.ReadFileAsync("/tmp/demo/hello.txt");
Console.WriteLine($"Content: {content}");

var entries = await sandbox.Files.ListDirectoryAsync("/tmp/demo", depth: 1);
foreach (var entry in entries) Console.WriteLine(entry.Path);

var files = await sandbox.Files.SearchAsync(new SearchEntry { Path = "/tmp/demo", Pattern = "*.txt" });
foreach (var file in files)
{
    Console.WriteLine(file.Path);
}

await sandbox.Files.DeleteFilesAsync(new[] { "/tmp/demo/hello.txt" });
await sandbox.Files.DeleteDirectoriesAsync(new[] { "/tmp/demo" });
```

For binary data, use `byte[]` or `Stream` in `WriteEntry.Data`, and download with `ReadBytesAsync()` or `ReadBytesStreamAsync()`. Read options support partial downloads.

#### File operations with an explicit identity

Bind a filesystem client to a Linux UID and GID:

```csharp
var files = sandbox.FilesWithIdentity(1001, 2000);
```

The bound client exposes the usual file operations and shares the sandbox connection, authentication, and endpoint routing headers. The existing file client retains its default identity. Select the UID/GID separately for command execution.

This requires Linux and an Execd version that supports filesystem execution identity. Unsupported servers return an error; requests never fall back to the default filesystem routes. Keep sandbox credentials and identity selection in the trusted backend. Linux filesystem permissions apply; this feature does not create a tenant isolation boundary. See [Execd](/architecture/data-plane/execd) for permission and workspace provisioning details.

### 5. Endpoints

`GetEndpointAsync()` returns an endpoint **without a scheme** (for example `"localhost:44772"`). Use `GetEndpointUrlAsync()` if you want a ready-to-use absolute URL.

```csharp
var endpoint = await sandbox.GetEndpointAsync(44772);
Console.WriteLine(endpoint.EndpointAddress);

var url = await sandbox.GetEndpointUrlAsync(44772);
Console.WriteLine(url); // e.g., "http://localhost:44772"
```

Forward `endpoint.Headers` on requests to the sandbox service, including any secure-access credentials. The health-check example above shows an HTTP request to an application listening on the target port.

### 6. Sandbox Management (Admin)

Use `SandboxManager` for administrative tasks and finding existing sandboxes.

```csharp
await using var manager = SandboxManager.Create(new SandboxManagerOptions
{
    ConnectionConfig = config
});

var list = await manager.ListSandboxInfosAsync(new SandboxFilter
{
    States = new[] { SandboxStates.Running },
    Page = 1, // First page only; increase for subsequent pages.
    PageSize = 10
});

foreach (var s in list.Items)
{
    Console.WriteLine(s.Id);
}
```

### Resource metrics

Read current sandbox resource usage with `await sandbox.Metrics.GetMetricsAsync()`. This is separate from [SDK creation telemetry](/sdks/observability#creation-metrics).

## Snapshots, templates, and metadata

| Operation | Public API |
| --- | --- |
| Snapshot a sandbox | `sandbox.CreateSnapshotAsync` or `manager.CreateSnapshotAsync` |
| Inspect/list/delete snapshots | `manager.GetSnapshotAsync`, `ListSnapshotsAsync`, `DeleteSnapshotAsync` |
| Restore a snapshot | `Sandbox.CreateAsync` with `SnapshotId` and no `Image`/`Entrypoint` |
| Manage Fsb templates | `manager.CreateTemplateAsync`, `GetTemplateAsync`, `ListTemplatesAsync`, `DeleteTemplateAsync` |
| Create from a published template | `Sandbox.CreateFromTemplateAsync` with `SandboxCreateFromTemplateOptions` |
| Patch metadata | `sandbox.PatchMetadataAsync` or `manager.PatchSandboxMetadataAsync` |

Poll snapshot status before restoring. Template builds are asynchronous; wait for `Succeeded` before use. Template-backed creation requires `TimeoutSeconds` and inherits workload configuration from the published template. Metadata patch values add/replace keys; `null` deletes a key. See the [lifecycle contract](/api/#1-sandbox-lifecycle-yml) for backend constraints.

Snapshot support depends on the runtime and server configuration. Renew the source sandbox first if its remaining TTL may expire during snapshot creation. This example waits up to 15 minutes and retains the snapshot for reuse:

```csharp
await using var manager = SandboxManager.Create(new SandboxManagerOptions { ConnectionConfig = config });
var snapshot = await sandbox.CreateSnapshotAsync("demo");
Console.WriteLine($"Snapshot: {snapshot.Id}");
using var budget = new CancellationTokenSource(TimeSpan.FromMinutes(15));
while (true)
{
    snapshot = await manager.GetSnapshotAsync(snapshot.Id, budget.Token);
    if (snapshot.Status.State == "Ready") break;
    if (snapshot.Status.State == "Failed")
        throw new InvalidOperationException(snapshot.Status.Message);
    await Task.Delay(2000, budget.Token);
}
await using var restored = await Sandbox.CreateAsync(new SandboxCreateOptions
{
    SnapshotId = snapshot.Id,
    ConnectionConfig = config,
});
try { Console.WriteLine(restored.Id); }
finally { await restored.KillAsync(); }
// When no longer needed: await manager.DeleteSnapshotAsync(snapshot.Id);
```

Use an existing template after its build reaches `Succeeded`:

```csharp
await using var templated = await Sandbox.CreateFromTemplateAsync(new SandboxCreateFromTemplateOptions
{
    TemplateId = "your-published-template-id",
    TimeoutSeconds = 600,
    ConnectionConfig = config,
});
```

Add/replace a metadata key and remove another:

```csharp
await sandbox.PatchMetadataAsync(new Dictionary<string, string?>
{
    ["project"] = "demo",
    ["obsolete-key"] = null,
});
```

## Configuration

### 1. Connection Configuration

The `ConnectionConfig` class manages API server connection settings.

::: info Transfer timeouts
`RequestTimeoutSeconds` bounds the wait for response headers, not the whole transfer. The deadline starts with the request (so the connect phase is bounded too), is cleared while the request body is written, and is re-armed once the body of the current hop has been sent: a healthy upload or download that keeps making progress is never cut off by it. A download that gets no response headers, or an upload that gets none within the timeout after its body has been sent, fails with a `TaskCanceledException` carrying a `TimeoutException` instead of hanging (the Go SDK's `ResponseHeaderTimeout` contract); a caller cancellation still surfaces as an `OperationCanceledException` with the caller's token. All non-transfer calls keep the whole-call request timeout.

A request body that never finishes being sent, and a response body that stops making progress after the headers have arrived, are bounded only by the `CancellationToken` you pass to the call, so pass one when a transfer needs an end-to-end cap. The upload guarantee assumes the HTTP handler streams the request body (for example `SocketsHttpHandler` on .NET 5+): the default `HttpClientHandler` on .NET Framework buffers the request body before sending it, so a slow but progressing upload can still count against the header timeout there.

A 307/308 redirect resends the request body once per hop. A seekable upload source (`FileStream`, `MemoryStream`, ...) is rewound and resent in full; a non-seekable source cannot be replayed without buffering it, so the upload fails with a clear error instead of sending an empty file part to the redirected endpoint.
:::

| Parameter | Description | Default | Environment Variable |
| --- | --- | --- | --- |
| `ApiKey` | API key for authentication | Optional | `OPEN_SANDBOX_API_KEY` |
| `Domain` | Sandbox service domain (`host[:port]`) | `localhost:8080` | `OPEN_SANDBOX_DOMAIN` |
| `Protocol` | HTTP protocol (`Http`/`Https`) | `Http` | - |
| `RequestTimeoutSeconds` | Request timeout applied to SDK HTTP calls | `30` | - |
| `UseServerProxy` | Request server-proxied sandbox endpoint URLs | `false` | - |
| `Headers` | Extra headers applied to every request | `{}` | - |
| `DisableMetrics` | Disable SDK create-latency telemetry (see [SDK Telemetry](/sdks/observability#creation-metrics)) | `false` | `OPENSANDBOX_DISABLE_METRICS` |

```csharp
using OpenSandbox.Config;

// 1. Basic configuration
var config = new ConnectionConfig(new ConnectionConfigOptions
{
    Domain = "api.opensandbox.io",
    ApiKey = "your-key",
    RequestTimeoutSeconds = 60,
    // UseServerProxy = true, // Useful when the client cannot access sandbox endpoint directly
});

// 2. Advanced: custom headers
var config2 = new ConnectionConfig(new ConnectionConfigOptions
{
    Domain = "api.opensandbox.io",
    ApiKey = "your-key",
    Headers = new Dictionary<string, string>
    {
        ["X-Custom-Header"] = "value"
    },
});
```

::: tip SDK Telemetry
`Sandbox.CreateAsync` reports create latency to `POST /v1/metrics/events` by default. Set `ConnectionConfigOptions.DisableMetrics = true` or export `OPENSANDBOX_DISABLE_METRICS=1` to opt out. See [SDK Telemetry](/sdks/observability#creation-metrics).
:::

### 2. SDK Logging

The SDK uses `Microsoft.Extensions.Logging` abstractions. `SdkDiagnosticsOptions`
configures local logging; it does not retrieve remote sandbox diagnostic logs or
events. For remote diagnostics, use `GetDiagnosticContextAsync`,
`GetDiagnosticLogsAsync`, and `GetDiagnosticEventsAsync` on `SandboxManager`
or `Sandbox`; see [SDK diagnostics](/sdks/#diagnostics). Client Pool and
built-in pool warmup tracing are not currently available in C#.
Install `Microsoft.Extensions.Logging.Console` to use `AddConsole()` below.

```csharp
using Microsoft.Extensions.Logging;
using OpenSandbox.Config;

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder.SetMinimumLevel(LogLevel.Debug);
    builder.AddConsole();
});

var sandbox = await Sandbox.CreateAsync(new SandboxCreateOptions
{
    Image = "python:3.11",
    ConnectionConfig = new ConnectionConfig(),
    Diagnostics = new SdkDiagnosticsOptions
    {
        LoggerFactory = loggerFactory
    }
});
```

### 3. Sandbox Creation Configuration

`Sandbox.CreateAsync()` allows configuring the sandbox environment.

| Parameter | Description | Default |
| --- | --- | --- |
| `Image` | Docker image to use | One of image or snapshot ID |
| `TimeoutSeconds` | Automatic termination timeout (server-side TTL) | 10 minutes |
| `Entrypoint` | Container entrypoint command | `["tail","-f","/dev/null"]` |
| `Resource` | CPU and memory limits (string map) | `{"cpu":"1","memory":"2Gi"}` |
| `Env` | Environment variables | `{}` |
| `Metadata` | Custom metadata tags | `{}` |
| `NetworkPolicy` | Optional outbound network policy (egress) | - |
| `CredentialProxy` | Optional Credential Vault proxy startup settings | - |
| `Volumes` | Optional storage mounts (`Host` / `PVC` / `OSSFS`, supports `ReadOnly` and `SubPath`) | - |
| `Extensions` | Extra server-defined fields | `{}` |
| `SkipHealthCheck` | Skip readiness checks (`Running` + health check) | `false` |
| `HealthCheck` | Custom readiness check | - |
| `ReadyTimeoutSeconds` | Max time to wait for readiness | 30 seconds |
| `HealthCheckPollingInterval` | Poll interval while waiting (milliseconds) | 200 ms |
| `SnapshotId` | Restore a snapshot instead of passing `Image`; omit `Entrypoint` | - |
| `ResourceRequests` | Kubernetes resource requests; must not exceed limits | - |
| `Lifecycle` | Pre-start and periodic hooks | - |
| `Platform` | OS/architecture constraint | - |
| `SecureAccess` | Require endpoint access credentials | `false` |
| `ManualCleanup` | Disable TTL expiration for image/snapshot creation | `false` |

::: warning
Metadata keys under `opensandbox.io/` are reserved for system-managed labels and will be rejected by the server.
:::

```csharp
var sandbox = await Sandbox.CreateAsync(new SandboxCreateOptions
{
    ConnectionConfig = config,
    Image = "python:3.11",
    NetworkPolicy = new NetworkPolicy
    {
        DefaultAction = NetworkRuleAction.Deny,
        Egress = new List<NetworkRule>
        {
            new() { Action = NetworkRuleAction.Allow, Target = "pypi.org" }
        }
    },
    Volumes = new[]
    {
        new Volume
        {
            Name = "workspace",
            Host = new Host { Path = "/tmp/opensandbox-e2e/host-volume-test" },
            MountPath = "/workspace",
            ReadOnly = false
        }
    }
});
```

### 4. Runtime Egress Policy Updates

Runtime egress policy routing depends on the sandbox origin. For image-backed sandboxes, the SDK resolves port `18080` and calls the sidecar `/policy` API. For template-backed sandboxes (including restored template snapshots), the SDK detects `OPEN-SANDBOX-ORIGIN: template` and routes policy operations through the lifecycle `/sandboxes/{sandboxId}/networkpolicy` API.

Patch uses merge semantics:
- Incoming rules take priority over existing rules with the same `Target`.
- Existing rules for other targets remain unchanged.
- Within a single patch payload, the first rule for a `Target` wins.
- The current `DefaultAction` is preserved.

```csharp
var policy = await sandbox.GetEgressPolicyAsync();

await sandbox.PatchEgressRulesAsync(new[]
{
    new NetworkRule { Action = NetworkRuleAction.Allow, Target = "www.github.com" },
    new NetworkRule { Action = NetworkRuleAction.Deny, Target = "pypi.org" }
});
```

### 5. Credential Vault

Credential Vault requires a sandbox-side egress service and is unavailable for template-backed sandboxes. It injects outbound credentials from the egress sidecar while keeping real secrets out of sandbox environment variables, commands, files, and logs. Create the sandbox with `CredentialProxy` enabled, then write credentials and bindings through `sandbox.CredentialVault` or the sandbox helper methods.

```csharp
var sandbox = await Sandbox.CreateAsync(new SandboxCreateOptions
{
    ConnectionConfig = config,
    Image = "python:3.11",
    NetworkPolicy = new NetworkPolicy
    {
        DefaultAction = NetworkRuleAction.Deny,
        Egress = new List<NetworkRule>
        {
            new() { Action = NetworkRuleAction.Allow, Target = "api.example.com" }
        }
    },
    CredentialProxy = new CredentialProxyConfig { Enabled = true }
});

await sandbox.CreateCredentialVaultAsync(
    new[]
    {
        new Credential
        {
            Name = "api-token",
            Source = new InlineCredentialSource { Value = "<token>" }
        }
    },
    new[]
    {
        new CredentialBinding
        {
            Name = "api-token",
            Match = new CredentialMatch
            {
                Schemes = new[] { "https" },
                Hosts = new[] { "api.example.com" },
                Paths = new[] { "/v1/*" }
            },
            Auth = new CredentialAuth
            {
                Type = "apiKey",
                Name = "x-api-key",
                Credential = "api-token"
            }
        }
    });
```

See [Credential Vault](/guides/credential-vault) for auth types, binding guidance, and Git/curl examples.

### 6. Timeout and Retry Behavior

- `ConnectionConfig.RequestTimeoutSeconds` controls timeout for SDK HTTP calls. File uploads and downloads are exempt while they keep making progress; the timeout still bounds the connect phase and how long a transfer waits for response headers before failing.
- `RunCommandOptions.TimeoutSeconds` controls command execution timeout for command runs.
- `RunInSessionOptions.TimeoutSeconds` controls command execution timeout for session runs.
- `SandboxCreateOptions.TimeoutSeconds` controls sandbox server-side TTL.
- `ReadyTimeoutSeconds` controls readiness waits. For `ConnectAsync` / `ResumeAsync`, endpoint discovery and health checks share this timeout. Blocking custom code can delay timeout reporting.
- The SDK does not automatically retry failed API requests; implement retries in caller code where appropriate.

### 7. Resource Cleanup

Both `Sandbox` and `SandboxManager` implement `IAsyncDisposable`. Use `await using` or call `DisposeAsync()` when done.

```csharp
await using var sandbox = await Sandbox.CreateAsync(options);
// ... use sandbox ...
// Automatically disposed when leaving scope
```

## Error Handling

The SDK throws `SandboxException` (and derived exceptions such as `SandboxApiException`, `SandboxReadyTimeoutException`, and `InvalidArgumentException`) when operations fail.

```csharp
try
{
    var execution = await sandbox.Commands.RunAsync("echo 'Hello Sandbox!'");
    Console.WriteLine(execution.Logs.Stdout.FirstOrDefault()?.Text);
}
catch (SandboxReadyTimeoutException)
{
    Console.Error.WriteLine("Sandbox did not become ready before the configured timeout.");
}
catch (SandboxApiException ex)
{
    Console.Error.WriteLine($"API Error: status={ex.StatusCode}, requestId={ex.RequestId}, message={ex.Message}");
}
catch (SandboxException ex)
{
    Console.Error.WriteLine($"Sandbox Error: [{ex.Error.Code}] {ex.Error.Message}");
}
```

## Supported Frameworks

- .NET Standard 2.0 (for maximum compatibility with .NET Framework 4.6.1+, .NET Core 2.0+, Mono, Xamarin, etc.)
- .NET Standard 2.1
- .NET 6.0
- .NET 7.0
- .NET 8.0
- .NET 9.0
- .NET 10.0

## License

Apache License 2.0

---

### Sdks Go (`docs/sdks/go.md`)

---
title: Go SDK
description: Go client library for the OpenSandbox API covering lifecycle, execd, and egress operations.
---

# OpenSandbox Go SDK

Go client library for the [OpenSandbox](https://github.com/opensandbox-group/OpenSandbox/) API.

Provides high-level sandbox helpers and low-level clients for these API areas:
- **Lifecycle** -- Create, manage, and destroy sandbox instances
- **Execd** -- Execute commands, manage files, monitor metrics inside sandboxes
- **Egress** -- Inspect and mutate sandbox network policy at runtime

## Installation

```bash
# go 1.20+
go get github.com/alibaba/OpenSandbox/sdks/sandbox/go
```

## Quick Start

### Create and manage a sandbox

Use the high-level helper to create a sandbox, resolve its endpoint, and wait for readiness before running commands:

```go
package main

import (
    "context"
    "fmt"
    "log"

    opensandbox "github.com/alibaba/OpenSandbox/sdks/sandbox/go"
)

func run() error {
    ctx := context.Background()
    ttl := 600
    config := opensandbox.ConnectionConfig{
        Domain: "localhost:8080",
        Protocol: "http",
        UseServerProxy: true,
    }
    sandbox, err := opensandbox.CreateSandbox(ctx, config, opensandbox.SandboxCreateOptions{
        Image: "python:3.12",
        TimeoutSeconds: &ttl,
    })
    if err != nil {
        return err
    }
    defer sandbox.Close()
    defer sandbox.Kill(context.Background())

    result, err := sandbox.RunCommand(ctx, "echo sandbox-ready", nil)
    if err != nil {
        return err
    }
    fmt.Println(result.Text())
    return nil
}

func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}
```

`ConnectSandbox` and `ResumeSandbox` check health only when you pass the optional `ReadyOptions` argument (an empty `ReadyOptions{}` uses the default check). Omitting it resolves the endpoint without a health check. Pause is asynchronous: wait until sandbox info reports `Paused` before resuming.

The following snippets go inside a function returning `error`, with the live `sandbox`, `config`, and `ctx` above. Add standard-library imports used by each snippet. Creation examples are alternatives; terminate sandboxes with `Kill` when done. `Close` currently does not terminate the sandbox.

### Run a command with streaming output

Use structured callbacks for normal command output:

```go
result, err := sandbox.RunCommand(ctx, "echo 'Hello from sandbox!'", &opensandbox.ExecutionHandlers{
    OnStdout: func(message opensandbox.OutputMessage) error {
        fmt.Println(message.Text)
        return nil
    },
})
if err != nil {
    return err
}
if result.ExitCode == nil || *result.ExitCode != 0 {
    return fmt.Errorf("command failed: %v", result.Error)
}
```

Use the low-level client when you need raw SSE events or command status/log APIs. `event.Data` contains JSON; import `encoding/json` and `os` for this example:

```go
endpoint, err := sandbox.GetEndpoint(ctx, 44772)
if err != nil {
    return err
}
exec := opensandbox.NewExecdClient(
    config.Protocol + "://" + endpoint.Endpoint, "",
    opensandbox.WithHeaders(endpoint.Headers),
)

err = exec.RunCommand(ctx, opensandbox.RunCommandRequest{
    Command: "echo 'Hello from sandbox!'",
    Timeout: 30000,
}, func(event opensandbox.StreamEvent) error {
    var message struct{ Type, Text string }
    if err := json.Unmarshal([]byte(event.Data), &message); err != nil {
        return err
    }
    switch message.Type {
    case "stdout":
        fmt.Println(message.Text)
    case "stderr":
        fmt.Fprintln(os.Stderr, message.Text)
    case "execution_complete":
        fmt.Println("[done]")
    }
    return nil
})
if err != nil {
    return err
}
```

`RunCommandRequest.Timeout` is in **milliseconds**. Use this low-level client for `InterruptCommand`, `GetCommandStatus`, and `GetCommandLogs`, which are not exposed as high-level `Sandbox` methods.

For native execution, replace the request above with the following. On Linux, it prints literal `$HOME` and keeps `hello world` as one argument:

```go
_, err = sandbox.RunCommandWithOpts(ctx, opensandbox.RunCommandRequest{
    Argv:    []string{"printf", "%s\n", "$HOME", "hello world"},
    Timeout: 30000,
}, nil)
if err != nil {
    return err
}
```

Native argv execution requires an updated execd. See [command execution modes](/architecture/data-plane/execd#command-execution) for executable lookup and platform behavior.

### Background commands

Reuse the `exec` client from the streaming example to poll status and incremental logs. Command timeout is in milliseconds and is separate from sandbox TTL.

```go
execution, err := sandbox.RunCommandWithOpts(ctx, opensandbox.RunCommandRequest{
    Command: "for i in 1 2 3; do echo step-$i; sleep 1; done",
    Background: true,
    Timeout: 30_000,
}, nil)
if err != nil {
    return err
}
if execution.ID == "" {
    return fmt.Errorf("no command ID returned")
}
cursor := int64(0)
deadline := time.Now().Add(45 * time.Second)
for {
    if time.Now().After(deadline) {
        if err := exec.InterruptCommand(ctx, execution.ID); err != nil {
            return err
        }
        return fmt.Errorf("command did not finish")
    }
    status, err := exec.GetCommandStatus(ctx, execution.ID)
    if err != nil {
        return err
    }
    logs, err := exec.GetCommandLogs(ctx, execution.ID, &cursor)
    if err != nil {
        return err
    }
    fmt.Print(logs.Output)
    cursor = logs.Cursor
    if !status.Running {
        if status.ExitCode == nil || *status.ExitCode != 0 {
            return fmt.Errorf("command failed: %s", status.Error)
        }
        break
    }
    select {
    case <-ctx.Done():
        return ctx.Err()
    case <-time.After(500 * time.Millisecond):
    }
}
```

### Persistent shell sessions

A Bash session preserves shell variables and the working directory across commands.

```go
session, err := sandbox.CreateSession(ctx)
if err != nil {
    return err
}
defer sandbox.DeleteSession(context.Background(), session.ID)
_, err = sandbox.RunInSession(ctx, session.ID, opensandbox.RunInSessionRequest{
    Command: "cd /tmp; export DEMO=hello",
    Timeout: 30_000,
}, nil)
if err != nil {
    return err
}
result, err := sandbox.RunInSession(ctx, session.ID, opensandbox.RunInSessionRequest{
    Command: "echo \"$DEMO\"; pwd",
    Timeout: 30_000,
}, nil)
if err != nil {
    return err
}
fmt.Println(result.Text())
```

### Persistent environment variables

Set environment variables that the runtime injects into every subsequent command and session — without hand-writing shell escaping against the sandbox env file.

```go
err := sandbox.SetEnv(ctx, "MY_TOKEN", "it's a safe value")
```

Keys must match `[A-Za-z_][A-Za-z0-9_]*`. Values without a single quote are stored verbatim; values containing a single quote use the env file's double-quoted form, in which shell-style `$NAME` sequences may be expanded when the runtime loads the file. The env file is append-only: the last write for a key wins. Returns an error if the sandbox fails to persist the variable.

For filesystem/process isolation within a sandbox, see [Isolation Sessions](/guides/isolation-sessions). These are separate from Bash sessions.

### File operations

Upload from an `io.Reader` and close download streams after use. This example uses `strings`, `io`, and `os` from the standard library:

```go
err := sandbox.UploadFile(ctx, strings.NewReader("Hello Sandbox!"), opensandbox.UploadFileOptions{
    Metadata: opensandbox.FileMetadata{Path: "/tmp/demo.txt", Mode: 644},
})
if err != nil {
    return err
}
body, err := sandbox.DownloadFile(ctx, "/tmp/demo.txt", "")
if err != nil {
    return err
}
_, copyErr := io.Copy(os.Stdout, body)
closeErr := body.Close()
if copyErr != nil {
    return copyErr
}
if closeErr != nil {
    return closeErr
}
entries, err := sandbox.ListDirectory(ctx, "/tmp")
if err != nil {
    return err
}
for _, entry := range entries {
    fmt.Println(entry.Path)
}
if err := sandbox.DeleteFiles(ctx, []string{"/tmp/demo.txt"}); err != nil {
    return err
}
```

The same upload/download methods support binary files. Pass a Range header such as `"bytes=0-1023"` to `DownloadFile` for a partial download.

#### File operations with an explicit identity

Bind a filesystem client to a Linux UID and GID:

```go
files, err := sandbox.FilesWithIdentity(1001, 2000)
if err != nil {
    return err
}
```

The bound client exposes the usual file operations and shares the sandbox connection, authentication, and endpoint routing headers. The existing file client retains its default identity. Select the UID/GID separately for command execution.

This requires Linux and an Execd version that supports filesystem execution identity. Unsupported servers return an error; requests never fall back to the default filesystem routes. Keep sandbox credentials and identity selection in the trusted backend. Linux filesystem permissions apply; this feature does not create a tenant isolation boundary. See [Execd](/architecture/data-plane/execd) for permission and workspace provisioning details.

### Pause and reconnect

Pause is asynchronous and runtime-dependent. Use a deadline before resuming:

```go
pauseCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
if err := sandbox.Pause(pauseCtx); err != nil {
    return err
}
for {
    info, err := sandbox.GetInfo(pauseCtx)
    if err != nil {
        return err
    }
    if info.Status.State == "Paused" {
        break
    }
    if info.Status.State == "Failed" {
        return fmt.Errorf("pause failed: %s", info.Status.Message)
    }
    select {
    case <-pauseCtx.Done():
        return pauseCtx.Err()
    case <-time.After(time.Second):
    }
}
resumed, err := opensandbox.ResumeSandbox(ctx, config, sandbox.ID(), opensandbox.ReadyOptions{})
if err != nil {
    return err
}
defer resumed.Close()
if _, err := resumed.Renew(ctx, 30*time.Minute); err != nil {
    return err
}
```

Use `ConnectSandbox(ctx, config, id, opensandbox.ReadyOptions{})` to attach to an already running sandbox with a readiness check. See [Pause and Resume](/guides/pause-resume).

### Resource metrics

Use `metrics, err := sandbox.GetMetrics(ctx)` to read current sandbox resource usage. This is separate from [SDK creation telemetry](/sdks/observability#creation-metrics).

### Check egress policy

Runtime egress reads and patches go directly to the sandbox egress sidecar. The SDK first resolves the sandbox endpoint on port `18080`, then calls the sidecar `/policy` API.

```go
policy, err := sandbox.GetEgressPolicy(ctx)
if err != nil {
    return err
}
fmt.Printf("Mode: %s, Default: %s\n", policy.Mode, policy.Policy.DefaultAction)

_, err = sandbox.PatchEgressRules(ctx, []opensandbox.NetworkRule{
    {Action: "allow", Target: "api.example.com"},
})
if err != nil {
    return err
}
```

Template-backed sandboxes have no sandbox-side egress sidecar: the SDK detects them via the server's `OPEN-SANDBOX-ORIGIN` response header (see [Fsb Template Management](#fsb-template-management)) and routes the same `GetEgressPolicy` / `PatchEgressRules` / `DeleteEgressRules` calls through the lifecycle control plane (`/sandboxes/{sandboxId}/networkpolicy`) instead.

Patch uses merge semantics:
- Incoming rules take priority over existing rules with the same `target`.
- Existing rules for other targets remain unchanged.

### Use Credential Vault

Credential Vault injects outbound credentials from the egress sidecar while keeping real secrets out of sandbox environment variables, commands, files, and logs. Create the sandbox with `CredentialProxy` enabled, then write credentials and bindings through the sandbox helpers or `EgressClient`.

```
/* Detailed source-code truncated for AI context efficiency. */
```

See [Credential Vault](/guides/credential-vault) for auth types, binding guidance, and Git/curl examples.

::: warning
Credential Vault is unavailable for template-backed sandboxes: they have no sandbox-side egress sidecar. `sandbox.CredentialVault(ctx)` returns an error for them.
:::

### Client Pool

Use `NewSandboxPoolBuilder` with an in-memory store or the Redis adapter in `github.com/alibaba/OpenSandbox/sdks/sandbox/go/poolredis`. Go supports all four acquire policies, namespace retirement, and per-acquire TTL/health-check overrides.

Go uses `ReconcileInterval` and `WarmupConcurrency`; it does not expose the
Python/JVM/JavaScript create-QPS, initial-delay, or post-prepare-check settings.
See [Client Pool](/guides/client-pool) for defaults, examples, and cleanup.
Go does not currently emit built-in pool warmup traces.
Both `SandboxManager` and `Sandbox` expose `GetDiagnosticContext`,
`GetDiagnosticLogs`, and `GetDiagnosticEvents`; see [SDK diagnostics](/sdks/#diagnostics).

## Lifecycle Hooks

Set `Lifecycle` in `SandboxCreateOptions`. `PreStart` completes before the entrypoint starts, while `Periodic` hooks run on their schedules after startup.

```go
hookTimeout := 120
sandbox, err := opensandbox.CreateSandbox(ctx, config, opensandbox.SandboxCreateOptions{
    Image: "ubuntu:24.04",
    Lifecycle: &opensandbox.SandboxLifecycle{
        PreStart: &opensandbox.LifecycleHook{
            Command:        []string{"sh", "-c", "echo ready > /tmp/prestart.done"},
            TimeoutSeconds: &hookTimeout,
        },
        Periodic: []opensandbox.PeriodicLifecycleHook{
            {
                Name:           "checkpoint",
                Schedule:       "@every 5m",
                Command:        []string{"sh", "-c", "date -u >> /tmp/checkpoints.log"},
                TimeoutSeconds: &hookTimeout,
            },
        },
    },
})
if err != nil {
    return err
}
defer sandbox.Close()
defer sandbox.Kill(context.Background())
fmt.Println(sandbox.ID())
```

The Server validates `TimeoutSeconds`; `PreStart` accepts 1–10800 seconds, while `Periodic` accepts 1–300 seconds. Both default to 60 seconds when omitted. See [Lifecycle Hooks](/guides/lifecycle-hooks) for timing, failure behavior, and provider limitations.

### Idle policy (auto-pause and wake-on-access)

`SandboxLifecycle.IdlePolicy` (OSEP-0024, fast-sandbox only) tells the control plane to auto-pause the sandbox after `IdleTimeoutSeconds` (30–86400) of continuously idle ingress-observed traffic, and to wake it on the next request. It is control-plane state: it never reaches the guest, is accepted in template mode (`SandboxFromTemplateOptions.IdlePolicy`), and is rejected by non-fast-sandbox backends. Omitting it preserves non-automated behavior exactly.

```go
idleTimeout := 300
wakeOnAccess := true
sandbox, err := opensandbox.CreateSandbox(ctx, config, opensandbox.SandboxCreateOptions{
    Image: "ubuntu:24.04",
    Lifecycle: &opensandbox.SandboxLifecycle{
        IdlePolicy: &opensandbox.IdlePolicy{
            IdleTimeoutSeconds: &idleTimeout,
            OnIdle:             opensandbox.OnIdlePause,
            WakeOnAccess:       &wakeOnAccess,
        },
    },
})
```

`ActiveProbe` lets the application veto a pause while real work runs without producing ingress traffic: declare an endpoint answering `{"active": bool, "message"?: string}` and the server polls it when the sandbox is otherwise idle; a busy answer resets the idle clock. Invalid policies fail fast client-side through `IdlePolicy.Validate()`.

### Renewal policy (renew-on-access)

`SandboxLifecycle.RenewalPolicy.ExtendSeconds` (OSEP-0009) opts the sandbox into automatic expiration extension on observed access: each proxied or gateway-observed request moves the expiration to `max(now + ExtendSeconds, current expiresAt)`. It is control-plane state and never reaches the guest. Requires the server operator to enable `[renew_intent]` — see [Renew-on-Access](/guides/renew-on-access). The deprecated `extensions["access.renew.extend.seconds"]` key remains supported through 1.x and is removed in 2.0. Invalid policies fail fast client-side through `RenewalPolicy.Validate()`.

```go
sandbox, err := opensandbox.CreateSandbox(ctx, config, opensandbox.SandboxCreateOptions{
    Image: "ubuntu:24.04",
    Lifecycle: &opensandbox.SandboxLifecycle{
        RenewalPolicy: &opensandbox.RenewalPolicy{ExtendSeconds: 3600},
    },
})
```

### Wake retry

A request to a paused sandbox may arrive while its restore is still in progress. The gateway answers `503` with the `OpenSandbox-Wake: expired` marker; the client recognizes it and resends the original request after the `Retry-After` delay (safe for any method — the marker proves the request was never forwarded). The wake budget defaults to 5 attempts / 60 seconds across all wake cycles; when it is exhausted the client returns a `*SandboxWakeTimeoutError` (carrying `Attempts` and `Waited`) while the server-side resume continues in the background. Bare `503` responses follow the existing retry rules untouched. Wake retry is active whenever a `WithRetry` config is set (`WakeRetryEnabled` defaults to enabled even for hand-built configs; pass `WakeRetryDisabled()` to opt out); streaming connections bypass wake retry — a waking sandbox fails fast with the marked 503 instead of parking the connect — matching the other official SDKs, and established streams are never re-executed.

## Fsb Template Management

fsb (fast-sandbox microVM) golden-image templates are managed through `SandboxManager`. Template builds are asynchronous: `CreateTemplate` returns with `Status.Phase` set to `Pending`; poll `GetTemplate` until the phase reaches `Succeeded` or `Failed`. Only a `Succeeded` template can create sandboxes. Template management requires a configured Fsb provider on Kubernetes. Replace the publish URI with a location configured for your server.

```
/* Detailed source-code truncated for AI context efficiency. */
```

### Creating a Sandbox from a Template

Use `CreateSandboxFromTemplate` to create a sandbox from a `Succeeded` template. Template mode fixes the workload shape on the server: only `Metadata`, `NetworkPolicy` and `Extensions` may accompany the template ID, and `TimeoutSeconds` is required.

```go
sandbox, err := opensandbox.CreateSandboxFromTemplate(ctx, config, "tpl-abc",
    opensandbox.SandboxFromTemplateOptions{
        TimeoutSeconds: 600,
        NetworkPolicy: &opensandbox.NetworkPolicy{
            DefaultAction: "deny",
            Egress: []opensandbox.NetworkRule{
                {Action: "allow", Target: "api.example.com"},
            },
        },
    })
if err != nil {
    return err
}

fmt.Println(sandbox.Origin()) // "template"
```

## Snapshots and metadata

`Sandbox.CreateSnapshot` and `SandboxManager.CreateSnapshot` start snapshot creation. Use manager `GetSnapshot`, `ListSnapshots`, and `DeleteSnapshot` to manage snapshots, then pass `SnapshotID` to `SandboxCreateOptions` to restore. Poll snapshot status before restore. Use `Sandbox.PatchMetadata` or `SandboxManager.PatchSandboxMetadata` to add/replace keys; nil values remove keys.

Snapshot support depends on the runtime and server configuration. Renew the source sandbox first if its remaining TTL may expire during snapshot creation. Wait for `Ready` before restoring. This example retains the snapshot for reuse:

```go
manager := opensandbox.NewSandboxManager(config)
snapshot, err := sandbox.CreateSnapshot(ctx, opensandbox.CreateSnapshotRequest{Name: "demo"})
if err != nil {
    return err
}
fmt.Println("Snapshot:", snapshot.ID)
snapshotCtx, cancel := context.WithTimeout(ctx, 15*time.Minute)
defer cancel()
for {
    snapshot, err = manager.GetSnapshot(snapshotCtx, snapshot.ID)
    if err != nil {
        return err
    }
    if snapshot.Status.State == opensandbox.SnapshotStateReady {
        break
    }
    if snapshot.Status.State == opensandbox.SnapshotStateFailed {
        return fmt.Errorf("snapshot failed: %s", snapshot.Status.Message)
    }
    select {
    case <-snapshotCtx.Done():
        return snapshotCtx.Err()
    case <-time.After(2 * time.Second):
    }
}
restored, err := opensandbox.CreateSandbox(ctx, config, opensandbox.SandboxCreateOptions{
    SnapshotID: snapshot.ID,
})
if err != nil {
    return err
}
defer restored.Close()
defer restored.Kill(context.Background())
fmt.Println(restored.ID())
// When no longer needed: manager.DeleteSnapshot(ctx, snapshot.ID)
```

Add/replace a metadata key and remove another:

```go
project := "demo"
if _, err := sandbox.PatchMetadata(ctx, opensandbox.MetadataPatch{
    "project": &project, "obsolete-key": nil,
}); err != nil {
    return err
}
```

## API Reference

### LifecycleClient

Created with `NewLifecycleClient(baseURL, apiKey string, opts ...Option)`.

| Method | Description |
|--------|-------------|
| `CreateSandbox(ctx, req)` | Create a new sandbox from a container image |
| `GetSandbox(ctx, id)` | Get sandbox details by ID |
| `ListSandboxes(ctx, opts)` | List sandboxes with filtering and pagination |
| `DeleteSandbox(ctx, id)` | Delete a sandbox |
| `PauseSandbox(ctx, id)` | Pause a running sandbox |
| `ResumeSandbox(ctx, id)` | Resume a paused sandbox |
| `RenewExpiration(ctx, id, expiresAt)` | Set sandbox expiration to a future timestamp (may shorten or extend) |
| `GetEndpoint(ctx, sandboxID, port, useServerProxy)` | Get public endpoint for a sandbox port |
| `GetSignedEndpoint(ctx, sandboxID, port, expires)` | Get signed endpoint URL with a signed route token |
| `CreateTemplate(ctx, req)` | Declare a fsb template (async golden-image build) |
| `GetTemplate(ctx, templateID)` | Get a template with its latest build status |
| `ListTemplates(ctx, opts)` | List templates with metadata filtering and pagination |
| `DeleteTemplate(ctx, templateID)` | Delete a template |
| `GetNetworkPolicy(ctx, sandboxID)` | Get a sandbox's egress policy from the control plane |
| `PatchNetworkPolicy(ctx, sandboxID, rules)` | Merge egress rules into a sandbox's policy |
| `DeleteNetworkPolicyRules(ctx, sandboxID, targets)` | Remove a sandbox's egress rules by target |

### ExecdClient

Created with `NewExecdClient(baseURL, accessToken string, opts ...Option)`.

**Health:**

| Method | Description |
|--------|-------------|
| `Ping(ctx)` | Check server health |

**Code Execution:**

| Method | Description |
|--------|-------------|
| `ListContexts(ctx, language)` | List active code execution contexts |
| `CreateContext(ctx, req)` | Create a code execution context |
| `GetContext(ctx, contextID)` | Get context details |
| `DeleteContext(ctx, contextID)` | Delete a context |
| `DeleteContextsByLanguage(ctx, language)` | Delete all contexts for a language |
| `ExecuteCode(ctx, req, handler)` | Execute code with SSE streaming |
| `InterruptCode(ctx, sessionID)` | Interrupt running code |

**Command Execution:**

| Method | Description |
|--------|-------------|
| `CreateSession(ctx)` | Create a bash session |
| `RunInSession(ctx, sessionID, req, handler)` | Run command in session with SSE |
| `DeleteSession(ctx, sessionID)` | Delete a bash session |
| `RunCommand(ctx, req, handler)` | Run a command with SSE streaming |
| `InterruptCommand(ctx, sessionID)` | Interrupt running command |
| `GetCommandStatus(ctx, commandID)` | Get command execution status |
| `GetCommandLogs(ctx, commandID, cursor)` | Get command stdout/stderr |

**File Operations:**

| Method | Description |
|--------|-------------|
| `GetFileInfo(ctx, path)` | Get file metadata |
| `DeleteFiles(ctx, paths)` | Delete files |
| `SetPermissions(ctx, req)` | Change file permissions |
| `MoveFiles(ctx, req)` | Move/rename files |
| `SearchFiles(ctx, dir, pattern)` | Search files by glob pattern |
| `ListDirectory(ctx, path)` | List immediate directory contents (server-side default depth) |
| `ListDirectoryWithDepth(ctx, path, depth)` | List directory contents up to the given depth (`0` returns empty) |
| `ReplaceInFiles(ctx, req)` | Text replacement in files |
| `UploadFile(ctx, file, opts)` | Upload a file to the sandbox |
| `UploadFiles(ctx, entries)` | Upload multiple files to the sandbox |
| `DownloadFile(ctx, remotePath, rangeHeader)` | Download a file from the sandbox |

**Directory Operations:**

| Method | Description |
|--------|-------------|
| `CreateDirectory(ctx, path, mode)` | Create a directory (mkdir -p) |
| `DeleteDirectory(ctx, path)` | Delete a directory recursively |

**Metrics:**

| Method | Description |
|--------|-------------|
| `GetMetrics(ctx)` | Get system resource metrics |
| `WatchMetrics(ctx, handler)` | Stream metrics via SSE |

### EgressClient

Created with `NewEgressClient(baseURL, authToken string, opts ...Option)`.

| Method | Description |
|--------|-------------|
| `GetPolicy(ctx)` | Get current egress policy |
| `PatchPolicy(ctx, rules)` | Merge rules into current policy |
| `CreateCredentialVault(ctx, req)` | Create sandbox-local Credential Vault state |
| `GetCredentialVault(ctx)` | Get sanitized Credential Vault state |
| `PatchCredentialVault(ctx, req)` | Atomically mutate credentials and bindings |
| `DeleteCredentialVault(ctx)` | Delete sandbox-local Credential Vault state |
| `ListCredentialVaultCredentials(ctx)` | List sanitized credential metadata |
| `GetCredentialVaultCredential(ctx, name)` | Get sanitized metadata for one credential |
| `ListCredentialVaultBindings(ctx)` | List sanitized binding metadata |
| `GetCredentialVaultBinding(ctx, name)` | Get sanitized metadata for one binding |

## SSE Streaming

Methods that stream output (`RunCommand`, `ExecuteCode`, `RunInSession`, `WatchMetrics`) accept an `EventHandler` callback:

```go
type EventHandler func(event opensandbox.StreamEvent) error
```

Each `StreamEvent` contains:
- `Event` -- the event type (e.g. `"stdout"`, `"stderr"`, `"result"`, `"execution_complete"`). For NDJSON streams, this is extracted from the JSON `type` field automatically.
- `Data` -- the raw event payload (JSON string for NDJSON streams).
- `ID` -- optional event identifier

Return a non-nil error from the handler to stop processing the stream early.

## Client Options

All client constructors accept optional `Option` functions. Custom HTTP clients and health checks must honor context cancellation for timeouts to take effect:

```go
// Reuse the resolved endpoint from the streaming example.
execClient := opensandbox.NewExecdClient(
    config.Protocol + "://" + endpoint.Endpoint, "",
    opensandbox.WithHeaders(endpoint.Headers),
    opensandbox.WithHTTPClient(&http.Client{Timeout: 60 * time.Second}),
)
if err := execClient.Ping(ctx); err != nil {
    return err
}
```

::: info TLS Certificate Strength
SDK-created HTTP clients enforce NIST 2030 minimum TLS certificate strength by default (RSA >= 2048, EC >= 224, DSA P >= 2048/Q >= 224, hash >= 224). If you must interoperate with legacy endpoints, set `AllowWeakServerCertKeyLengths: true` in `TransportConfig`.
:::

::: tip SDK Telemetry
`CreateSandbox` reports create latency to `POST /v1/metrics/events` by default. Set `ConnectionConfig.DisableMetrics` or `OPENSANDBOX_DISABLE_METRICS=1` to opt out. See [SDK Telemetry](/sdks/observability#creation-metrics).
:::

### Follow redirects

The Go SDK retains Go's standard redirect behavior, including for file uploads and downloads. On redirects to a different origin (scheme, hostname, or effective port), it removes the client's default and configured authentication headers (including `X-EXECD-ACCESS-TOKEN`, `X-API-Key`, or a custom `WithAuthHeader` name), as well as all headers whose names start with `OPEN-SANDBOX-` or `OPENSANDBOX-`, case-insensitively. Each redirect destination is compared with the initial request's origin, so subsequent hops on another origin remain protected. Same-origin redirects retain these headers. Unrelated custom headers continue to follow Go's standard forwarding rules.

This protection applies to lifecycle (including creation metrics), execd, egress, file transfers, and SSE requests. A custom `http.Client.CheckRedirect` supplied through `WithHTTPClient` or `ConnectionConfig.HTTPClient` still controls whether redirects are followed; the SDK removes protected headers after that callback runs, even if the callback adds them again. The SDK copies the supplied HTTP client rather than modifying it. HTTP and HTTPS default ports are treated as equivalent to explicit ports 80 and 443 respectively. Upload method conversion and body replay follow Go's existing rules; this protection does not enable replay of streaming multipart bodies.

## Error Handling

Non-2xx responses are returned as `*opensandbox.APIError`:

```go
_, err := sandbox.GetInfo(ctx)
if err != nil {
    var apiErr *opensandbox.APIError
    if errors.As(err, &apiErr) {
        fmt.Printf("HTTP %d: %s — %s\n", apiErr.StatusCode, apiErr.Response.Code, apiErr.Response.Message)
    }
    return err
}
```

## License

Apache 2.0

---

### Sdks Javascript (`docs/sdks/javascript.md`)

---
title: JavaScript/TypeScript SDK
description: TypeScript/JavaScript SDK for creating, managing, and interacting with secure OpenSandbox environments.
---

# OpenSandbox SDK for JavaScript/TypeScript

A TypeScript/JavaScript SDK for low-level interaction with OpenSandbox. It provides the ability to create, manage, and interact with secure sandbox environments, including executing shell commands, managing files, and reading resource metrics.

## Installation

### npm

```bash
npm install @alibaba-group/opensandbox
```

### pnpm

```bash
pnpm add @alibaba-group/opensandbox
```

### yarn

```bash
yarn add @alibaba-group/opensandbox
```

## Quick Start

This example uses Node.js 20+ with ES modules and top-level `await`. It creates a sandbox, runs a shell command, and releases both remote and local resources.

::: tip
Before running this example, ensure the OpenSandbox service is running. See the [Getting Started](/getting-started/) guide for startup instructions.
:::

```ts
import { ConnectionConfig, Sandbox, SandboxException } from "@alibaba-group/opensandbox";

const config = new ConnectionConfig({
  domain: "api.opensandbox.io",
  apiKey: "your-api-key",
  // protocol: "https",
  // requestTimeoutSeconds: 60,
});

try {
  const sandbox = await Sandbox.create({
    connectionConfig: config,
    image: "ubuntu",
    timeoutSeconds: 10 * 60,
  });

  try {
    const execution = await sandbox.commands.run("echo 'Hello Sandbox!'");
    console.log(execution.logs.stdout[0]?.text);
  } finally {
    try {
      await sandbox.kill();
    } finally {
      await sandbox.close();
    }
  }
} catch (err) {
  if (err instanceof SandboxException) {
    console.error(
      `Sandbox Error: [${err.error.code}] ${err.error.message ?? ""}`,
    );
    console.error(`Request ID: ${err.requestId ?? "N/A"}`);
  } else {
    console.error(err);
  }
}
```

## Lifecycle Hooks

Set `lifecycle` in `Sandbox.create`. `preStart` completes before the entrypoint starts, while `periodic` hooks run on their schedules after startup.

```ts
const sandbox = await Sandbox.create({
  connectionConfig: config,
  image: "ubuntu:24.04",
  lifecycle: {
    preStart: {
      command: ["sh", "-c", "echo ready > /tmp/prestart.done"],
      timeoutSeconds: 120,
    },
    periodic: [
      {
        name: "checkpoint",
        schedule: "@every 5m",
        command: ["sh", "-c", "date -u >> /tmp/checkpoints.log"],
        timeoutSeconds: 120,
      },
    ],
  },
});
```

The Server validates `timeoutSeconds`; `preStart` accepts 1–10800 seconds, while `periodic` accepts 1–300 seconds. Both default to 60 seconds when omitted. See [Lifecycle Hooks](/guides/lifecycle-hooks) for timing, failure behavior, and provider limitations.

### Idle policy (auto-pause and wake-on-access)

`lifecycle.idlePolicy` (OSEP-0024, fast-sandbox only) tells the control plane to auto-pause the sandbox after `idleTimeoutSeconds` (30–86400) of continuously idle ingress-observed traffic, and to wake it on the next request. It is control-plane state: it never reaches the guest, is allowed in template mode (`Sandbox.createFromTemplate({ idlePolicy })`), and is rejected by non-fast-sandbox backends. Omitting it preserves non-automated behavior exactly.

```ts
const sandbox = await Sandbox.create({
  connectionConfig: config,
  image: "ubuntu:24.04",
  timeoutSeconds: 86400,
  lifecycle: {
    idlePolicy: {
      idleTimeoutSeconds: 300,
      onIdle: "pause",
      wakeOnAccess: true,
    },
  },
});
```

`activeProbe` lets the application veto a pause while real work runs without producing ingress traffic: declare an endpoint answering `{"active": bool, "message"?: string}` and the server polls it when the sandbox is otherwise idle; a busy answer resets the idle clock.

### Renewal policy (renew-on-access)

`lifecycle.renewalPolicy.extendSeconds` (OSEP-0009) opts the sandbox into automatic expiration extension on observed access: each proxied or gateway-observed request moves the expiration to `max(now + extendSeconds, current expiresAt)`. It is control-plane state and never reaches the guest. Requires the server operator to enable `[renew_intent]` — see [Renew-on-Access](/guides/renew-on-access). The deprecated `extensions["access.renew.extend.seconds"]` key remains supported through 1.x and is removed in 2.0.

```ts
const sandbox = await Sandbox.create({
  connectionConfig: config,
  image: "ubuntu:24.04",
  timeoutSeconds: 3600,
  lifecycle: {
    renewalPolicy: { extendSeconds: 3600 },
  },
});
```

### Wake retry

A request to a paused sandbox may arrive while its restore is still in progress. The gateway answers `503` with the `OpenSandbox-Wake: expired` header; the transport recognizes it and resends the original request after the `Retry-After` delay (safe for any method — the marker proves the request was never forwarded). The wake budget defaults to 5 attempts / 60 seconds across all wake cycles; when it is exhausted the SDK throws `SandboxWakeTimeoutException` (carrying `attempts` and `waitedMs`) while the server-side resume continues in the background. Bare `503` responses follow the existing rules untouched. Tune it on the connection config: `wakeRetryEnabled` (default true), `wakeMaxAttempts` (5), `wakeOverallDeadlineSeconds` (60, `null` for unbounded). The SSE transport is wrapped too, so `commands.run()` wakes a paused sandbox like any other request (an established stream is never re-dispatched — only the marked 503 connect replays); the upload transport stays bare because upload bodies are streams and cannot be replayed.

## Client Pool and observability

`SandboxPool` provides in-memory and Redis-backed stores, four acquire policies, and staged warmup controls. The Redis store is exported from `@alibaba-group/opensandbox/pool-redis`. See [Client Pool](/guides/client-pool) for examples, configuration, and namespace retirement.

Set `enableTracing: true` in `ConnectionConfig` to enable [pool warmup tracing](/sdks/observability#pool-warmup-tracing).
JavaScript emits phase spans but fewer diagnostic attributes than Python/JVM.
Create-latency [telemetry](/sdks/observability#creation-metrics) has a separate opt-out setting.
Use `SandboxManager.getDiagnosticContext(sandboxId)` to identify the runtime
backend without connecting to execd. Both the manager and sandbox expose
`getDiagnosticLogs` / `getDiagnosticEvents`; see [SDK diagnostics](/sdks/#diagnostics).

## Usage Examples

The snippets below use `config` and a live `sandbox` from the quick start. Run them before its cleanup block. Examples use TypeScript with top-level `await` in Node.js; omit type annotations in JavaScript. Creation examples are alternatives. Terminate each sandbox with `kill()` and release its client with `close()` when done.

### 1. Lifecycle Management

Manage the sandbox lifecycle, including renewal, pausing, and resuming.

```ts
const info = await sandbox.getInfo();
console.log("State:", info.status.state);
console.log("Created:", info.createdAt);
console.log("Expires:", info.expiresAt); // null when manual cleanup mode is used

await sandbox.pause();

const deadline = Date.now() + 120_000;
while (true) {
  if (Date.now() >= deadline) throw new Error("Sandbox did not pause within 120 seconds");
  const current = await sandbox.getInfo();
  if (current.status.state === "Paused") break;
  if (current.status.state === "Failed") throw new Error(current.status.message);
  await new Promise((resolve) => setTimeout(resolve, 1000));
}

// Resume returns a fresh handle; close both handles when done.
const resumed = await sandbox.resume();
try {
  await resumed.renew(30 * 60); // expiresAt = now + timeoutSeconds
} finally {
  await resumed.close();
}
```

Pause is asynchronous and runtime-dependent. See [Pause and Resume](/guides/pause-resume). Attach to an already running sandbox with `Sandbox.connect({ sandboxId, connectionConfig: config })`.

Create a non-expiring sandbox by passing `timeoutSeconds: null`:

```ts
const manual = await Sandbox.create({
  connectionConfig: config,
  image: "ubuntu",
  timeoutSeconds: null,
});
```

### 2. Custom Health Check

Resolving an endpoint confirms that a route exists; it does not confirm that the application on that port is healthy. For service readiness, make a bounded request to the application's health endpoint and include the returned endpoint headers.

Define custom logic to determine whether the sandbox is ready/healthy. This overrides the default ping check. Checks must not block the event loop and may continue running after timeout.

```ts
const sandbox = await Sandbox.create({
  connectionConfig: config,
  image: "nginx:latest",
  entrypoint: ["nginx", "-g", "daemon off;"],
  healthCheck: async (sbx) => {
    const ep = await sbx.getEndpoint(80);
    const url = await sbx.getEndpointUrl(80);
    try {
      const response = await fetch(url, {
        headers: ep.headers,
        signal: AbortSignal.timeout(2000),
      });
      await response.body?.cancel();
      return response.status === 200;
    } catch {
      return false;
    }
  },
});
```

### 3. Command Execution & Streaming

Execute commands and handle output streams in real-time.

```ts
import type { ExecutionHandlers } from "@alibaba-group/opensandbox";

const handlers: ExecutionHandlers = {
  onStdout: (m) => console.log("STDOUT:", m.text),
  onStderr: (m) => console.error("STDERR:", m.text),
  onExecutionComplete: (c) =>
    console.log("Finished in", c.executionTimeMs, "ms"),
};

await sandbox.commands.run(
  'for i in 1 2 3; do echo "Count $i"; sleep 0.2; done',
  undefined,
  handlers,
);
```

To execute a native program without shell parsing, pass an argument list. On Linux, this example prints literal `$HOME` and keeps `hello world` as one argument:

```ts
await sandbox.commands.run(["printf", "%s\n", "$HOME", "hello world"]);
```

Native argv execution requires an updated execd. See [command execution modes](/architecture/data-plane/execd#command-execution) for executable lookup and platform behavior.

#### Background commands

Poll status and incremental logs. The command timeout is separate from sandbox TTL.

```ts
const execution = await sandbox.commands.run(
  'for i in 1 2 3; do echo "step $i"; sleep 1; done',
  { background: true, timeoutSeconds: 30 },
);
if (!execution.id) throw new Error("No command ID returned");
let cursor = 0;
const deadline = Date.now() + 45_000;
while (true) {
  if (Date.now() >= deadline) {
    await sandbox.commands.interrupt(execution.id);
    throw new Error("Command did not finish");
  }
  const status = await sandbox.commands.getCommandStatus(execution.id);
  const logs = await sandbox.commands.getBackgroundCommandLogs(execution.id, cursor);
  process.stdout.write(logs.content);
  cursor = logs.cursor ?? cursor;
  if (status.running === false) {
    if (status.exitCode !== 0) throw new Error(`Command failed: ${status.exitCode}, ${status.error}`);
    break;
  }
  await new Promise((resolve) => setTimeout(resolve, 500));
}
```

#### Persistent shell sessions

A Bash session preserves shell variables and the working directory across commands.

```ts
const sessionId = await sandbox.commands.createSession({ workingDirectory: "/tmp" });
try {
  await sandbox.commands.runInSession(sessionId, "export DEMO=hello");
  const result = await sandbox.commands.runInSession(sessionId, 'echo "$DEMO"; pwd');
  console.log(result.logs.stdout.map((message) => message.text).join(""));
} finally {
  await sandbox.commands.deleteSession(sessionId);
}
```

#### Persistent environment variables

Set environment variables that the runtime injects into every subsequent command and session — without hand-writing shell escaping against the sandbox env file.

```ts
await sandbox.commands.setEnv("MY_TOKEN", "it's a safe value");
```

Keys must match `[A-Za-z_][A-Za-z0-9_]*`. Values without a single quote are stored verbatim; values containing a single quote use the env file's double-quoted form, in which shell-style `$NAME` sequences may be expanded when the runtime loads the file. The env file is append-only: the last write for a key wins. Throws if the sandbox fails to persist the variable.

For filesystem/process isolation within a sandbox, see [Isolation Sessions](/guides/isolation-sessions). These are separate from Bash sessions.

### 4. File Operations

Manage files and directories, including read, write, list/search, and delete.

```ts
await sandbox.files.createDirectories([{ path: "/tmp/demo", mode: 755 }]);

await sandbox.files.writeFiles([
  { path: "/tmp/demo/hello.txt", data: "Hello World", mode: 644 },
]);

const content = await sandbox.files.readFile("/tmp/demo/hello.txt");
console.log("Content:", content);

const entries = await sandbox.files.listDirectory({ path: "/tmp/demo", depth: 1 });
console.log(entries.map((entry) => entry.path));

const files = await sandbox.files.search({
  path: "/tmp/demo",
  pattern: "*.txt",
});
console.log(files.map((f) => f.path));

await sandbox.files.deleteDirectories(["/tmp/demo"]);
```

For binary files, pass a `Uint8Array` to `writeFiles()` and read with `readBytes()` or `readBytesStream()`. Read options support `offset` and `limit` for partial downloads.

#### File operations with an explicit identity

Bind a filesystem client to a Linux UID and GID:

```ts
const files = sandbox.filesWithIdentity(1001, 2000);
```

The bound client exposes the usual file operations and shares the sandbox connection, authentication, and endpoint routing headers. The existing file client retains its default identity. Select the UID/GID separately for command execution.

This requires Linux and an Execd version that supports filesystem execution identity. Unsupported servers return an error; requests never fall back to the default filesystem routes. Keep sandbox credentials and identity selection in the trusted backend. Linux filesystem permissions apply; this feature does not create a tenant isolation boundary. See [Execd](/architecture/data-plane/execd) for permission and workspace provisioning details.

### 5. Endpoints

`getEndpoint()` returns an endpoint **without a scheme** (for example `"localhost:44772"`). Use `getEndpointUrl()` if you want a ready-to-use absolute URL (for example `"http://localhost:44772"`).

```ts
const endpoint = await sandbox.getEndpoint(44772);
const url = await sandbox.getEndpointUrl(44772);
console.log(url, Object.keys(endpoint.headers ?? {}));
```

When making an HTTP request to a sandbox service, forward `endpoint.headers`, including credentials required by secure access. The health-check example above shows a request to an application that is actually listening on the target port.

### 6. Volume Mounts

`volumes` supports `host`, `pvc`, and `ossfs` backends. Each volume must specify exactly one backend.

```ts
const sandbox = await Sandbox.create({
  connectionConfig: config,
  image: "ubuntu",
  volumes: [
    {
      name: "oss-data",
      ossfs: {
        bucket: "bucket-a",
        endpoint: "oss-cn-hangzhou.aliyuncs.com",
        accessKeyId: process.env.OSS_ACCESS_KEY_ID!,
        accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET!,
        version: "2.0",
      },
      mountPath: "/mnt/oss",
      subPath: "prefix",
    },
  ],
});
```

### 7. Sandbox Management (Admin)

Use `SandboxManager` for administrative tasks and finding existing sandboxes.

```ts
import { SandboxManager } from "@alibaba-group/opensandbox";

const manager = SandboxManager.create({ connectionConfig: config });
try {
  // First page only; increase page for subsequent pages.
  const list = await manager.listSandboxInfos({
    states: ["Running"], pageSize: 10, page: 1,
  });
  console.log(list.items.map((s) => s.id));
} finally {
  await manager.close();
}
```

### Resource metrics

Read current sandbox resource usage with `await sandbox.getMetrics()`. This is separate from [SDK creation telemetry](/sdks/observability#creation-metrics).

## Snapshots, templates, and metadata

| Operation | Public API |
| --- | --- |
| Snapshot a sandbox | `manager.createSnapshot(sandboxId, { name })` |
| Inspect/list/delete snapshots | `manager.getSnapshot`, `listSnapshots`, `deleteSnapshot` |
| Restore a snapshot | `Sandbox.create({ snapshotId, connectionConfig })` |
| Manage Fsb templates | `manager.createTemplate`, `getTemplate`, `listTemplates`, `deleteTemplate` |
| Create from a published template | `Sandbox.createFromTemplate({ templateId, timeoutSeconds, connectionConfig })` |
| Patch metadata | `sandbox.patchMetadata` or `manager.patchSandboxMetadata` |

Poll snapshot status before restoring. Template builds are asynchronous; wait for `status.phase === "Succeeded"` before use. Template-backed creation requires a TTL and inherits workload configuration from the published template. Metadata patch values add/replace keys; `null` deletes a key. See the [lifecycle contract](/api/#1-sandbox-lifecycle-yml) for backend constraints.

Snapshot support depends on the runtime and server configuration. Renew the source sandbox first if its remaining TTL may expire during snapshot creation. Wait for `Ready` before restoring; the snapshot remains available after this example:

```ts
import { SandboxManager } from "@alibaba-group/opensandbox";

const manager = SandboxManager.create({ connectionConfig: config });
try {
  let snapshot = await manager.createSnapshot(sandbox.id, { name: "demo" });
  console.log("Snapshot:", snapshot.id);
  const deadline = Date.now() + 900_000;
  while (true) {
    if (Date.now() >= deadline) throw new Error(`Snapshot ${snapshot.id} is not ready`);
    snapshot = await manager.getSnapshot(snapshot.id);
    if (snapshot.status.state === "Ready") break;
    if (snapshot.status.state === "Failed") throw new Error(snapshot.status.message);
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
  const restored = await Sandbox.create({ snapshotId: snapshot.id, connectionConfig: config });
  try {
    console.log(restored.id);
  } finally {
    try { await restored.kill(); } finally { await restored.close(); }
  }
  // When no longer needed: await manager.deleteSnapshot(snapshot.id);
} finally {
  await manager.close();
}
```

Use an existing template after its build reaches `Succeeded`:

```ts
const templated = await Sandbox.createFromTemplate({
  templateId: "your-published-template-id",
  timeoutSeconds: 600,
  connectionConfig: config,
});
```

Add/replace a metadata key and remove another:

```ts
await sandbox.patchMetadata({ project: "demo", "obsolete-key": null });
```

## Configuration

### 1. Connection Configuration

The `ConnectionConfig` class manages API server connection settings.

::: info Runtime Notes
- In browsers, the SDK uses the global `fetch` implementation.
- `requestTimeoutSeconds` bounds the wait for a response: it stops applying once the response headers arrive (streaming responses are not cut off). It never caps a healthy file upload body, which can legitimately take longer than the timeout; on Node.js a stalled upload, or an upload that gets no response headers within the timeout once the body has been sent, is aborted instead of hanging (the Go SDK's `ResponseHeaderTimeout` contract).
- In Node.js, every `Sandbox` and `SandboxManager` clones the base `ConnectionConfig` via `withTransportIfMissing()`, so each instance gets an isolated `undici` keep-alive pool. Call `sandbox.close()` or `manager.close()` when you are done so the SDK can release the associated agent.
:::

| Parameter               | Description                                                                                                  | Default          | Environment Variable   |
| ----------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------- | ---------------------- |
| `apiKey`                | API key for authentication                                                                                   | Optional         | `OPEN_SANDBOX_API_KEY` |
| `domain`                | Sandbox service domain (`host[:port]`)                                                                       | `localhost:8080` | `OPEN_SANDBOX_DOMAIN`  |
| `protocol`              | HTTP protocol (`http`/`https`)                                                                               | `http`           | -                      |
| `requestTimeoutSeconds` | Request timeout applied to SDK HTTP calls                                                                    | `30`             | -                      |
| `debug`                 | Enable basic HTTP debug logging                                                                              | `false`          | -                      |
| `headers`               | Extra headers applied to every request                                                                       | `{}`             | -                      |
| `useServerProxy`        | Use sandbox server as proxy for execd/endpoint requests (e.g. when client cannot reach the sandbox directly) | `false`          | -                      |
| `enableTracing` | Enable [pool warmup tracing](/sdks/observability#pool-warmup-tracing) | `false` | - |
| `disableMetrics`        | Disable SDK create-latency telemetry (see [SDK Telemetry](/sdks/observability#creation-metrics))                          | `false`          | `OPENSANDBOX_DISABLE_METRICS` |

```ts
import { ConnectionConfig } from "@alibaba-group/opensandbox";

// 1. Basic configuration
const config = new ConnectionConfig({
  domain: "api.opensandbox.io",
  apiKey: "your-key",
  requestTimeoutSeconds: 60,
});

// 2. Advanced: custom headers
const config2 = new ConnectionConfig({
  domain: "api.opensandbox.io",
  apiKey: "your-key",
  headers: { "X-Custom-Header": "value" },
});
```

### 2. Sandbox Creation Configuration

`Sandbox.create()` allows configuring the sandbox environment.

| Parameter                    | Description                                      | Default                      |
| ---------------------------- | ------------------------------------------------ | ---------------------------- |
| `image`                      | Docker image to use                              | One of image or snapshot ID |
| `timeoutSeconds`             | Automatic termination timeout (server-side TTL)  | 10 minutes                   |
| `entrypoint`                 | Container entrypoint command                     | `["tail","-f","/dev/null"]`  |
| `resource`                   | CPU and memory limits (string map)               | `{"cpu":"1","memory":"2Gi"}` |
| `env`                        | Environment variables                            | `{}`                         |
| `metadata`                   | Custom metadata tags                             | `{}`                         |
| `networkPolicy`              | Optional outbound network policy (egress)        | -                            |
| `credentialProxy`            | Optional Credential Vault proxy startup settings | -                            |
| `extensions`                 | Extra server-defined fields                      | `{}`                         |
| `skipHealthCheck`            | Skip readiness checks (`Running` + health check) | `false`                      |
| `healthCheck`                | Custom readiness check                           | -                            |
| `readyTimeoutSeconds`        | Max time to wait for readiness                   | 30 seconds                   |
| `healthCheckPollingInterval` | Poll interval while waiting (milliseconds)       | 200 ms                       |
| `snapshotId` | Restore a snapshot instead of passing `image` | - |
| `resourceRequests` | Kubernetes resource requests; must not exceed limits | - |
| `lifecycle` | Pre-start and periodic hooks | - |
| `platform` | OS/architecture constraint | - |
| `volumes` | Host, PVC, or OSSFS mounts | - |
| `secureAccess` | Require endpoint access credentials | `false` |

::: warning
Metadata keys under `opensandbox.io/` are reserved for system-managed labels and will be rejected by the server.
:::

```ts
const sandbox = await Sandbox.create({
  connectionConfig: config,
  image: "python:3.11",
  networkPolicy: {
    defaultAction: "deny",
    egress: [{ action: "allow", target: "pypi.org" }],
  },
});
```

### 3. Runtime Egress Policy Updates

Runtime egress policy routing depends on the sandbox origin. For image-backed sandboxes, the SDK resolves port `18080` and calls the sidecar `/policy` API. For template-backed sandboxes (including restored template snapshots), the SDK detects `OPEN-SANDBOX-ORIGIN: template` and routes policy operations through the lifecycle `/sandboxes/{sandboxId}/networkpolicy` API.

Patch uses merge semantics:
- Incoming rules take priority over existing rules with the same `target`.
- Existing rules for other targets remain unchanged.
- Within a single patch payload, the first rule for a `target` wins.
- The current `defaultAction` is preserved.

```ts
const policy = await sandbox.getEgressPolicy();

await sandbox.patchEgressRules([
  { action: "allow", target: "www.github.com" },
  { action: "deny", target: "pypi.org" },
]);
```

### 4. Credential Vault

Credential Vault requires a sandbox-side egress service and is unavailable for template-backed sandboxes. It injects outbound credentials from the egress sidecar while keeping real secrets out of sandbox environment variables, commands, files, and logs. Create the sandbox with `credentialProxy` enabled, then write credentials and bindings through `sandbox.credentialVault`.

```ts
const sandbox = await Sandbox.create({
  connectionConfig: config,
  image: "python:3.11",
  networkPolicy: {
    defaultAction: "deny",
    egress: [{ action: "allow", target: "api.example.com" }],
  },
  credentialProxy: { enabled: true },
});

await sandbox.credentialVault.create({
  credentials: [{ name: "api-token", source: { value: "<token>" } }],
  bindings: [
    {
      name: "api-token",
      match: {
        schemes: ["https"],
        hosts: ["api.example.com"],
        paths: ["/v1/*"],
      },
      auth: { type: "apiKey", name: "x-api-key", credential: "api-token" },
    },
  ],
});
```

See [Credential Vault](/guides/credential-vault) for auth types, binding guidance, and Git/curl examples.

### 5. Resource Cleanup

Both `Sandbox` and `SandboxManager` own a scoped HTTP agent when running on Node.js so you can safely reuse the same `ConnectionConfig`. Once you are finished interacting with the sandbox or administration APIs, call `sandbox.close()` / `manager.close()` to release the underlying agent.

## Browser Notes

::: warning
- The SDK can run in browsers, but **streaming file uploads are Node-only**.
- If you pass `ReadableStream` or `AsyncIterable` for `writeFiles`, the browser will fall back to **buffering in memory** before upload.
- Reason: browsers do not support streaming `multipart/form-data` bodies with custom boundaries (required by the execd upload API).
:::

---

### Sdks Kotlin (`docs/sdks/kotlin.md`)

---
title: Kotlin/Java SDK
description: Kotlin SDK for creating, managing, and interacting with secure OpenSandbox environments.
---

# OpenSandbox SDK for Kotlin/Java

Create sandboxes, run commands, and manage files from Java or Kotlin. Both languages use the same JVM SDK and synchronous APIs.

## Installation

### Gradle (Kotlin DSL)

```kotlin
dependencies {
    implementation("com.alibaba.opensandbox:sandbox:{latest_version}")
}
```

### Maven

```xml
<dependency>
    <groupId>com.alibaba.opensandbox</groupId>
    <artifactId>sandbox</artifactId>
    <version>{latest_version}</version>
</dependency>
```

## Quick Start

The Java examples use Java 11+. The quick start creates a sandbox, runs a shell command, and releases both remote and local resources.

::: tip
Before running this example, ensure the OpenSandbox service is running. See the [Getting Started](/getting-started/) guide for startup instructions.
:::

```java
import com.alibaba.opensandbox.sandbox.Sandbox;
import com.alibaba.opensandbox.sandbox.config.ConnectionConfig;
import com.alibaba.opensandbox.sandbox.domain.exceptions.SandboxException;
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.Execution;

public class QuickStart {
    public static void main(String[] args) {
        // 1. Configure connection
        ConnectionConfig config = ConnectionConfig.builder()
            .domain("api.opensandbox.io")
            .apiKey("your-api-key")
            .build();

        // 2. Create a Sandbox using try-with-resources
        try (Sandbox sandbox = Sandbox.builder()
                .connectionConfig(config)
                .image("ubuntu")
                .build()) {

            try {
                Execution execution = sandbox.commands().run("echo 'Hello Sandbox!'");
                System.out.println(execution.getLogs().getStdout().get(0).getText());
            } finally {
                sandbox.kill();
            } // try-with-resources closes the client even if kill() fails.

        } catch (SandboxException e) {
            // Handle Sandbox specific exceptions
            System.err.println("Sandbox Error: [" + e.getError().getCode() + "] " + e.getError().getMessage());
            System.err.println("Request ID: " + e.getRequestId());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

### Kotlin

```kotlin
import com.alibaba.opensandbox.sandbox.Sandbox
import com.alibaba.opensandbox.sandbox.config.ConnectionConfig

fun main() {
    val config = ConnectionConfig.builder()
        .domain("api.opensandbox.io")
        .apiKey("your-api-key")
        .build()
    Sandbox.builder().image("ubuntu").connectionConfig(config).build().use { sandbox ->
        try {
            val result = sandbox.commands().run("echo 'Hello Sandbox!'")
            result.logs.stdout.forEach { print(it.text) }
        } finally {
            sandbox.kill()
        }
    }
}
```

`use` / try-with-resources releases the local client. `kill()` terminates the remote sandbox. The remaining examples use Java; Kotlin calls the same methods.

## Lifecycle Hooks

Configure lifecycle hooks on `Sandbox.Builder`. `preStart` completes before the entrypoint starts, while `periodic` hooks run on their schedules after startup.

```java
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.LifecycleHook;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.PeriodicLifecycleHook;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.SandboxLifecycle;

SandboxLifecycle lifecycle = SandboxLifecycle.builder()
    .preStart(LifecycleHook.builder()
        .command("sh", "-c", "echo ready > /tmp/prestart.done")
        .timeoutSeconds(120)
        .build())
    .periodic(PeriodicLifecycleHook.builder()
        .name("checkpoint")
        .schedule("@every 5m")
        .command("sh", "-c", "date -u >> /tmp/checkpoints.log")
        .timeoutSeconds(120)
        .build())
    .build();

Sandbox sandbox = Sandbox.builder()
    .connectionConfig(config)
    .image("ubuntu:24.04")
    .lifecycle(lifecycle)
    .build();
```

The Server validates `timeoutSeconds`; `preStart` accepts 1–10800 seconds, while `periodic` accepts 1–300 seconds. Both default to 60 seconds when omitted. See [Lifecycle Hooks](/guides/lifecycle-hooks) for timing, failure behavior, and provider limitations.

### Idle policy (auto-pause and wake-on-access)

`SandboxLifecycle.idlePolicy` (OSEP-0024, fast-sandbox only) tells the control plane to auto-pause the sandbox after `idleTimeoutSeconds` (30–86400) of continuously idle ingress-observed traffic, and to wake it on the next request. It is control-plane state: it never reaches the guest, is accepted in template mode (`TemplateLauncher.idlePolicy(...)`), and is rejected by non-fast-sandbox backends. Omitting it preserves non-automated behavior exactly.

```kotlin
val sandbox = Sandbox.builder()
    .connectionConfig(config)
    .image("ubuntu:24.04")
    .lifecycle(
        SandboxLifecycle.builder()
            .idlePolicy {
                idleTimeoutSeconds(300)
                onIdle(OnIdleAction.PAUSE)
                wakeOnAccess(true)
            }
            .build()
    )
    .build()
```

`activeProbe` lets the application veto a pause while real work runs without producing ingress traffic: declare an endpoint answering `{"active": bool, "message"?: string}` and the server polls it when the sandbox is otherwise idle; a busy answer resets the idle clock. Invalid policies fail fast in the `IdlePolicy.Builder`.

### Renewal policy (renew-on-access)

`SandboxLifecycle.renewalPolicy.extendSeconds` (OSEP-0009) opts the sandbox into automatic expiration extension on observed access: each proxied or gateway-observed request moves the expiration to `max(now + extendSeconds, current expiresAt)`. It is control-plane state and never reaches the guest. Requires the server operator to enable `[renew_intent]` — see [Renew-on-Access](/guides/renew-on-access). The deprecated `extensions["access.renew.extend.seconds"]` key remains supported through 1.x and is removed in 2.0. Invalid policies fail fast in the `RenewalPolicy.Builder`.

```kotlin
val sandbox = Sandbox.builder()
    .connectionConfig(config)
    .image("ubuntu:24.04")
    .lifecycle(
        SandboxLifecycle.builder()
            .renewalPolicy { extendSeconds(3600) }
            .build()
    )
    .build()
```

### Wake retry

A request to a paused sandbox may arrive while its restore is still in progress. The gateway answers `503` with the `OpenSandbox-Wake: expired` marker; the retry interceptor recognizes it and resends the original request after the `Retry-After` delay (safe for any method — the marker proves the request was never forwarded). The wake budget defaults to 5 attempts / 60 seconds across all wake cycles; when it is exhausted the SDK throws `SandboxWakeTimeoutException` (carrying `attempts` and `waited`) while the server-side resume continues in the background. Bare `503` responses follow the existing retry rules untouched. Tune it on `RetryPolicy`: `wakeRetryEnabled` (default true), `wakeMaxAttempts` (5), `wakeOverallDeadline` (60s, `null` for unbounded).

## Usage Examples

Use a live `sandbox` and `config` from the quick start, before cleanup. Java snippets belong inside a method that handles or declares checked exceptions; place imports at the top of the file. Common imports are:

```java
import java.time.Duration;
import java.util.*;
import com.alibaba.opensandbox.sandbox.SandboxManager;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.*;
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.*;
import com.alibaba.opensandbox.sandbox.domain.models.execd.filesystem.*;
```

Creation examples are alternatives. Close each client and kill sandboxes that are no longer needed.

### 1. Lifecycle Management

Manage the sandbox lifecycle, including renewal, pausing, and resuming.

```java
// Renew the sandbox
// This resets the expiration time to (current time + duration)
sandbox.renew(Duration.ofMinutes(30));

// Request pause (runtime-dependent)
sandbox.pause();

long deadline = System.nanoTime() + Duration.ofMinutes(2).toNanos();
while (true) {
    if (System.nanoTime() >= deadline) throw new IllegalStateException("Pause timed out");
    SandboxInfo info = sandbox.getInfo();
    if (SandboxState.PAUSED.equals(info.getStatus().getState())) break;
    if (SandboxState.FAILED.equals(info.getStatus().getState())) {
        throw new IllegalStateException(info.getStatus().getMessage());
    }
    Thread.sleep(1000);
}

// Resume returns a new local handle.
try (Sandbox resumed = Sandbox.resumer()
    .sandboxId(sandbox.getId())
    .connectionConfig(config)
    .resume()) {
    System.out.println("State: " + resumed.getInfo().getStatus().getState());
}
```

Pause is asynchronous and runtime-dependent. See [Pause and Resume](/guides/pause-resume). To attach to a running sandbox, use `Sandbox.connector().sandboxId(id).connectionConfig(config).connect()`.

Create a non-expiring sandbox by passing `timeout(null)`:

```java
Sandbox manual = Sandbox.builder()
    .connectionConfig(config)
    .image("ubuntu")
    .timeout(null)
    .build();
```

### 2. Custom Health Check

Resolving an endpoint confirms that a route exists; it does not confirm that the application on that port is healthy. For service readiness, make a bounded request to the application's health endpoint and include the returned endpoint headers.

Define custom logic to determine if the sandbox is healthy. This overrides the default ping check. Set timeouts within custom checks; the SDK cannot interrupt them.

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

// This HTTP probe uses Java 11+.
HttpClient http = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build();
Sandbox sandbox = Sandbox.builder()
    .connectionConfig(config)
    .image("nginx:latest")
    .entrypoint(List.of("nginx", "-g", "daemon off;"))
    .healthCheck(sbx -> {
        try {
            SandboxEndpoint endpoint = sbx.getEndpoint(80);
            HttpRequest.Builder request = HttpRequest.newBuilder()
                .uri(URI.create(config.getProtocol() + "://" + endpoint.getEndpoint() + "/"))
                .timeout(Duration.ofSeconds(2)).GET();
            endpoint.getHeaders().forEach(request::header);
            return http.send(request.build(), HttpResponse.BodyHandlers.discarding()).statusCode() == 200;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException("Health check interrupted", e);
        } catch (Exception e) {
            return false;
        }
    })
    .build();
```

### 3. Command Execution & Streaming

Execute commands and handle output streams in real-time.

```java
// Create handlers for streaming output
ExecutionHandlers handlers = ExecutionHandlers.builder()
    .onStdout(msg -> System.out.println("STDOUT: " + msg.getText()))
    .onStderr(msg -> System.err.println("STDERR: " + msg.getText()))
    .onExecutionComplete(complete ->
        System.out.println("Command finished in " + complete.getExecutionTimeInMillis() + "ms")
    )
    .build();

// Execute command with handlers
RunCommandRequest request = RunCommandRequest.builder()
    .command("for i in {1..5}; do echo \"Count $i\"; sleep 0.5; done")
    .handlers(handlers)
    .build();

sandbox.commands().run(request);
```

To execute a native program without shell parsing, pass an argument list. On Linux, this example prints literal `$HOME` and keeps `hello world` as one argument:

```java
sandbox.commands().run(RunCommandRequest.builder()
    .argv(List.of("printf", "%s\n", "$HOME", "hello world"))
    .build());
```

Native argv execution requires an updated execd. See [command execution modes](/architecture/data-plane/execd#command-execution) for executable lookup and platform behavior.

#### Background commands

Poll incremental logs and status. Command timeout is separate from sandbox TTL.

```java
Execution execution = sandbox.commands().run(RunCommandRequest.builder()
    .command("for i in 1 2 3; do echo step-$i; sleep 1; done")
    .background(true).timeout(Duration.ofSeconds(30)).build());
String commandId = Objects.requireNonNull(execution.getId(), "No command ID returned");
Long cursor = 0L;
long deadline = System.nanoTime() + Duration.ofSeconds(45).toNanos();
while (true) {
    if (System.nanoTime() >= deadline) {
        sandbox.commands().interrupt(commandId);
        throw new IllegalStateException("Command did not finish");
    }
    CommandStatus status = sandbox.commands().getCommandStatus(commandId);
    CommandLogs logs = sandbox.commands().getBackgroundCommandLogs(commandId, cursor);
    System.out.print(logs.getContent());
    if (logs.getCursor() != null) cursor = logs.getCursor();
    if (Boolean.FALSE.equals(status.getRunning())) {
        if (!Integer.valueOf(0).equals(status.getExitCode())) {
            throw new IllegalStateException("Command failed: " + status.getExitCode() + ", " + status.getError());
        }
        break;
    }
    Thread.sleep(500);
}
```

#### Persistent shell sessions

A Bash session preserves shell variables and the working directory across commands.

```java
String sessionId = sandbox.commands().createSession("/tmp");
try {
    sandbox.commands().runInSession(sessionId, RunInSessionRequest.builder()
        .command("export DEMO=hello").build());
    Execution result = sandbox.commands().runInSession(sessionId, RunInSessionRequest.builder()
        .command("echo \"$DEMO\"; pwd").build());
    result.getLogs().getStdout().forEach(message -> System.out.print(message.getText()));
} finally {
    sandbox.commands().deleteSession(sessionId);
}
```

#### Persistent environment variables

Set environment variables that the runtime injects into every subsequent command and session — without hand-writing shell escaping against the sandbox env file.

```java
sandbox.commands().setEnv("MY_TOKEN", "it's a safe value");
```

Keys must match `[A-Za-z_][A-Za-z0-9_]*`. Values without a single quote are stored verbatim; values containing a single quote use the env file's double-quoted form, in which shell-style `$NAME` sequences may be expanded when the runtime loads the file. The env file is append-only: the last write for a key wins. Throws `SandboxException` if the sandbox fails to persist the variable.

For filesystem/process isolation within a sandbox, see [Isolation Sessions](/guides/isolation-sessions). These are separate from Bash sessions.

### 4. File Operations

Manage files and directories, including read, write, list, delete, and search.

```java
// 1. Write file
sandbox.files().write(List.of(
    WriteEntry.builder()
        .path("/tmp/hello.txt")
        .data("Hello World")
        .mode(644)
        .build()
));

// 2. Read file
String content = sandbox.files().readFile("/tmp/hello.txt", "UTF-8", null);
System.out.println("Content: " + content);

// List immediate children; search filters by a filename pattern.
sandbox.files().listDirectory("/tmp").forEach(entry -> System.out.println(entry.getPath()));

// 3. Search files
List<EntryInfo> files = sandbox.files().search(
    SearchEntry.builder()
        .path("/tmp")
        .pattern("*.txt")
        .build()
);
files.forEach(f -> System.out.println("Found: " + f.getPath()));

// 4. Delete file
sandbox.files().deleteFiles(List.of("/tmp/hello.txt"));
```

#### File operations with an explicit identity

Bind a filesystem client to a Linux UID and GID:

```kotlin
val files = sandbox.filesWithIdentity(1001, 2000)
```

The bound client exposes the usual file operations and shares the sandbox connection, authentication, and endpoint routing headers. The existing file client retains its default identity. Select the UID/GID separately for command execution.

This requires Linux and an Execd version that supports filesystem execution identity. Unsupported servers return an error; requests never fall back to the default filesystem routes. Keep sandbox credentials and identity selection in the trusted backend. Linux filesystem permissions apply; this feature does not create a tenant isolation boundary. See [Execd](/architecture/data-plane/execd) for permission and workspace provisioning details.

### 5. Sandbox Management (Admin)

Use `SandboxManager` for administrative tasks and finding existing sandboxes.

```java
try (SandboxManager manager = SandboxManager.builder()
    .connectionConfig(config)
    .build()) {
    // First page only; increase page for subsequent pages.
    PagedSandboxInfos sandboxes = manager.listSandboxInfos(
        SandboxFilter.builder().states(SandboxState.RUNNING).pageSize(10).page(1).build()
    );
    sandboxes.getSandboxInfos().forEach(info -> System.out.println(info.getId()));
}
```

### Resource metrics

Read current sandbox resource usage with `sandbox.getMetrics()`. This is separate from [SDK creation telemetry](/sdks/observability#creation-metrics).

### 6. Client Pool and observability

`SandboxPool` provides four acquire policies and staged warmup controls. Use `InMemoryPoolStateStore` in one process, or the optional `com.alibaba.opensandbox:sandbox-pool-redis` module with a caller-managed Jedis client for distributed deployments. See [Client Pool](/guides/client-pool) for configuration, examples, cleanup, and namespace retirement.

Set `ConnectionConfig.builder().enableTracing(true)` to emit [pool warmup traces](/sdks/observability#pool-warmup-tracing).
The JVM SDK also adds trace IDs to SLF4J MDC. For remote logs/events, use
`SandboxManager.getDiagnosticContext`, `getDiagnosticLogs`, and
`getDiagnosticEvents`. These methods also exist on `Sandbox`;
see [SDK diagnostics](/sdks/#diagnostics).
Create-latency reporting is controlled separately by [SDK Telemetry](/sdks/observability#creation-metrics).

## Snapshots and metadata

Create a snapshot with `sandbox.createSnapshot` or `manager.createSnapshot`. `SandboxManager` exposes `getSnapshot`, `listSnapshots`, `deleteSnapshot`, and `waitForSnapshotReady` to wait for asynchronous snapshot completion. Restore with `Sandbox.builder().snapshotId(snapshotId).connectionConfig(config).build()`.

Use `sandbox.patchMetadata` or `manager.patchSandboxMetadata` to add/replace metadata keys; a `null` value removes a key. Runtime support is described by the [lifecycle contract](/api/#1-sandbox-lifecycle-yml).

Snapshot support depends on the runtime and server configuration. Renew the source sandbox first if its remaining TTL may expire during snapshot creation. The JVM helper waits for `Ready` and raises an exception on failure or timeout:

```java
try (SandboxManager manager = SandboxManager.builder().connectionConfig(config).build()) {
    SnapshotInfo snapshot = sandbox.createSnapshot("demo");
    System.out.println("Snapshot: " + snapshot.getId());
    manager.waitForSnapshotReady(snapshot.getId(), Duration.ofMinutes(15));
    try (Sandbox restored = Sandbox.builder()
            .snapshotId(snapshot.getId()).connectionConfig(config).build()) {
        try {
            System.out.println(restored.getId());
        } finally {
            restored.kill();
        }
    }
    // The snapshot is retained. When no longer needed: manager.deleteSnapshot(snapshot.getId());
}
```

Metadata updates can include both replacement values and removals. Use a map that accepts null values (`Map.of` does not):

```java
Map<String, String> patch = new HashMap<>();
patch.put("project", "demo");
patch.put("obsolete-key", null);
sandbox.patchMetadata(patch);
```

## Configuration

### 1. Connection Configuration

The `ConnectionConfig` class manages API server connection settings.

::: info Transfer timeouts
`requestTimeout` bounds each transport phase - connecting, waiting for response headers, and waiting for more of a response body - but it never caps a healthy file upload or download that keeps making progress. A transfer that stalls for longer than the timeout, or an upload that gets no response headers within it after the body has been sent, fails instead of hanging (the Go SDK's `ResponseHeaderTimeout` contract). An explicit `RetryPolicy.overallDeadline` still caps the whole transfer.
:::

| Parameter        | Description                                | Default                      | Environment Variable   |
| ---------------- | ------------------------------------------ | ---------------------------- | ---------------------- |
| `apiKey`         | API Key for authentication                 | Optional; needed when server auth is enabled | `OPEN_SANDBOX_API_KEY` |
| `domain`         | The endpoint domain of the sandbox service | `localhost:8080` | `OPEN_SANDBOX_DOMAIN`  |
| `protocol`       | HTTP protocol (http/https)                 | `http`                       | -                      |
| `requestTimeout` | Timeout for API requests                   | 30 seconds                   | -                      |
| `debug`          | Enable debug logging for HTTP requests     | `false`                      | -                      |
| `headers`        | Custom HTTP headers                        | Empty                        | -                      |
| `connectionPool` | Shared OKHttp ConnectionPool               | SDK-created per instance     | -                      |
| `retryPolicy`    | Automatic retry policy for non-streaming requests (see [Automatic retries](#_2-automatic-retries)) | Enabled (`RetryPolicy()`) | -                 |
| `useServerProxy` | Use sandbox server as proxy for execd/endpoint requests (e.g. when client cannot reach the sandbox directly) | `false` | -                      |
| `disableMetrics` | Disable SDK create-latency telemetry (see [SDK Telemetry](/sdks/observability#creation-metrics)) | `false` | `OPENSANDBOX_DISABLE_METRICS` |
| `enableTracing` | Enable OpenTelemetry tracing for pool warmup (see [SDK Tracing](/sdks/observability#pool-warmup-tracing)) | `false` | - |

```java
import okhttp3.ConnectionPool;
import java.util.concurrent.TimeUnit;

// 1. Basic configuration
ConnectionConfig config = ConnectionConfig.builder()
    .apiKey("your-key")
    .domain("api.opensandbox.io")
    .requestTimeout(Duration.ofSeconds(60))
    .build();

// 2. Advanced: Shared Connection Pool
// If you create many Sandbox instances, sharing a connection pool is recommended to save resources.
// SDK default keep-alive is 30 seconds for its own pools.
ConnectionPool sharedPool = new ConnectionPool(50, 30, TimeUnit.SECONDS);

ConnectionConfig sharedConfig = ConnectionConfig.builder()
    .apiKey("your-key")
    .domain("api.opensandbox.io")
    .headers(Map.of(
        "X-Custom-Header", "value",
        "X-Request-ID", "trace-123"
    ))
    .connectionPool(sharedPool) // Inject shared pool
    .build();
```

::: tip SDK Telemetry
`Sandbox.builder()...build()` reports create latency to `POST /v1/metrics/events` by default. Call `ConnectionConfig.builder().disableMetrics(true)` or export `OPENSANDBOX_DISABLE_METRICS=1` to opt out. See [SDK Telemetry](/sdks/observability#creation-metrics).
:::

### 2. Automatic retries

The SDK retries transient failures automatically. `ConnectionConfig` installs a `RetryInterceptor` (`com.alibaba.opensandbox.sandbox.transport.RetryPolicy`) on the SDK's non-streaming HTTP clients.

Default behavior:

- **Enabled by default.** Idempotent methods (`GET/HEAD/PUT/DELETE/OPTIONS`) are retried on `429`, `502`, `503`, and on pre-send transport failures (DNS, TCP connect, TLS handshake).
- **`POST`/`PATCH` are never retried on a status code by default**, since the request may already have been applied server-side. Pre-send transport failures (before any byte is written) are still retried for these methods.
- Up to `3` retries with decorrelated-jitter exponential backoff, honoring a server `Retry-After` header (capped at 60s).
- **SSE / streaming requests bypass all automatic retry** because their bodies are not safely replayable. The SSE client also disables OkHttp's built-in connection recovery to prevent a streaming command POST from being replayed.

::: warning Behavior change
SDK-policy retries are on by default. This can increase the number of HTTP attempts and tail latency compared to earlier SDK versions. To disable the new SDK-policy retries, use `RetryPolicy.disabled()`; non-streaming requests then fall back to OkHttp's pre-existing built-in connection recovery.
:::

```java
import com.alibaba.opensandbox.sandbox.transport.RetryPolicy;
import com.alibaba.opensandbox.sandbox.transport.StatusCode;
import java.time.Duration;
import java.util.Set;

// Disable SDK-policy retries and retain OkHttp's built-in connection recovery.
ConnectionConfig config = ConnectionConfig.builder()
    .apiKey("your-key")
    .domain("api.opensandbox.io")
    .retryPolicy(RetryPolicy.disabled())
    .build();

// Custom policy: more retries, an overall wall-clock deadline, and an opt-in to
// retry POST/PATCH on 503 (only safe if your endpoints are idempotent).
ConnectionConfig tuned = ConnectionConfig.builder()
    .apiKey("your-key")
    .domain("api.opensandbox.io")
    .retryPolicy(new RetryPolicy(
        /* maxRetries */ 5,
        /* initialBackoff */ Duration.ofMillis(500),
        /* maxBackoff */ Duration.ofSeconds(30),
        /* backoffMultiplier */ 2.0,
        /* jitter */ com.alibaba.opensandbox.sandbox.transport.JitterMode.DECORRELATED,
        /* retryableStatusCodesIdempotent */ RetryPolicy.DEFAULT_IDEMPOTENT_STATUS,
        /* retryableStatusCodesNonIdempotent */ Set.of(StatusCode.SERVICE_UNAVAILABLE),
        /* perAttemptTimeout */ null,
        /* overallDeadline */ Duration.ofSeconds(20),
        /* onRetry */ null))
    .build();
```

### 3. Sandbox Creation Configuration

The `Sandbox.builder()` allows configuring the sandbox environment.

| Parameter      | Description                              | Default                         |
| -------------- | ---------------------------------------- | ------------------------------- |
| `image`        | Docker image to use                      | One of image or snapshot ID |
| `timeout`      | Automatic termination timeout            | 10 minutes                      |
| `entrypoint`   | Container entrypoint command             | `["tail", "-f", "/dev/null"]`   |
| `resource`     | CPU and memory limits                    | `{"cpu": "1", "memory": "2Gi"}` |
| `env`          | Environment variables                    | Empty                           |
| `metadata`     | Custom metadata tags                     | Empty                           |
| `extensions`   | Opaque server-side extension parameters  | Empty                           |
| `networkPolicy` | Optional outbound network policy (egress) | -                             |
| `credentialProxy` | Optional Credential Vault proxy startup settings | -                     |
| `readyTimeout` | Max time to wait for sandbox to be ready | 30 seconds                      |
| `snapshotId` | Restore a snapshot instead of an image | - |
| `resourceRequests` | Kubernetes resource requests; must not exceed limits | - |
| `lifecycle` | Pre-start and periodic hooks | - |
| `platform` | OS/architecture constraint | - |
| `volumes` | Host, PVC, or OSSFS mounts | - |
| `secureAccess` | Require endpoint access credentials | `false` |

::: warning
Metadata keys under `opensandbox.io/` are reserved for system-managed labels and will be rejected by the server.
:::

```java
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.NetworkPolicy;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.NetworkRule;

Sandbox sandbox = Sandbox.builder()
    .connectionConfig(config)
    .image("python:3.11")
    .timeout(Duration.ofMinutes(30))
    .resource(Map.of("cpu", "2", "memory", "4Gi"))
    .env("PYTHONPATH", "/app")
    .metadata("project", "demo")
    .extension("storage.id", "dataset-001")
    .networkPolicy(
        NetworkPolicy.builder()
            .defaultAction(NetworkPolicy.DefaultAction.DENY)
            .addEgress(
                NetworkRule.builder()
                    .action(NetworkRule.Action.ALLOW)
                    .target("pypi.org")
                    .build()
            )
            .build()
    )
    .build();
```

### 4. Runtime Egress Policy Updates

Runtime egress reads and patches go directly to the sandbox egress sidecar. The SDK first resolves the sandbox endpoint on port `18080`, then calls the sidecar `/policy` API.

Template-backed sandboxes have no sandbox-side egress sidecar: the SDK detects them via the server's `OPEN-SANDBOX-ORIGIN` response header (see [Fsb Template Management](#fsb-template-management)) and routes the same `getEgressPolicy` / `patchEgressRules` / `deleteEgressRules` calls through the lifecycle control plane (`/sandboxes/{sandboxId}/networkpolicy`) instead.

Patch uses merge semantics:
- Incoming rules take priority over existing rules with the same `target`.
- Existing rules for other targets remain unchanged.
- Within a single patch payload, the first rule for a `target` wins.
- The current `defaultAction` is preserved.

```java
NetworkPolicy policy = sandbox.getEgressPolicy();

sandbox.patchEgressRules(
    List.of(
        NetworkRule.builder().action(NetworkRule.Action.ALLOW).target("www.github.com").build(),
        NetworkRule.builder().action(NetworkRule.Action.DENY).target("pypi.org").build()
    )
);
```

### 5. Credential Vault

Credential Vault injects outbound credentials from the egress sidecar while keeping real secrets out of sandbox environment variables, commands, files, and logs. Create the sandbox with `credentialProxyEnabled(true)`, then write credentials and bindings through `sandbox.credentialVault()`.

```
/* Detailed source-code truncated for AI context efficiency. */
```

See [Credential Vault](/guides/credential-vault) for auth types, binding guidance, and Git/curl examples.

::: warning
Credential Vault is unavailable for template-backed sandboxes: they have no sandbox-side egress sidecar. `sandbox.credentialVault()` throws for them.
:::

## Fsb Template Management

fsb (fast-sandbox microVM) golden-image templates are managed through `SandboxManager`. Template builds are asynchronous: `createTemplate` returns with `status.phase` set to `Pending`; poll `getTemplate` until the phase reaches `Succeeded` or `Failed`. Only a `Succeeded` template can create sandboxes. These operations require a configured Fsb runtime. Replace the example publish URI with a location configured for your server, and use an open `SandboxManager` created with `SandboxManager.builder().connectionConfig(config).build()`. Close the manager when finished.

```
/* Detailed source-code truncated for AI context efficiency. */
```

### Creating a Sandbox from a Template

Use `Sandbox.fromTemplate()` to create a sandbox from a `Succeeded` template. Template mode fixes the workload shape on the server: only `metadata`, `networkPolicy` and `extensions` may accompany the template id, and `timeout` is required.

```java
import java.time.Duration;

Sandbox sandbox = Sandbox.fromTemplate()
    .connectionConfig(config)
    .templateId("tpl_123")
    .timeout(Duration.ofMinutes(30))
    .metadata("project", "demo")
    .networkPolicy(
        NetworkPolicy.builder()
            .defaultAction(NetworkPolicy.DefaultAction.DENY)
            .addEgress(
                NetworkRule.builder()
                    .action(NetworkRule.Action.ALLOW)
                    .target("pypi.org")
                    .build()
            )
            .build()
    )
    .create();
```

The created sandbox reports `SandboxOrigin.TEMPLATE` from `sandbox.getOrigin()`. Template-backed sandboxes have no egress sidecar, so the SDK routes their egress policy through the lifecycle control plane automatically — including `Sandbox.connector()` and `Sandbox.resumer()` re-attach flows, which detect the origin from the server's `OPEN-SANDBOX-ORIGIN` response header.

---

### Sdks Mcp (`docs/sdks/mcp.md`)

---
title: MCP Sandbox Server
description: OpenSandbox MCP Server exposes sandbox operations as MCP tools for Claude Code, Cursor, and other MCP-capable clients.
---

# OpenSandbox MCP Sandbox Server

## Overview

OpenSandbox MCP Server exposes the OpenSandbox Python SDK as MCP tools for Claude Code, Cursor, and other MCP-capable clients. It provides focused sandbox lifecycle management, command execution, and text file operations.

## Installation & Startup

### Source

```bash
cd sdks/mcp/sandbox/python
uv sync
uv run opensandbox-mcp
```

### Package

```bash
pip install opensandbox-mcp
opensandbox-mcp
```

### Configuration

Environment variables:

- `OPEN_SANDBOX_API_KEY`
- `OPEN_SANDBOX_DOMAIN`

CLI overrides:

```bash
opensandbox-mcp --api-key ... --domain ... --protocol https
```

Config fields:

- `api_key`: OpenSandbox API key for authentication.
- `domain`: OpenSandbox API domain, for example `api.opensandbox.io`.
- `protocol`: `http` or `https` for API requests.
- `request_timeout_seconds`: HTTP request timeout in seconds.
- `transport`: `stdio` by default, or `streamable-http`.

### Streamable HTTP

```bash
opensandbox-mcp \
  --transport streamable-http \
  --host 0.0.0.0 \
  --port 8000
```

The HTTP transport defaults to `127.0.0.1:8000`. Set `--host 0.0.0.0` when the MCP server must be reached from another container, and use `--port` to select a different listening port. Keep the default loopback host unless the container network or an upstream gateway protects the exposed endpoint.

## Integrations

### Claude Code stdio

```bash
claude mcp add opensandbox-sandbox --transport stdio -- \
  opensandbox-mcp --api-key "$OPEN_SANDBOX_API_KEY" --domain "$OPEN_SANDBOX_DOMAIN"
```

### Claude Code http

```bash
claude mcp add opensandbox-sandbox --transport http http://localhost:8000/mcp
```

### Cursor stdio

```json
{
  "mcpServers": {
    "opensandbox-sandbox": {
      "command": "opensandbox-mcp",
      "args": [
        "--api-key",
        "${OPEN_SANDBOX_API_KEY}",
        "--domain",
        "${OPEN_SANDBOX_DOMAIN}"
      ]
    }
  }
}
```

### Cursor http

```json
{
  "mcpServers": {
    "opensandbox-sandbox": {
      "url": "http://localhost:8000/mcp"
    }
  }
}
```

## Tools

::: info
- Tools that interact with a sandbox use an ID registered by `sandbox_create` or `sandbox_connect`. `sandbox_list` lists server-side sandboxes without an existing local sandbox registration.
- `file_read`/`file_write` are text-only; use `encoding` and `range_header` for large files.
:::

The MCP server exposes the tool set below, not the complete Python SDK. It does not currently register tools for Client Pool, pool tracing, snapshots, templates, Credential Vault, or stable remote diagnostic logs/events. Use the [SDK guides](/sdks/) or [CLI](/cli/) for those workflows where supported.

### Sandbox

- `sandbox_create`: create a new sandbox and register it locally
- `sandbox_connect`: attach to an existing sandbox and register it locally
- `sandbox_kill`: terminate a sandbox by ID
- `sandbox_get_info`: fetch sandbox info by ID
- `sandbox_list`: list sandboxes with optional `filter` object
- `sandbox_renew`: extend sandbox expiration
- `sandbox_healthcheck`: check if sandbox is healthy
- `sandbox_get_metrics`: get resource metrics
- `sandbox_get_endpoint`: get network endpoint for a port

### Command Execution

- `command_run`: run a command inside a sandbox
- `command_interrupt`: interrupt a running command

### Filesystem

- `file_read`: read a text file
- `file_write`: write a text file
- `file_delete`: delete files
- `file_search`: search for files by glob
- `file_create_directories`: create directories
- `file_delete_directories`: delete directories
- `file_move`: move/rename files or directories
- `file_replace_contents`: replace file content

## Minimal Workflow

1. `sandbox_create` -> keep the `sandbox_id`.
2. `file_write` code or assets into the sandbox.
3. `command_run` to execute, install dependencies, or start a service.
4. `sandbox_get_endpoint` if you expose a port.
5. `sandbox_kill` when finished.

## Usage Examples

Here are some examples of what you can ask an LLM to do:

- "Create a Python sandbox and run a quick health command."
- "Write a Python script into the sandbox and run it."
- "Download a GitHub repo, install dependencies, and run its tests."
- "Generate a CSV file with fake sales data and run a simple summary script."
- "Start a tiny web server on port 8000 and return the public URL."
- "Build a minimal REST API (hello + health) and expose it on port 8000."
- "Create a tar.gz of /app and report the file size."
- "Build a simple Snake game and return the web endpoint where it can be accessed."

---

### Sdks Observability (`docs/sdks/observability.md`)

---
title: SDK Observability
description: Configure SDK creation metrics and OpenTelemetry pool warmup traces.
---

# SDK Observability

Use creation metrics to track sandbox startup latency and pool warmup traces to locate slow phases. They have separate controls and export paths:

| Signal | Use it to | SDK support | Default | Export path |
| --- | --- | --- | --- | --- |
| [Pool warmup traces](#pool-warmup-tracing) | Inspect creation, readiness, preparation, renewal, and idle publication | Python, JavaScript/TypeScript, Kotlin/Java | Off; enable tracing in the connection config | Application-owned OpenTelemetry provider and exporter |
| [Creation metrics](#creation-metrics) | Measure sandbox create latency and success | All five SDKs | On; disable through config or `OPENSANDBOX_DISABLE_METRICS=1` | SDK posts to the lifecycle server, which exports metrics when `[otel]` is enabled |

OpenTelemetry (OTel) provides the tracing and metrics infrastructure; enabling tracing does not enable or disable creation metrics. For remote sandbox logs and events, see the [diagnostics API](/api/#diagnostics).

## Pool warmup tracing

The Python, JavaScript/TypeScript, and Kotlin/Java SDKs can emit [OpenTelemetry](https://opentelemetry.io/) traces for the client-side `SandboxPool` warmup path. Each warmup task becomes one trace that covers the full lifecycle — from the moment the reconcile loop submits the task until the warmed sandbox is committed to the idle buffer — with per-phase spans so you can find the actual warmup bottleneck.

Tracing is **opt-in** (`enable_tracing=True` in Python or `enableTracing: true` in JavaScript, or `.enableTracing(true)` on the JVM) and **best-effort**: without an OpenTelemetry SDK + exporter in the application, all span calls are no-ops and nothing is exported. Tracing never affects pool behavior.

### SDK support

| SDK | Pool warmup tracing |
| --- | --- |
| Python async/sync | Phase spans, sandbox identity, terminal classification, readiness counters |
| JavaScript/TypeScript | Phase spans, pool identity, success/failure and error type |
| Kotlin/Java | Phase spans, sandbox identity, terminal classification, readiness counters, SLF4J MDC |
| Go / C# | No built-in pool warmup tracing |

This guide describes the default branch. Check your installed SDK's `ConnectionConfig` for `enable_tracing` / `enableTracing` before enabling it. Tracing defaults to **off**, independently of [create-latency telemetry](/sdks/observability#creation-metrics), which defaults to **on**.

### Enabling tracing

#### 1. Add an OpenTelemetry SDK + exporter to your application

These SDKs depend only on the OpenTelemetry API (no-op by default). To actually export traces you bring your own SDK and exporter. For Python:

```bash
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
```

For Node.js, install an OpenTelemetry SDK and exporter compatible with your application. See the [OpenTelemetry Node.js setup](https://opentelemetry.io/docs/languages/js/getting-started/nodejs/).

For Kotlin/Java:

```kotlin
dependencies {
    implementation("io.opentelemetry:opentelemetry-api:1.51.0")
    implementation("io.opentelemetry:opentelemetry-sdk:1.51.0")
    implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.51.0")
}
```

#### 2. Configure the global OpenTelemetry provider

Warmup spans use the language's global provider. Configure it at application startup, before creating or starting the pool. In JavaScript, register the provider, async context manager, and W3C propagator with the global `@opentelemetry/api` instance (a Node.js OpenTelemetry SDK can configure these together). For Python, use `opentelemetry.trace.set_tracer_provider(...)`; for Kotlin/Java, configure `GlobalOpenTelemetry`, for example:

```java
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;

SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
    .addSpanProcessor(BatchSpanProcessor.create(
        OtlpGrpcSpanExporter.builder()
            .setEndpoint("http://otel-collector:4317")
            .build()))
    .build();

OpenTelemetrySdk sdk = OpenTelemetrySdk.builder()
    .setTracerProvider(tracerProvider)
    .build();

GlobalOpenTelemetry.set(sdk);
```

::: tip Propagators
`OpenTelemetrySdk.builder()` defaults to **noop propagators**. If you want the SDK to inject the W3C `traceparent` header into lifecycle requests (so the lifecycle server can join the same trace once it supports tracing), configure W3C propagation explicitly:

```java
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
```
:::

::: tip Sampling
To keep trace volume bounded, use a sampling strategy such as `parentbased_traceidratio(0.1)` on the `SdkTracerProvider`. Trace-id-ratio sampling keeps client and server spans consistent for the same warmup.
:::

#### 3. Turn tracing on for the pool

JavaScript / TypeScript:

```ts
import { ConnectionConfig } from "@alibaba-group/opensandbox";

const config = new ConnectionConfig({ enableTracing: true });
// Pass config as connectionConfig when constructing SandboxPool.
```

Python:

```python
from opensandbox.config import ConnectionConfig

config = ConnectionConfig(enable_tracing=True)
```

Kotlin/Java:

```java
ConnectionConfig config = ConnectionConfig.builder()
    .enableTracing(true)
    .build();

SandboxPool pool = SandboxPool.builder()
    .poolName("demo-pool")
    .maxIdle(3)
    .stateStore(new InMemoryPoolStateStore())
    .connectionConfig(config)
    .creationSpec(PoolCreationSpec.builder().image("ubuntu:22.04").build())
    .build();
```

Pass this connection config to the pool. The SDK flag defaults to `false`; OpenTelemetry exporter and sampling configuration remain application-owned. Shut down the pool before flushing and closing the application tracer provider.

### What is traced

Each warmup task produces **one trace** with a root span and six possible phase types (siblings under the root, so each phase duration stands alone for comparison). Each readiness stage is summarized by one span across all of its delayed attempts; optional stages are absent when they are not configured:

| Span name | Covers |
|-----------|--------|
| `pool.warmup` (root) | Warmup task, from admission through completion. Python/JVM backdate it to submission time to include queue wait |
| `pool.warmup.create` | Sandbox creator invocation. The built-in lifecycle path makes one HTTP attempt; readiness is no longer part of this span |
| `pool.warmup.readiness` | Complete pre-prepare readiness stage (`warmupHealthCheck` or `ping`), including all delayed attempts |
| `pool.warmup.prepare` | The single invocation of `warmupSandboxPreparer` (user init script / setup work) |
| `pool.warmup.post_prepare_readiness` | Complete optional post-prepare validation stage, including all delayed attempts |
| `pool.warmup.renew` | TTL renewal right before committing the sandbox |
| `pool.warmup.commit` | Primary-lock renewal + `putIdle` against the state store |

#### Attributes by language

All three SDKs emit `pool.name`, `pool.owner`, `pool.run.generation`, and `pool.leader.epoch`. JavaScript currently adds `warmup.result` (`success` or `failure`) and `warmup.error.type`; it does not emit the sandbox identity, terminal-stage classification, or readiness-attempt counters below. Its success flag alone does not prove that the sandbox was committed to idle.

The following richer root attributes apply to **Python and Kotlin/Java**:

| Attribute | Value |
|-----------|-------|
| `pool.name` | Pool name |
| `pool.owner` | Pool owner id |
| `pool.run.generation` | Pool run generation |
| `pool.leader.epoch` | Leader epoch captured when this warmup was admitted |
| `sandbox.id` | Sandbox id when creation progressed far enough to obtain one |
| `sandbox.image` | Creation image |
| `warmup.stage` | Terminal stage: `admission`, `create`, `readiness`, `prepare`, `post_prepare_readiness`, `renew`, or `commit` |
| `warmup.result` | `success`, `failure`, `dropped`, or `cancelled` |
| `warmup.reason` | Stable terminal reason when the result is not successful |
| `warmup.error.category` | Stable error category such as `rate_limit`, `http_4xx`, `http_5xx`, `timeout`, `connection`, `callback`, or `state_store` |
| `warmup.error.type` | Exception class when an error is available |

Python/JVM readiness summary spans additionally expose `warmup.health.attempt_count`, `warmup.health.false_count`, `warmup.health.exception_count`, and `warmup.scheduler.delay_ms`. Failures are recorded with `recordException` on the affected phase span. The root span keeps the classified terminal stage, result, reason, and OpenTelemetry error status without duplicating the phase exception event.

::: warning Development snapshot attribute migration
Earlier development snapshots used the unnamespaced `result` and `drop.reason` attributes. The supported schema uses `warmup.result` and `warmup.reason` consistently across traces and structured logs. The old keys are not emitted in parallel; update any dashboards created against a development snapshot.
:::

### Correlating logs to traces

In Kotlin/Java, while a warmup trace is in progress, the pool publishes trace IDs to the SLF4J [MDC](https://www.slf4j.org/api/org/slf4j/MDC.html):

| MDC key | Value |
|---------|-------|
| `trace_id` | Current trace id |
| `span_id` | Current span id |

MDC requires a real SLF4J provider (logback, log4j2, ...). Add the keys to your log pattern once, and every pool log line carries the trace context:

```xml
<pattern>%d %-5level [%thread] %logger{36} trace_id=%X{trace_id} span_id=%X{span_id} - %msg%n</pattern>
```

### Querying traces

The trace id is random, so a warmup trace cannot be looked up "by pool name" directly. The reliable paths are:

1. **JVM log correlation.** With MDC configured, the pool logs `pool_name` and `sandbox_id` on its warmup lines (e.g. `Pool warmup sandbox entered idle`). Search your logs for a `sandbox_id` — the matching log lines carry `trace_id`, which you can open directly in your trace backend.
2. **Attribute query in the trace backend.** Filter spans by time window and attribute, e.g. TraceQL `{ span.pool.name = "demo-pool" }` (Grafana Tempo), or Jaeger tag search on `pool.name=...`. Backends that derive metrics from spans (Tempo metrics, Datadog span analytics) let you look at `pool.warmup` duration percentiles per `pool.name` first, then drill into slow traces.
3. **Trace-id-ratio sampling.** With sampled traces, `trace_id` in logs and the backend are consistent for the same warmup.

#### Bottleneck drill-down

The readiness counters and backdated queue timing below apply to Python/JVM. For JavaScript, compare phase durations and the four pool identity attributes.

```
pool.warmup root duration (p50/p95/p99) per pool.name
  └─ phase spans: create / readiness / prepare / post_prepare_readiness / renew / commit
       └─ single trace: root start gap = queue wait, then each phase duration
```

| Symptom | Likely cause |
|---------|--------------|
| Long gap before `pool.warmup.create` | Create tasks waiting for an executor thread; compare `warmupCreateQps` with create latency |
| Long gap between create and the first readiness span | Expected `warmupHealthCheckInitialDelay`, or delayed-stage capacity exhausted because `warmupConcurrency` is too low |
| `pool.warmup.create` slow | Lifecycle create API slow (for example image pull / execd startup) |
| Slow `pool.warmup.readiness` with a high `warmup.health.attempt_count` | Sandbox startup or the configured readiness predicate is the bottleneck |
| `pool.warmup.prepare` slow | Your `warmupSandboxPreparer` work is the bottleneck |
| Slow `pool.warmup.post_prepare_readiness` with a high attempt count | Prepared service is not yet healthy, or its validation predicate is slow |
| `pool.warmup.renew` slow | Lifecycle API TTL renewal |
| `pool.warmup.commit` slow | State-store lease renewal or idle publication (for example Redis round-trips) |

## Creation metrics

OpenSandbox SDKs report sandbox creation latency to the configured lifecycle server by default. This reporting is separate from opt-in [pool warmup tracing](/sdks/observability#pool-warmup-tracing). Reporting is best-effort: failures never affect `Sandbox.create`, and the payload contains no user content.

### Requirements

The `POST /v1/metrics/events` endpoint and the SDK reporters described below require the following minimum versions. Older SDKs simply do not emit events; older servers reject unknown routes with `404`, which the SDK swallows silently (see [Version skew](#version-skew) below).

| Component | Minimum version |
|-----------|-----------------|
| Server (`opensandbox-server`) | `0.2.2` |
| Python SDK (`opensandbox`) | `0.1.15` |
| JavaScript / TypeScript SDK (`@alibaba-group/opensandbox`) | `0.1.11` |
| Go SDK (`github.com/alibaba/OpenSandbox/sdks/sandbox/go`) | `1.0.5` |
| C# SDK (`Alibaba.OpenSandbox`) | `0.1.5` |
| Kotlin / Java SDK (`com.alibaba.opensandbox:sandbox`) | `1.0.17` |

#### Version skew

Reporting is fire-and-forget in every SDK: the POST runs on a background task/thread, and reporting failures are ignored. Debug logging varies by SDK; JavaScript, for example, does not log these failures. This means you can upgrade the SDK and the server independently:

- **New SDK, old server (`< 0.2.2`)**: the server returns `404` for `/v1/metrics/events`. The SDK ignores the response. `Sandbox.create` behavior is unchanged and no user-visible error is raised. A debug message may be emitted depending on the SDK.
- **Old SDK, new server**: the SDK does not emit events. The server histogram simply records nothing for that client.
- **Network errors, TLS failures, timeouts**: same behavior as the `404` case — swallowed, `Sandbox.create` unaffected.

### What is sent

After create succeeds or fails, the SDK fire-and-forget posts to `POST /v1/metrics/events`:

```json
{
  "eventType": "sandbox.create",
  "sandboxId": "sbx_...",
  "image": "python:3.12",
  "createDurationMs": 1842,
  "success": true
}
```

- `sandboxId` / `image` may be omitted when create fails early.
- SDK language and version come from the HTTP `User-Agent` header (for example `OpenSandbox-Python-SDK/0.1.15`), not from body fields.

The server accepts the event with `204` and, when `[otel]` is enabled, records an OTEL histogram. See [server configuration](https://github.com/opensandbox-group/OpenSandbox/blob/main/server/configuration.md#otel).

### When it runs

| SDK | Trigger |
|-----|---------|
| Python (async + sync) | After `Sandbox.create` / sync create completes or raises |
| JavaScript / TypeScript | After `Sandbox.create` completes or fails |
| Go | After `CreateSandbox` completes or fails |
| C# | After `Sandbox.CreateAsync` completes or fails |
| Kotlin | After standalone `Sandbox.builder()...build()` or a pool direct-create fallback completes or fails |

::: info Kotlin staged pool warmup
Kotlin staged warmup deliberately does **not** emit the legacy `sandbox.create` event. Its create phase returns before readiness polling, optional preparation, post-prepare validation, renewal, and idle commit, so reporting that partial phase as the existing end-to-end create histogram would give the metric a different meaning from standalone create.

This exclusion applies only to staged warmup. Standalone create and pool direct-create fallback keep reporting normally. Use the pool's structured summary logs and optional [warmup tracing](/sdks/observability#pool-warmup-tracing) to observe the complete staged-warmup lifecycle.
:::

### How to disable

Default is on. Opt out with either:

1. Environment variable (all SDKs):

```bash
export OPENSANDBOX_DISABLE_METRICS=1
```

2. Connection config field:

::: code-group

```python [Python]
from opensandbox import Sandbox
from opensandbox.config import ConnectionConfig

config = ConnectionConfig(disable_metrics=True)
sandbox = await Sandbox.create("python:3.12", connection_config=config)
```

```typescript [JavaScript]
import { ConnectionConfig, Sandbox } from "@alibaba-group/opensandbox";

const connectionConfig = new ConnectionConfig({ disableMetrics: true });
const sandbox = await Sandbox.create({
  image: "python:3.12",
  connectionConfig,
});
```

```go [Go]
cfg := opensandbox.ConnectionConfig{DisableMetrics: true}
sandbox, err := opensandbox.CreateSandbox(ctx, cfg, opensandbox.SandboxCreateOptions{
    Image: "python:3.12",
})
```

```csharp [C#]
using OpenSandbox;
using OpenSandbox.Config;

var connectionConfig = new ConnectionConfig(new ConnectionConfigOptions
{
    DisableMetrics = true,
});
var sandbox = await Sandbox.CreateAsync(new SandboxCreateOptions
{
    Image = "python:3.12",
    ConnectionConfig = connectionConfig,
});
```

```kotlin [Kotlin]
import com.alibaba.opensandbox.sandbox.Sandbox
import com.alibaba.opensandbox.sandbox.config.ConnectionConfig

val connectionConfig = ConnectionConfig.builder()
    .disableMetrics(true)
    .build()
val sandbox = Sandbox.builder()
    .image("python:3.12")
    .connectionConfig(connectionConfig)
    .build()
```

:::

These requests go to your configured lifecycle server. Use opt-out when you do not want this additional traffic or create-latency data recorded there.

---

### Sdks Python (`docs/sdks/python.md`)

---
title: Python SDK
description: Python SDK for creating, managing, and interacting with secure OpenSandbox environments.
---

# OpenSandbox SDK for Python

Create sandboxes, run commands, and manage files with async or synchronous Python APIs.

## Installation

### pip

```bash
pip install opensandbox
```

### uv

```bash
uv add opensandbox
```

## Quick Start

The following example shows how to create a sandbox and execute a shell command.

::: tip
Before running this example, ensure the OpenSandbox service is running. See the [Getting Started](/getting-started/) guide for startup instructions.
:::

```python
import asyncio
from opensandbox.sandbox import Sandbox
from opensandbox.config import ConnectionConfig
from opensandbox.exceptions import SandboxException

async def main():
    # 1. Configure connection
    config = ConnectionConfig(
        domain="api.opensandbox.io",
        api_key="your-api-key"
    )

    # 2. Create a Sandbox
    try:
        sandbox = await Sandbox.create(
            "ubuntu",
            connection_config=config
        )
        try:
            # 3. Execute a shell command
            execution = await sandbox.commands.run("echo 'Hello Sandbox!'")

            # 4. Print output
            print(execution.logs.stdout[0].text)
        finally:
            # 5. Terminate the remote sandbox and close local resources
            await sandbox.destroy()

    except SandboxException as e:
        # Handle Sandbox specific exceptions
        print(f"Sandbox Error: [{e.error.code}] {e.error.message}")
        # Server logs can be correlated by this request id (if available)
        print(f"Request ID: {e.request_id}")
    except Exception as e:
        print(f"Error: {e}")

if __name__ == "__main__":
    asyncio.run(main())
```

### Synchronous Quick Start

If you prefer a synchronous API, use `SandboxSync` / `SandboxManagerSync` and `ConnectionConfigSync`:

```python
from datetime import timedelta

from opensandbox import SandboxSync
from opensandbox.config import ConnectionConfigSync

config = ConnectionConfigSync(
    domain="api.opensandbox.io",
    api_key="your-api-key",
    request_timeout=timedelta(seconds=30),
)

sandbox = SandboxSync.create("ubuntu", connection_config=config)
try:
    execution = sandbox.commands.run("echo 'Hello Sandbox!'")
    print(execution.logs.stdout[0].text)
finally:
    sandbox.destroy()
```

Use `destroy()` for create-use-discard workflows. It calls `kill()` before `close()` and still closes local resources if remote termination fails. Context managers continue to call only `close()`, so the remote sandbox remains available for later `connect()` calls unless you explicitly kill or destroy it.

## Client Pool and observability

Use `SandboxPoolSync` for synchronous applications and `SandboxPoolAsync` for asyncio. Both provide in-memory and Redis-backed stores, four acquire policies, and staged warmup controls. See [Client Pool](/guides/client-pool) for examples, configuration, cleanup, and distributed deployment.

Enable `ConnectionConfig(enable_tracing=True)` or
`ConnectionConfigSync(enable_tracing=True)` for [pool warmup traces](/sdks/observability#pool-warmup-tracing).
For backend context and remote logs/events, use `SandboxManager.get_diagnostic_context`,
`get_diagnostic_logs`, and `get_diagnostic_events`. These methods also exist on
`Sandbox`; see [SDK diagnostics](/sdks/#diagnostics).
Create-latency reporting is controlled separately by [SDK Telemetry](/sdks/observability#creation-metrics).

## Lifecycle Hooks

Pass a `SandboxLifecycle` when creating a sandbox. `pre_start` completes before the entrypoint starts, while `periodic` hooks run on their schedules after startup.

```python
from opensandbox.models.sandboxes import (
    LifecycleHook,
    PeriodicLifecycleHook,
    SandboxLifecycle,
)

sandbox = await Sandbox.create(
    "ubuntu:24.04",
    connection_config=config,
    lifecycle=SandboxLifecycle(
        pre_start=LifecycleHook(
            command=["sh", "-c", "echo ready > /tmp/prestart.done"],
            timeout_seconds=120,
        ),
        periodic=[
            PeriodicLifecycleHook(
                name="checkpoint",
                schedule="@every 5m",
                command=["sh", "-c", "date -u >> /tmp/checkpoints.log"],
                timeout_seconds=120,
            )
        ],
    ),
)
```

The Server validates `timeout_seconds`; `pre_start` accepts 1–10800 seconds, while `periodic` accepts 1–300 seconds. Both default to 60 seconds when omitted. See [Lifecycle Hooks](/guides/lifecycle-hooks) for timing, failure behavior, and provider limitations.

### Idle policy (auto-pause and wake-on-access)

`SandboxLifecycle.idle_policy` (OSEP-0024, fast-sandbox only) tells the control plane to auto-pause the sandbox after `idle_timeout_seconds` (30–86400) of continuously idle ingress-observed traffic, and to wake it on the next request. It is control-plane state: it never reaches the guest, is allowed in template mode (`Sandbox.create_from_template(idle_policy=...)`), and is rejected by non-fast-sandbox backends. Omitting it preserves non-automated behavior exactly.

```python
from datetime import timedelta

sandbox = await Sandbox.create(
    "ubuntu:24.04",
    connection_config=config,
    timeout=timedelta(seconds=86400),
    lifecycle=SandboxLifecycle(
        idle_policy=IdlePolicy(
            idle_timeout_seconds=300,
            on_idle="pause",
            wake_on_access=True,
        )
    ),
)
```

`active_probe` lets the application veto a pause while real work runs without producing ingress traffic: declare an endpoint answering `{"active": bool, "message"?: string}` and the server polls it when the sandbox is otherwise idle; a busy answer resets the idle clock.

```python
IdlePolicy(
    idle_timeout_seconds=300,
    on_idle="pause",
    active_probe=ActivityProbe(port=8080, path="/active", timeout_seconds=2),
)
```

### Renewal policy (renew-on-access)

`SandboxLifecycle.renewal_policy.extend_seconds` (OSEP-0009) opts the sandbox into automatic expiration extension on observed access: each proxied or gateway-observed request moves the expiration to `max(now + extend_seconds, current expires_at)`. It is control-plane state and never reaches the guest. Requires the server operator to enable `[renew_intent]` — see [Renew-on-Access](/guides/renew-on-access). The deprecated `extensions["access.renew.extend.seconds"]` key remains supported through 1.x and is removed in 2.0.

```python
from datetime import timedelta

sandbox = await Sandbox.create(
    "ubuntu:24.04",
    connection_config=config,
    timeout=timedelta(seconds=3600),
    lifecycle=SandboxLifecycle(
        renewal_policy=RenewalPolicy(extend_seconds=3600),
    ),
)
```

### Wake retry

A request to a paused sandbox may arrive while its restore is still in progress. The gateway answers `503` with the `OpenSandbox-Wake: expired` marker; the SDK recognizes it and resends the original request after the `Retry-After` delay (safe for any method — the marker proves the request was never forwarded). The wake budget defaults to 5 attempts / 60 seconds across all wake cycles; when it is exhausted the SDK raises `SandboxWakeTimeoutException` (carrying `attempts` and `waited`) while the server-side restore continues in the background. Bare `503` responses follow the existing retry rules untouched. Streaming (SSE) calls bypass wake retry.

## Usage Examples

The snippets below use `config` and a live `sandbox` from the quick start. Run async snippets inside an async function, before its cleanup block. Creation examples are alternatives; call `destroy()` when each sandbox is no longer needed. For the synchronous API, use `SandboxSync` and `SandboxManagerSync`, and omit `await` / `async` from SDK calls and context managers.

### 1. Lifecycle Management

Manage the sandbox lifecycle, including renewal, pausing, and resuming.

```python
import time
from datetime import timedelta

# Renew the sandbox
# This resets the expiration time to (current time + duration)
await sandbox.renew(timedelta(minutes=30))

# Request pause (runtime-dependent)
await sandbox.pause()

deadline = time.monotonic() + 120
while True:
    if time.monotonic() >= deadline:
        raise TimeoutError("Sandbox did not pause within 120 seconds")
    info = await sandbox.get_info()
    if info.status.state == "Paused":
        break
    if info.status.state == "Failed":
        raise RuntimeError(info.status.message)
    await asyncio.sleep(1)

# Resume creates a new local handle.
resumed = await Sandbox.resume(sandbox_id=sandbox.id, connection_config=config)
await sandbox.close()
sandbox = resumed

# Get current status
info = await sandbox.get_info()
print(f"State: {info.status.state}")
print(f"Expires: {info.expires_at}")  # None when no automatic expiration is configured
```

Pause is asynchronous and runtime-dependent. See [Pause and Resume](/guides/pause-resume). Use `await Sandbox.connect(sandbox_id, connection_config=config)` to attach to an already running sandbox without resuming it.

Create a non-expiring sandbox by explicitly passing `timeout=None`. Omitting `timeout` uses the default 10-minute TTL:

```python
manual = await Sandbox.create(
    "ubuntu",
    connection_config=config,
    timeout=None,
)
```

### 2. Custom Health Check

Resolving an endpoint confirms that a route exists; it does not confirm that the application on that port is healthy. For service readiness, make a bounded request to the application's health endpoint and include the returned endpoint headers.

With the built-in health probe, readiness checks during creation, connection, and resume fail immediately when the health endpoint returns HTTP 401 or 403. The SDK raises `SandboxApiException` with the original status, error details, and request ID instead of waiting for `SandboxReadyTimeoutException`. Check the endpoint credentials or permissions before retrying. Transient health failures retain their existing polling behavior; `is_healthy()` still returns `False` for a failed built-in health probe.

Define custom logic to determine if the sandbox is healthy. This overrides the default ping check. Synchronous checks must set their own timeouts because the SDK cannot interrupt them; asynchronous checks must not block the event loop or suppress cancellation.

```python
import httpx

async def custom_health_check(sbx: Sandbox) -> bool:
    endpoint = await sbx.get_endpoint(80)
    url = f"{sbx.connection_config.protocol}://{endpoint.endpoint}/"
    try:
        async with httpx.AsyncClient(timeout=2) as client:
            response = await client.get(url, headers=endpoint.headers)
        return response.status_code == 200
    except httpx.RequestError:
        return False

sandbox = await Sandbox.create(
    "nginx:latest",
    entrypoint=["nginx", "-g", "daemon off;"],
    connection_config=config,
    health_check=custom_health_check,
)
```

### 3. Command Execution & Streaming

Execute commands and handle output streams in real-time.

```python
from opensandbox.models.execd import ExecutionHandlers, RunCommandOpts

# Define async handlers for streaming output
async def handle_stdout(msg):
    print(f"STDOUT: {msg.text}")

async def handle_stderr(msg):
    print(f"STDERR: {msg.text}")

async def handle_complete(complete):
    print(f"Command finished in {complete.execution_time_in_millis}ms")

# Create handlers (all handlers must be async)
handlers = ExecutionHandlers(
    on_stdout=handle_stdout,
    on_stderr=handle_stderr,
    on_execution_complete=handle_complete
)

# Execute command with handlers
result = await sandbox.commands.run(
    "for i in {1..5}; do echo \"Count $i\"; sleep 0.5; done",
    handlers=handlers
)
```

To execute a native program without shell parsing, pass an argument list. On Linux, this example prints literal `$HOME` and keeps `hello world` as one argument:

```python
result = await sandbox.commands.run(["printf", "%s\n", "$HOME", "hello world"])
```

Native argv execution requires an updated execd. See [command execution modes](/architecture/data-plane/execd#command-execution) for executable lookup and platform behavior.

#### Background commands

Start a bounded command, read incremental logs, and check its exit status. Command timeout and sandbox TTL are separate settings.

```python
import time
from datetime import timedelta
from opensandbox.models.execd import RunCommandOpts

execution = await sandbox.commands.run(
    'for i in 1 2 3; do echo "step $i"; sleep 1; done',
    opts=RunCommandOpts(background=True, timeout=timedelta(seconds=30)),
)
if not execution.id:
    raise RuntimeError("No command ID returned")
cursor = 0
deadline = time.monotonic() + 45
while True:
    if time.monotonic() >= deadline:
        await sandbox.commands.interrupt(execution.id)
        raise TimeoutError("Command did not finish")
    status = await sandbox.commands.get_command_status(execution.id)
    logs = await sandbox.commands.get_background_command_logs(execution.id, cursor)
    print(logs.content, end="")
    cursor = logs.cursor if logs.cursor is not None else cursor
    if status.running is False:
        if status.exit_code != 0:
            raise RuntimeError(f"Command failed: {status.exit_code}, {status.error}")
        break
    await asyncio.sleep(0.5)
```

#### Persistent shell sessions

Use a Bash session to preserve shell variables and the working directory across commands. Delete the session when finished.

```python
session_id = await sandbox.commands.create_session(working_directory="/tmp")
try:
    await sandbox.commands.run_in_session(session_id, "export DEMO=hello")
    result = await sandbox.commands.run_in_session(session_id, 'echo "$DEMO"; pwd')
    print("".join(message.text for message in result.logs.stdout))
finally:
    await sandbox.commands.delete_session(session_id)
```

#### Persistent environment variables

Set environment variables that the runtime injects into every subsequent command and session — without hand-writing shell escaping against the sandbox env file.

```python
await sandbox.commands.set_env("MY_TOKEN", "it's a safe value")
```

Keys must match `[A-Za-z_][A-Za-z0-9_]*`. Values without a single quote are stored verbatim; values containing a single quote use the env file's double-quoted form, in which shell-style `$NAME` sequences may be expanded when the runtime loads the file. The env file is append-only: the last write for a key wins. Raises `SandboxException` if the sandbox fails to persist the variable. The sync API exposes the same method on `sandbox.commands`.

For commands that need filesystem/process isolation within a sandbox, see [Isolation Sessions](/guides/isolation-sessions). These are separate from Bash sessions.

### 4. File Operations

Manage files and directories, including read, write, list, delete, and search.

```python
from opensandbox.models.filesystem import DirectoryListEntry, WriteEntry, SearchEntry

# 1. Write file
await sandbox.files.write_files([
    WriteEntry(
        path="/tmp/hello.txt",
        data="Hello World",
        mode=644
    )
])

# 2. Read file
content = await sandbox.files.read_file("/tmp/hello.txt")
print(f"Content: {content}")

# List immediate children; search filters by a filename pattern.
entries = await sandbox.files.list_directory(DirectoryListEntry(path="/tmp", depth=1))
print([entry.path for entry in entries])

# 3. Search files
files = await sandbox.files.search(
    SearchEntry(
        path="/tmp",
        pattern="*.txt"
    )
)
for f in files:
    print(f"Found: {f.path}")

# 4. Delete file
await sandbox.files.delete_files(["/tmp/hello.txt"])
```

For binary data, pass `bytes` to `WriteEntry.data` and use `read_bytes()`; use `read_bytes_stream()` for large downloads. `read_file()` and `read_bytes()` also accept `offset` and `limit` for partial reads.

#### File operations with an explicit identity

Bind a filesystem client to a Linux UID and GID:

```python
files = sandbox.files_with_identity(1001, 2000)
```

Both the asynchronous and synchronous Sandbox clients expose this helper.

The bound client exposes the usual file operations and shares the sandbox connection, authentication, and endpoint routing headers. The existing file client retains its default identity. Select the UID/GID separately for command execution.

This requires Linux and an Execd version that supports filesystem execution identity. Unsupported servers return an error; requests never fall back to the default filesystem routes. Keep sandbox credentials and identity selection in the trusted backend. Linux filesystem permissions apply; this feature does not create a tenant isolation boundary. See [Execd](/architecture/data-plane/execd) for permission and workspace provisioning details.

### 5. Sandbox Management (Admin)

Use `SandboxManager` for administrative tasks and finding existing sandboxes.

```python
from opensandbox.manager import SandboxManager
from opensandbox.models.sandboxes import SandboxFilter

# Create manager using async context manager
async with await SandboxManager.create(connection_config=config) as manager:

    # First page only; increase page to retrieve subsequent pages.
    sandboxes = await manager.list_sandbox_infos(
        SandboxFilter(
            states=["Running"],
            page_size=10
        )
    )

    for info in sandboxes.sandbox_infos:
        print(f"Found sandbox: {info.id}")
```

### Resource metrics

Read current sandbox resource usage with `await sandbox.get_metrics()`. This is separate from [SDK creation telemetry](/sdks/observability#creation-metrics).

## Snapshots, templates, and metadata

| Operation | Public API |
| --- | --- |
| Snapshot a sandbox | `sandbox.create_snapshot(name=...)` or `manager.create_snapshot(sandbox_id, name=...)` |
| Inspect/list/delete snapshots | `manager.get_snapshot`, `list_snapshots`, `delete_snapshot` |
| Restore a snapshot | `Sandbox.create(snapshot_id=..., connection_config=config)` |
| Manage Fsb templates | `manager.create_template`, `get_template`, `list_templates`, `delete_template` |
| Create from a published template | `Sandbox.create_from_template(template_id, timeout=..., connection_config=config)` |
| Patch metadata | `sandbox.patch_metadata` or `manager.patch_sandbox_metadata` |

These APIs also exist on the synchronous SDK. Snapshot creation and template builds are asynchronous: inspect status before restoring or using a template. Templates must reach `Succeeded`; template-backed creation requires a TTL and inherits environment, resources, volumes, and lifecycle hooks from the template. Metadata patch values add/replace keys; `None` deletes a key. See the [lifecycle contract](/api/#1-sandbox-lifecycle-yml) for backend constraints.

Snapshot support depends on the runtime and server configuration. Renew the source sandbox first if its remaining TTL may expire during snapshot creation. This example waits up to 15 minutes, restores a new sandbox, and retains the snapshot for reuse:

```python
import time
from opensandbox.manager import SandboxManager

async with await SandboxManager.create(connection_config=config) as manager:
    snapshot = await sandbox.create_snapshot(name="demo")
    print("Snapshot:", snapshot.id)
    deadline = time.monotonic() + 900
    while True:
        if time.monotonic() >= deadline:
            raise TimeoutError(f"Snapshot {snapshot.id} is not ready")
        snapshot = await manager.get_snapshot(snapshot.id)
        if snapshot.status.state == "Ready":
            break
        if snapshot.status.state == "Failed":
            raise RuntimeError(snapshot.status.message)
        await asyncio.sleep(2)
    restored = await Sandbox.create(snapshot_id=snapshot.id, connection_config=config)
    try:
        print(restored.id)
    finally:
        await restored.destroy()
    # When no longer needed: await manager.delete_snapshot(snapshot.id)
```

Create from an existing template after its build reaches `Succeeded`:

```python
from datetime import timedelta

templated = await Sandbox.create_from_template(
    "your-published-template-id",
    timeout=timedelta(minutes=10),
    connection_config=config,
)
```

Add, replace, or remove metadata on a running sandbox:

```python
await sandbox.patch_metadata({"project": "demo", "obsolete-key": None})
```

## Configuration

### 1. Connection Configuration

The `ConnectionConfig` class manages API server connection settings.

| Parameter         | Description                                | Default                      | Environment Variable   |
| ----------------- | ------------------------------------------ | ---------------------------- | ---------------------- |
| `api_key`         | API Key for authentication                 | Optional; needed when server auth is enabled | `OPEN_SANDBOX_API_KEY` |
| `domain`          | The endpoint domain of the sandbox service | `localhost:8080` | `OPEN_SANDBOX_DOMAIN`  |
| `protocol`        | HTTP protocol (http/https)                 | `http`                       | -                      |
| `request_timeout` | Timeout for API requests                   | 30 seconds                   | -                      |
| `debug`           | Enable debug logging for HTTP requests     | `False`                      | -                      |
| `headers`         | Custom HTTP headers                        | Empty                        | -                      |
| `follow_redirects` | Follow HTTP redirects for SDK requests    | `False`                      | -                      |
| `event_hooks`     | Additional httpx hooks for adapter clients | Empty                        | -                      |
| `transport`       | Shared httpx transport (pool/proxy/retry); custom transports must honor request timeouts  | SDK-created per instance     | -                      |
| `retry_policy`    | Automatic retry policy for non-streaming requests (see [Automatic retries](#_2-automatic-retries)) | Enabled (`RetryPolicy()`) | -                 |
| `use_server_proxy` | Use sandbox server as proxy for execd/endpoint requests (e.g. when client cannot reach the sandbox directly) | `False` | -                      |
| `disable_metrics` | Disable SDK create-latency telemetry (see [SDK Telemetry](/sdks/observability#creation-metrics)) | `False` | `OPENSANDBOX_DISABLE_METRICS` |
| `enable_tracing` | Enable OpenTelemetry tracing for pool warmup (see [SDK Tracing](/sdks/observability#pool-warmup-tracing)) | `False` | - |

The Python Code Interpreter inherits the sandbox's connection configuration. With `use_server_proxy=True`, context requests and streamed code execution include the configured API key in `OPEN-SANDBOX-API-KEY` for server authentication. In direct mode, the SDK does not automatically attach this key to code execution requests. This applies to both the asynchronous and synchronous APIs.

When `follow_redirects` is enabled, same-origin redirects preserve request headers. An origin is the combination of scheme, host, and port, so changing any of those values is cross-origin. Before following a cross-origin redirect, the SDK removes every header whose name starts with `OPEN-SANDBOX-` or `OPENSANDBOX-` (case-insensitive). Other custom headers are not stripped automatically.

File uploads never follow redirects, even when `follow_redirects` is enabled, because streamed or file-backed multipart request bodies cannot always be safely replayed. This applies to both direct chunked uploads and uploads through the server proxy. A redirect response from an upload is surfaced as a `SandboxApiException` with the original 3xx status.

`ConnectionConfig.event_hooks` accepts async httpx hooks, while `ConnectionConfigSync.event_hooks` accepts synchronous hooks. Configured request hooks run before the SDK safety hook, so they cannot re-add protected OpenSandbox headers to a cross-origin request. Lifecycle telemetry honors `follow_redirects` and the SDK safety hook, but does not invoke configured user hooks.

```python
from datetime import timedelta

# 1. Basic configuration
config = ConnectionConfig(
    api_key="your-key",
    domain="api.opensandbox.io",
    request_timeout=timedelta(seconds=60)
)

# 2. Advanced: Custom headers and custom transport
# If you create many Sandbox instances, configuring a shared transport is recommended to optimize resource usage.
# SDK default keep-alive is 30 seconds for its own transports.
import httpx

config = ConnectionConfig(
    api_key="your-key",
    domain="api.opensandbox.io",
    headers={
        "X-Custom-Header": "value",
        "X-Request-ID": "trace-123",
    },
    transport=httpx.AsyncHTTPTransport(
        limits=httpx.Limits(
            max_connections=100,
            max_keepalive_connections=50,
            keepalive_expiry=30.0,
        )
    ),
)

# If you provide a custom transport, you are responsible for closing it:
# await config.transport.aclose()
```

### 2. Automatic retries

The SDK retries transient failures automatically. `ConnectionConfig` / `ConnectionConfigSync` install a retry wrapper around the default shared transport, controlled by `retry_policy` (`opensandbox.transport.RetryPolicy`).

Default behavior:

- **Enabled by default.** Idempotent methods (`GET/HEAD/PUT/DELETE/OPTIONS`) are retried on `429`, `502`, `503`, and on pre-send transport failures (DNS, TCP connect, TLS handshake, fresh-connection reset).
- **`POST`/`PATCH` are never retried on a status code by default**, since the request may already have been applied server-side. Pre-send transport failures (before any byte is written) are still retried for these methods.
- Up to `3` retries with decorrelated-jitter exponential backoff, honoring a server `Retry-After` header (capped at 60s).
- **SSE / streaming requests bypass retry** entirely (bodies are not replayable).

::: warning Behavior change
Retries are on by default. This can increase the number of HTTP attempts and tail latency compared to earlier SDK versions. If you rely on fast-fail semantics, opt out explicitly with `RetryPolicy.disabled()`.
:::

```python
from datetime import timedelta

from opensandbox.transport import RetryPolicy

# Fast-fail: never retry (also skips fresh-connection recovery).
config = ConnectionConfig(
    api_key="your-key",
    domain="api.opensandbox.io",
    retry_policy=RetryPolicy.disabled(),
)

# Custom policy: more retries, an overall wall-clock deadline, and an
# opt-in to retry POST/PATCH on 503 (only safe if your endpoints are
# idempotent).
from http import HTTPStatus

config = ConnectionConfig(
    api_key="your-key",
    domain="api.opensandbox.io",
    retry_policy=RetryPolicy(
        max_retries=5,
        overall_deadline=timedelta(seconds=20),
        retryable_status_codes_non_idempotent=frozenset(
            {HTTPStatus.SERVICE_UNAVAILABLE}
        ),
    ),
)
```

::: info
If you pass a custom `transport`, the SDK does **not** wrap it; installing retry behavior is then your responsibility.
:::

### 3. Sandbox Creation Configuration

The `Sandbox.create()` allows configuring the sandbox environment.

| Parameter       | Description                              | Default                         |
| --------------- | ---------------------------------------- | ------------------------------- |
| `image`    | Docker image specification               | One of image or snapshot ID |
| `timeout`       | Automatic termination timeout            | 10 minutes                      |
| `entrypoint`    | Container entrypoint command             | `["tail", "-f", "/dev/null"]`   |
| `resource`      | CPU and memory limits                    | `{"cpu": "1", "memory": "2Gi"}` |
| `env`           | Environment variables                    | Empty                           |
| `metadata`      | Custom metadata tags                     | Empty                           |
| `network_policy` | Optional outbound network policy (egress) | -                             |
| `credential_proxy` | Optional Credential Vault proxy startup settings | -                     |
| `ready_timeout` | Total budget for endpoint publication and health checks | 30 seconds                      |
| `snapshot_id` | Restore a snapshot instead of passing `image` | - |
| `resource_requests` | Kubernetes resource requests; must not exceed limits | - |
| `lifecycle` | Pre-start and periodic hooks | - |
| `platform` | OS/architecture constraint | - |
| `volumes` | Host, PVC, or OSSFS mounts | - |
| `secure_access` | Require endpoint access credentials | `False` |
| `skip_health_check` | Skip health checks; endpoint publication is still awaited | `False` |

::: warning
Metadata keys under `opensandbox.io/` are reserved for system-managed labels and will be rejected by the server.
:::

```python
from datetime import timedelta

from opensandbox.models.sandboxes import NetworkPolicy, NetworkRule

sandbox = await Sandbox.create(
    "python:3.11",
    connection_config=config,
    timeout=timedelta(minutes=30),
    resource={"cpu": "2", "memory": "4Gi"},
    env={"PYTHONPATH": "/app"},
    metadata={"project": "demo"},
    network_policy=NetworkPolicy(
        defaultAction="deny",
        egress=[NetworkRule(action="allow", target="pypi.org")],
    ),
)
```

### 4. Runtime Egress Policy Updates

Runtime egress policy routing depends on the sandbox origin. For image-backed sandboxes, the SDK resolves port `18080` and calls the sidecar `/policy` API. For template-backed sandboxes (including restored template snapshots), the SDK detects `OPEN-SANDBOX-ORIGIN: template` and routes policy operations through the lifecycle `/sandboxes/{sandboxId}/networkpolicy` API.

Patch uses merge semantics:
- Incoming rules take priority over existing rules with the same `target`.
- Existing rules for other targets remain unchanged.
- Within a single patch payload, the first rule for a `target` wins.
- The current `defaultAction` is preserved.

```python
from opensandbox.models.sandboxes import NetworkRule

policy = await sandbox.get_egress_policy()

await sandbox.patch_egress_rules(
    [
        NetworkRule(action="allow", target="www.github.com"),
        NetworkRule(action="deny", target="pypi.org"),
    ]
)
```

### 5. Credential Vault

Credential Vault requires a sandbox-side egress service and is unavailable for template-backed sandboxes. It injects outbound credentials from the egress sidecar while keeping real secrets out of sandbox environment variables, commands, files, and logs. Create the sandbox with `credential_proxy` enabled, then write credentials and bindings through `sandbox.credential_vault`.

```python
from opensandbox.models.sandboxes import (
    Credential,
    CredentialBinding,
    CredentialProxyConfig,
    NetworkPolicy,
    NetworkRule,
)

sandbox = await Sandbox.create(
    "python:3.11",
    connection_config=config,
    network_policy=NetworkPolicy(
        defaultAction="deny",
        egress=[NetworkRule(action="allow", target="api.example.com")],
    ),
    credential_proxy=CredentialProxyConfig(enabled=True),
)

await sandbox.credential_vault.create(
    credentials=[Credential(name="api-token", source={"value": "<token>"})],
    bindings=[
        CredentialBinding(
            name="api-token",
            match={
                "schemes": ["https"],
                "hosts": ["api.example.com"],
                "paths": ["/v1/*"],
            },
            auth={"type": "apiKey", "name": "x-api-key", "credential": "api-token"},
        )
    ],
)
```

See [Credential Vault](/guides/credential-vault) for auth types, binding guidance, and Git/curl examples.

---

### Kubernetes Docs BUILD IMAGES (`kubernetes/docs/BUILD-IMAGES.md`)

# Image Build Guide

This document describes how to build OpenSandbox Kubernetes Controller and Task Executor images.

## Option 1: Using the Build Script (Recommended)

### Local Build

```bash
cd kubernetes

# Build controller image
COMPONENT=controller TAG=v0.1.0 PUSH=false ./build.sh

# Build task-executor image
COMPONENT=task-executor TAG=v0.1.0 PUSH=false ./build.sh
```

### Build and Push to Registry

```bash
# Ensure you are logged in to Alibaba Cloud ACR
docker login sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com

# Optional: also push to GitHub Container Registry
docker login ghcr.io

# Build and push controller image
COMPONENT=controller TAG=v0.1.0 ./build.sh

# Build and push task-executor image
COMPONENT=task-executor TAG=v0.1.0 ./build.sh
```

### Environment Variables

- `COMPONENT`: The component to build. Options: `controller`, `task-executor`
- `TAG`: Image tag, defaults to `latest`
- `PUSH`: Whether to push to remote registry, defaults to `true`
- `GHCR_REPO`: Optional GHCR repository prefix, for example `ghcr.io/opensandbox-group/opensandbox`

## Option 2: Using GitHub Actions

### Manually Trigger the Workflow

1. Open the [Actions page](https://github.com/opensandbox-group/OpenSandbox/actions)
2. Select the "Publish Components Image" workflow
3. Click "Run workflow"
4. Select the component and image tag:
   - Component: Select the component name from the dropdown
     - Controller: `controller`
     - Task Executor: `task-executor`
   - Image tag: Enter the image tag, e.g. `v0.1.0`
5. Click "Run workflow" to start the build

### Trigger via Git Tag (Recommended)

Create a tag with a specific prefix to automatically trigger the build:

```bash
# Build controller v0.1.0
git tag k8s/controller/v0.1.0
git push origin k8s/controller/v0.1.0

# Build task-executor v0.1.0
git tag k8s/task-executor/v0.1.0
git push origin k8s/task-executor/v0.1.0
```

**Tag naming convention**: `k8s/<component>/<version>`
- `<component>`: Component name, `controller` or `task-executor`
- `<version>`: Image version, e.g. `v0.1.0`

## Option 3: Using Makefile

```bash
cd kubernetes

# Build controller image (local only)
make docker-build CONTROLLER_IMG=myregistry/opensandbox-controller:v0.1.0

# Build task-executor image (local only)
make docker-build-task-executor TASK_EXECUTOR_IMG=myregistry/opensandbox-task-executor:v0.1.0

# Push images
make docker-push CONTROLLER_IMG=myregistry/opensandbox-controller:v0.1.0
make docker-push-task-executor TASK_EXECUTOR_IMG=myregistry/opensandbox-task-executor:v0.1.0
```

## Image Registry

Built images are pushed to Docker Hub and Alibaba Cloud Container Registry (ACR). When `GHCR_REPO` is set, the same images are also pushed to GitHub Container Registry (GHCR).

### Docker Hub
- Controller: `opensandbox/controller:<tag>`
- Task Executor: `opensandbox/task-executor:<tag>`
- Image Committer: `opensandbox/image-committer:<tag>`

### Alibaba Cloud Container Registry (ACR)
- Controller: `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/controller:<tag>`
- Task Executor: `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/task-executor:<tag>`
- Image Committer: `sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/image-committer:<tag>`

### GitHub Container Registry (GHCR)
- Controller: `ghcr.io/<owner>/opensandbox/controller:<tag>`
- Task Executor: `ghcr.io/<owner>/opensandbox/task-executor:<tag>`
- Image Committer: `ghcr.io/<owner>/opensandbox/image-committer:<tag>`

## Multi-Architecture Support

The build script supports the following architectures by default:
- `linux/amd64`
- `linux/arm64`

To build for other architectures, modify the `PLATFORMS` variable in `build.sh`.

## Local Testing

To build an image for local testing without pushing:

```bash
# Build local image
COMPONENT=controller TAG=test PUSH=false ./build.sh

# Load into a Kind cluster for testing
kind load docker-image opensandbox-controller:test

# Or load into minikube for testing
minikube image load opensandbox-controller:test
```

## Troubleshooting

### Permission Issues

If you encounter Docker permission issues:
```bash
sudo usermod -aG docker $USER
newgrp docker
```

### Buildx Unavailable

Ensure Docker Buildx is enabled:
```bash
docker buildx create --use
docker buildx inspect --bootstrap
```

### Insufficient Disk Space

Clean up Docker cache:
```bash
docker system prune -a
docker builder prune -a
```

## Configuring a Private Image Registry

To use your own image registry, modify the registry address in `build.sh`:

```bash
# Edit build.sh
ACR_REPO="your-acr-registry.cr.aliyuncs.com/your-namespace"
```

Or specify the registry via environment variable at build time:
```bash
ACR_REPO=myregistry.com/myrepo COMPONENT=controller TAG=v0.1.0 ./build.sh
```

---

### Kubernetes Docs E2E TROUBLESHOOTING (`kubernetes/docs/E2E-TROUBLESHOOTING.md`)

# E2E Test Troubleshooting Guide

This document describes how to diagnose and resolve E2E test failures for the OpenSandbox Kubernetes component.

## E2E Test Overview

E2E tests are organized into three categories:

| Test Suite | Path | Run Command | Dependencies |
|------------|------|-------------|--------------|
| Core E2E | `test/e2e/` | `make test-e2e-main` | Kind + Docker |
| Task-Executor E2E | `test/e2e_task/` | included in `make test-e2e` | Docker |
| gVisor Runtime E2E | `test/e2e_runtime/gvisor/` | `make test-gvisor` | Kind + Docker + gVisor |

## General Troubleshooting Steps

### 1. Check the Test Failure Output

E2E tests use the Ginkgo framework. On failure, detailed assertion information is printed. Focus on:

- Failed `Eventually` assertions: a condition was not satisfied within the timeout
- Failed `Expect` assertions: an immediate condition was not met
- Debug information automatically collected in `AfterEach` (Core E2E only):
  - Controller manager pod logs
  - Kubernetes events
  - Pod describe output

### 2. Re-run a Single Failed Case

Use Ginkgo's focus mechanism to run only the failing case:

```bash
# Focus by description text
make test-e2e-main GINKGO_ARGS="-ginkgo.focus='Pool eviction'"

# Focus by regex
make test-e2e-main GINKGO_ARGS="-ginkgo.focus='should handle pod eviction'"
```

### 3. Preserve the Cluster for Manual Investigation

By default, E2E tests destroy the Kind cluster after completion. To preserve the cluster for manual debugging, create the cluster and deploy manually instead of using the full test runner:

```bash
# Manually create a Kind cluster
kind create cluster --name sandbox-k8s-test-e2e --image kindest/node:v1.22.4

# Install CRDs and deploy the controller
make install
make deploy CONTROLLER_IMG=<your-image>

# Manually apply the failing test data YAML
kubectl apply -f test/e2e/testdata/pool-basic.yaml

# Manually inspect resource state
kubectl get pools -A
kubectl get batchsandboxes -A
kubectl get pods -A
```

Clean up after investigation:

```bash
kind delete cluster --name sandbox-k8s-test-e2e
```

## Core E2E Common Issues

### Controller Pod Not Ready

**Symptom**: `Eventually(verifyControllerUp)` times out.

**Diagnosis**:

```bash
# Check pod status
kubectl get pods -n opensandbox-system -l control-plane=controller-manager

# View pod events
kubectl describe pod -n opensandbox-system -l control-plane=controller-manager

# View logs
kubectl logs -n opensandbox-system -l control-plane=controller-manager
```

**Common causes**:
- Image not loaded into the Kind cluster: run `kind load docker-image <image> --name sandbox-k8s-test-e2e`
- Image pull policy is `IfNotPresent` but the image does not exist
- Insufficient resource limits causing OOMKilled

### Pool Pods Stuck Not Running

**Symptom**: `Eventually` waiting for Pool Pod Running times out.

**Diagnosis**:

```bash
# Check pod status and reason
kubectl get pods -n <namespace> -l sandbox.opensandbox.io/pool-name=<pool-name>
kubectl describe pod <pod-name> -n <namespace>

# Common states
# - ImagePullBackOff: image pull failed
# - Pending: insufficient node resources
# - CrashLoopBackOff: container startup failed
```

**Common causes**:
- Sandbox image address is incorrect or cannot be pulled. Check the `utils.SandboxImage` variable and `{{.SandboxImage}}` in `testdata/*.yaml`
- Kind cluster node resources are insufficient (default Kind clusters have limited resources)

### BatchSandbox Allocation State Not as Expected

**Symptom**: `alloc-status` annotation is empty or the pod count is wrong.

**Diagnosis**:

```bash
# Check BatchSandbox allocation annotation
kubectl get batchsandbox <name> -n <namespace> -o jsonpath='{.metadata.annotations.sandbox\.opensandbox\.io/alloc-status}'

# Check Pool status
kubectl get pool <pool-name> -n <namespace> -o yaml

# Check controller logs for scheduling information
kubectl logs -n opensandbox-system -l control-plane=controller-manager | grep -i "schedule\|allocate\|insufficient"
```

**Common causes**:
- Insufficient available pods in the Pool. Check the `available` field in Pool status
- Allocation recovery failed. Search for "recovery" in controller startup logs
- Replica count exceeds PoolMax. Check the Pool's `capacitySpec.poolMax`

### Eventual Consistency Issues (Eventually Timeout but Manual Verification Passes)

**Symptom**: `Eventually` times out in the E2E test, but manually checking the resource state shows it already matches expectations.

**Common causes**:
- Default `EventuallyTimeout` is 2 minutes; complex scenarios may require longer
- Kind cluster performance is insufficient, causing controller processing delays
- Code was modified but the image was not rebuilt and reloaded into the Kind cluster

**Resolution**:

```bash
# Ensure the latest image is used
make docker-build-controller CONTROLLER_IMG=controller:dev
kind load docker-image controller:dev --name sandbox-k8s-test-e2e

# Or rebuild before testing
make test-e2e-main
```

## Task-Executor E2E Common Issues

### Docker Container Startup Failure

**Symptom**: Docker build or run command fails.

**Diagnosis**:

```bash
# Check if Docker is available
docker info

# Clean up leftover containers from previous runs
docker rm -f task-e2e-target task-e2e-executor
docker volume rm task-e2e-vol
```

### Task State Not as Expected

**Symptom**: Task creation or execution state is abnormal.

**Diagnosis**:

```bash
# Access the task-executor API directly (when container is running)
curl http://localhost:5758/tasks

# Check a specific task
curl http://localhost:5758/tasks/<task-name>

# View executor container logs
docker logs task-e2e-executor
```

### Process Not Visible in Sidecar Mode

**Symptom**: ProcessExecutor in sidecar mode cannot discover or manage the main container's processes.

**Diagnosis**:
- Confirm the `--pid=container:` parameter correctly points to the target container
- Confirm `--enable-sidecar-mode=true` is set
- Confirm `--main-container-name` matches the `SANDBOX_MAIN_CONTAINER` environment variable in the target container

## gVisor E2E Common Issues

### gVisor Runtime Not Installed

**Symptom**: Pod creation fails with RuntimeClass `gvisor` not found.

**Diagnosis**:

```bash
# Check RuntimeClass
kubectl get runtimeclass gvisor

# Reinstall
kubectl apply -f test/e2e_runtime/gvisor/testdata/runtimeclass.yaml
```

### runsc Binary Missing

**Symptom**: Pod is in `CreateContainerError` state with events indicating `runsc` not found.

**Diagnosis**:
- Confirm `make download-gvisor` has been executed
- Confirm `make setup-gvisor` has been executed and Kind nodes contain the runsc binary

## Environment Issues

### Stale Kind Clusters

If a previous test exited abnormally, stale Kind clusters may remain:

```bash
# List all Kind clusters
kind get clusters

# Delete stale clusters
kind delete cluster --name sandbox-k8s-test-e2e
kind delete cluster --name gvisor-test
```

### Docker Resource Exhaustion

```bash
# Check Docker disk usage
docker system df

# Clean up
docker system prune -a
docker builder prune -a
```

### Port Conflicts

Task-Executor E2E uses port 5758, Core E2E uses port 8081 (health probe). If ports are occupied:

```bash
# Check port usage
lsof -i :5758
lsof -i :8081

# Kill the occupying process
kill <PID>
```

## Useful Debug Commands

```bash
# View full controller logs
kubectl logs -n opensandbox-system -l control-plane=controller-manager -f

# View all OpenSandbox-related resources
kubectl get pools,batchsandboxes,pods -A

# View events for a specific resource
kubectl describe pool <pool-name> -n <namespace>
kubectl describe batchsandbox <sbx-name> -n <namespace>

# Verify CRDs are correctly installed
kubectl get crd batchsandboxes.sandbox.opensandbox.io -o yaml
kubectl get crd pools.sandbox.opensandbox.io -o yaml

# Check controller RBAC permissions
kubectl auth can-i --as=system:serviceaccount:opensandbox-system:opensandbox-controller-manager create pods
kubectl auth can-i --as=system:serviceaccount:opensandbox-system:opensandbox-controller-manager update batchsandboxes
```

---

### Kubernetes Docs Logging (`kubernetes/docs/logging.md`)

# Logging Configuration

## Features

The OpenSandbox Kubernetes Controller supports flexible logging configuration, including:

- Console log output (enabled by default)
- File log output (optional)
- Automatic log rotation (by file size)
- Automatic compression of old logs (gzip)
- Automatic cleanup of expired logs (by age or count)
- All standard zap options (log level, format, etc.)

## Command-Line Flags

### Log File Flags

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--enable-file-log` | bool | false | Enable log output to file |
| `--log-file-path` | string | `/var/log/sandbox-controller/controller.log` | Log file path |
| `--log-max-size` | int | 100 | Maximum size of a single log file in MB; rotates when exceeded |
| `--log-max-backups` | int | 10 | Maximum number of old log files to retain |
| `--log-max-age` | int | 30 | Maximum number of days to retain old log files |
| `--log-compress` | bool | true | Compress rotated log files (gzip) |

### Standard zap Flags (inherited from controller-runtime)

| Flag | Description |
|------|-------------|
| `--zap-devel` | Enable development mode (colorized output, more verbose stack traces) |
| `--zap-encoder` | Log encoding format: json or console |
| `--zap-log-level` | Log level: debug, info, error, etc. |
| `--zap-stacktrace-level` | Minimum log level at which stack traces are printed |
| `--zap-time-encoding` | Time encoding format: iso8601, millis, nano, etc. |

## Usage Examples

### 1. Console Output Only (Default)

```bash
./controller
```

### 2. Output to Both Console and File

```bash
./controller \
  --enable-file-log=true \
  --log-file-path=/var/log/sandbox-controller/controller.log
```

### 3. Custom Log Rotation Configuration

```bash
./controller \
  --enable-file-log=true \
  --log-file-path=/var/log/sandbox-controller/controller.log \
  --log-max-size=50 \
  --log-max-backups=5 \
  --log-max-age=7 \
  --log-compress=true
```

This configuration:
- Each log file can be up to 50MB
- Retain at most 5 old log files
- Retain log files for at most 7 days
- Compress old log files

### 4. Development Mode + File Output

```bash
./controller \
  --zap-devel=true \
  --enable-file-log=true \
  --log-file-path=/tmp/controller-dev.log
```

### 5. JSON Format + File Output

```bash
./controller \
  --zap-encoder=json \
  --enable-file-log=true \
  --log-file-path=/var/log/sandbox-controller/controller.log
```

### 6. Debug Level + File Output

```bash
./controller \
  --zap-log-level=debug \
  --enable-file-log=true \
  --log-file-path=/var/log/sandbox-controller/debug.log
```

## Kubernetes Deployment Configuration

When deploying in Kubernetes, configure logging options via the Deployment's `args`:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sandbox-controller
spec:
  template:
    spec:
      containers:
      - name: controller
        image: sandbox-controller:latest
        args:
        - --enable-file-log=true
        - --log-file-path=/var/log/controller/controller.log
        - --log-max-size=100
        - --log-max-backups=10
        - --log-max-age=30
        - --log-compress=true
        - --zap-encoder=json
        volumeMounts:
        - name: log-volume
          mountPath: /var/log/controller
      volumes:
      - name: log-volume
        emptyDir: {}
        # Or use a PersistentVolumeClaim
        # persistentVolumeClaim:
        #   claimName: controller-logs
```

## Log File Format

### Development Mode (--zap-devel=true)

```
2026-02-12T10:30:45.123+0800	INFO	setup	starting manager
2026-02-12T10:30:45.456+0800	INFO	controller	Reconciling	{"namespace": "default", "name": "example"}
```

### Production Mode (JSON)

```json
{"level":"info","ts":"2026-02-12T10:30:45.123+0800","logger":"setup","msg":"starting manager"}
{"level":"info","ts":"2026-02-12T10:30:45.456+0800","logger":"controller","msg":"Reconciling","namespace":"default","name":"example"}
```

## Log Rotation Mechanism

Log rotation is implemented by [lumberjack](https://github.com/natefinch/lumberjack) and supports:

1. **Size-based rotation**: When a log file reaches the size specified by `--log-max-size`, a new file is automatically created
2. **File naming**: Rotated files are named in the format `controller.log.2026-02-12T10-30-45.123`
3. **Automatic compression**: If `--log-compress` is enabled, old log files are compressed to `.gz` format
4. **Automatic cleanup**:
   - Retain the most recent N files based on `--log-max-backups`
   - Delete files older than the specified number of days based on `--log-max-age`

## Directory Permissions

Ensure the log directory exists and has write permissions:

```bash
# Create the log directory
mkdir -p /var/log/sandbox-controller

# Set permissions (adjust based on the actual runtime user)
chown controller:controller /var/log/sandbox-controller
chmod 755 /var/log/sandbox-controller
```

In Kubernetes, you can use an `initContainer` or `securityContext` to ensure correct permissions:

```yaml
spec:
  initContainers:
  - name: setup-log-dir
    image: busybox
    command: ['sh', '-c', 'mkdir -p /var/log/controller && chmod 755 /var/log/controller']
    volumeMounts:
    - name: log-volume
      mountPath: /var/log/controller
  containers:
  - name: controller
    securityContext:
      runAsUser: 1000
      runAsGroup: 1000
```

## Monitoring and Viewing Logs

### View Current Logs

```bash
tail -f /var/log/sandbox-controller/controller.log
```

### View Compressed Logs

```bash
zcat /var/log/sandbox-controller/controller.log.2026-02-12T10-30-45.123.gz | less
```

### Search Logs

```bash
# Search for error logs
grep -i error /var/log/sandbox-controller/controller.log

# Search across all log files (including compressed)
zgrep -i error /var/log/sandbox-controller/*.log*
```

## Best Practices

1. **Production environment**:
   ```bash
   --enable-file-log=true
   --log-file-path=/var/log/sandbox-controller/controller.log
   --log-max-size=100
   --log-max-backups=10
   --log-max-age=30
   --log-compress=true
   --zap-encoder=json
   ```

2. **Development environment**:
   ```bash
   --zap-devel=true
   --enable-file-log=true
   --log-file-path=/tmp/controller-dev.log
   --log-compress=false
   ```

3. **Debugging issues**:
   ```bash
   --zap-log-level=debug
   --enable-file-log=true
   --log-max-size=500
   --log-compress=false
   ```

4. **Limited disk space**:
   ```bash
   --enable-file-log=true
   --log-max-size=50
   --log-max-backups=3
   --log-max-age=7
   --log-compress=true
   ```

## Troubleshooting

### Log File Not Created

1. Check if the directory exists: `ls -la /var/log/sandbox-controller/`
2. Check permissions: `ls -ld /var/log/sandbox-controller/`
3. Verify the process has write permissions
4. Check controller startup logs for errors

### Log File Not Rotating

1. Confirm `--enable-file-log=true` is set
2. Check if the file size has reached the `--log-max-size` limit
3. Verify the lumberjack library is correctly installed: `go list -m gopkg.in/natefinch/lumberjack.v2`

### Excessive Disk Space Usage

1. Reduce the value of `--log-max-size`
2. Reduce the number of `--log-max-backups`
3. Reduce the number of days in `--log-max-age`
4. Ensure `--log-compress=true` is enabled

---

### Kubernetes Docs Telemetry (`kubernetes/docs/telemetry.md`)

# Controller Telemetry (OpenTelemetry)

This page has moved to the unified telemetry reference: [docs/guides/telemetry.md](../../docs/guides/telemetry.md) (Kubernetes controller section, published on the docs site at `/guides/telemetry#kubernetes-controller`).

---

### Components Egress Docs Benchmark (`components/egress/docs/benchmark.md`)

# Egress benchmark

**Prerequisites**: Docker, `curl` on the host. Domains: [`tests/hostname.txt`](../tests/hostname.txt) (one hostname per line; `#` and blank lines ignored). Run from `components/egress` or adjust paths.

---

## 1. `bench-dns-nft.sh`

**Compares**: plain **`curl`** container (**baseline**) → egress **`dns`** → egress **`dns+nft`**. Prints **Req/s**, **Avg**, **P50**, **P99**; percentages are vs **baseline**.

### Run

```bash
cd components/egress
./tests/bench-dns-nft.sh
```

Builds `opensandbox/egress:local` unless you set **`IMG=...`**. Optional: **`BENCH_SAMPLE_SIZE=n`** to use `n` random domains.

### View results

- **Terminal**: summary table at end.
- **Host `/tmp`**: `bench-e2e-baseline-total.txt`, `bench-e2e-dns-total.txt`, `bench-e2e-dns+nft-total.txt` (one **`time_total`** per line); `bench-e2e-{mode}-namelookup.txt`, `bench-e2e-{mode}-wall.txt`.

---

## 2. `bench-mitm-overhead.sh`

**Compares**: **`dns+nft`** without MITM vs **`dns+nft` + transparent mitmproxy**. Default **`BENCH_SCENARIOS=short,download`** — **`short`** = many HTTPS **HEAD**s; **`download`** = parallel **GET** to **`BENCH_DOWNLOAD_URL`** (default Cloudflare `__down` ~20 MiB).

### Run

```bash
cd components/egress
./tests/bench-mitm-overhead.sh
```

**`SKIP_BUILD=1`** skips image build; **`IMG`** is at the top of the script. One scenario only, e.g. **`BENCH_SCENARIOS=short`** or **`=download`**.

### View results

- **Terminal**: tables per scenario (latency / throughput vs no-MITM), plus **`E2E latency loss (avg time_total)`** in **ms/request** and **%**.
- **Host `/tmp`**:
  - Latency artifacts: `bench-mitm-*-short-*.txt`, `*-download-*.tsv`, `*-wall.txt`, etc.
  - **Container metrics** (always written): `bench-mitm-docker-stats-dns_nft.tsv`, `bench-mitm-docker-stats-dns_nft_mitm.tsv` — `unix_ts`, **`/proc/loadavg`** (load1/5/15, …), **`docker stats`** (CPUPerc, MemUsage, …). *`loadavg` inside the container often tracks the host; use for relative trends.*

---

## 3. Reference baselines (example runs)

Illustrative only — **same machine, same script**, not a SLA. **MITM** row = **`dns+nft` + transparent mitm**.

### `BENCH_SCENARIOS=download` (parallel GET, ~20 MiB, 4 streams, 1 round, 1 s sampling)

| Metric | `dns+nft` | + mitm |
|--------|-----------|--------|
| **CPUPerc** (docker) | Mostly **~2–5%**, max **~5.6%** | Often **~5–11%**, max **~10.9%** |
| **MemUsage** | **~9–18 MiB** | **~68–91 MiB** |
| **load1** | Up to **~0.23** | Spike **~0.66**, then **~0.4–0.6** |

**Takeaway**: ~**2×** peak CPU% and ~**5×** RSS vs no MITM in this trace.

### `BENCH_SCENARIOS=short` (HEAD storm; **sparse** rows if the phase is short)

Run profile (sample): `10 rounds × 40 URLs × 1 inflight = 400 requests`.

| Metric | `dns+nft` | + mitm |
|--------|-----------|--------|
| **Req/s** | **3.64** | **1.90** (**-47.6%**) |
| **Avg latency (time_total)** | **0.315 s** | **0.605 s** (**+91.9%**) |
| **P50 latency** | **0.136 s** | **0.143 s** (**+5.2%**) |
| **P99 latency** | **1.439 s** | **10.006 s** (**+595.2%**) |
| **E2E latency loss (avg)** | baseline | **+289.88 ms/request (+91.95%)** |

| Metric | `dns+nft` | + mitm |
|--------|-----------|--------|
| **CPUPerc** | Hot sample **~132%** | Hot sample **~232%** |
| **MemUsage** | **~6–10 MiB** | **~58–88 MiB** |

**`CPUPerc` > 100%** on multi-core is normal (container can use more than one core-equivalent per Docker’s metric).

**Takeaway**: this sample shows clear request-side overhead from transparent MITM, about **+289.88 ms/request** on average with throughput dropping to about half. `P50` is close to baseline while `P99` grows sharply, indicating tail-latency amplification. With only **40 requests**, tail metrics are timing-sensitive; use more rounds or more domains for stable P99.

CPU/memory trend remains consistent: peak CPU sample **~1.8×** (**232/132**), and RSS is much higher with mitmdump. For denser host/container telemetry, use longer runs or **`BENCH_DOCKER_STATS_INTERVAL=0.5`**.

---

### Components Egress Docs Fast Sandbox Docs (`components/egress/docs/fast-sandbox-docs.md`)

# Fast Sandbox documentation

The fast-sandbox MITM data plane and the firecracker e2e guides live in the docs site (source of truth):

- [docs/components/egress-fast-sandbox-mitm-data-plane.md](../../../docs/components/egress-fast-sandbox-mitm-data-plane.md)
- [docs/components/egress-fast-sandbox-firecracker-e2e.md](../../../docs/components/egress-fast-sandbox-firecracker-e2e.md)

This directory keeps component-internal notes only (e.g. the local uncommitted work tracker).

---

### Components Egress Docs Live Vault Acceptance (`components/egress/docs/live-vault-acceptance.md`)

# OSEP-0023 live credential-bound Vault loop: acceptance

Moved to [docs/guides/live-vault-acceptance.md](../../../docs/guides/live-vault-acceptance.md).

---

--- METRICS ---
- Files Extracted: 101
- Estimated Token Budget: ~259064 tokens
- Canonical Reference: https://codewiki.google/github.com/alibaba/OpenSandbox
