Metadata-Version: 2.4
Name: kanyun-sandbox
Version: 0.3.2
Summary: Python SDK for Kanyun Sandbox v2 control plane
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: httpx<1,>=0.27.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"

# Kanyun Sandbox Python SDK

Python SDK for the Kanyun Sandbox v2 control plane.

## Install

```bash
pip install kanyun-sandbox
```

- Package name: `kanyun-sandbox`
- Import: `kanyun_sandbox`

## Quick Start

```python
from kanyun_sandbox import Client, CreateSandboxRequest

client = Client(
    control_plane_url="http://127.0.0.1:8080",
    api_key="kanyun_xxx",
)

# Create a sandbox from an existing template
handle = client.create_sandbox(CreateSandboxRequest(
    template_name="my-devbox",
    ttl_seconds=3600,
))

print(f"Sandbox: {handle.info.claim_name}")
print(f"Status:  {handle.info.status}")

# When done
handle.destroy()
```

## Typical Workflow

A common usage flow looks like this:

```
准备镜像 → 创建模板 → (可选) 创建预热池 → 创建 Sandbox → 使用 → 销毁
```

### Step 1: Create a Template

You need a container image first (built by yourself or via the platform's image build system). Then create a template that references it:

```python
template = client.apply_template("my-devbox", {
    "runtimeProfileId": "rp-default",
    "displayName": "My Dev Box",
    "description": "Custom development environment",
    "defaultTTLSeconds": 3600,
    "maxTTLSeconds": 86400,
    "podTemplate": {
        "containers": [{
            "name": "main",
            "image": "registry.example.com/my-org/my-devbox:latest",
            "resources": {
                "requests": {"cpu": "1", "memory": "2Gi"},
                "limits": {"cpu": "2", "memory": "4Gi"},
            },
        }],
    },
})
print(f"Template created: {template.name}")
```

### Step 2: Create a Warm Pool (Optional)

If you need sandboxes to start quickly, pre-warm a pool of standby instances:

```python
warm_pool = client.apply_warm_pool("my-devbox-pool", {
    "templateName": "my-devbox",
    "desiredReplicas": 3,
})
print(f"Warm pool: {warm_pool.name}, ready: {warm_pool.ready_replicas}/{warm_pool.desired_replicas}")
```

### Step 3: Create a Sandbox

```python
handle = client.create_sandbox(CreateSandboxRequest(
    template_name="my-devbox",
    ttl_seconds=3600,
    metadata={"user": "alice", "purpose": "code-review"},
))

print(f"Sandbox: {handle.info.claim_name}")
print(f"IP:      {handle.info.ip}")
print(f"Expires: {handle.info.expires_at}")
```

### Step 4: Manage Sandbox Lifecycle

```python
# Extend TTL by another 30 minutes
handle.extend(1800)

# Refresh to get latest status
handle.refresh()
print(f"Status: {handle.info.status}")

# Get detailed info (includes metadata)
detail = handle.detail()
print(f"Metadata: {detail.metadata}")

# List events
events = handle.events()
for event in events:
    print(f"  {event.event_type} at {event.occurred_at}")

# Destroy when done
handle.destroy()
```

## Client Options

```python
client = Client(
    control_plane_url="http://127.0.0.1:8080",
    api_key="kanyun_xxx",
    agent_port=8080,            # Agent service port (default: 8080)
    agent_client_factory=None,  # Optional, see "Agent Access" section
    timeout=60.0,               # HTTP timeout in seconds
)
```

## API Reference

### Sandbox

| Method | Description |
|--------|-------------|
| `create_sandbox(request)` | Create a sandbox, returns `SandboxHandle` |
| `get_sandbox(id)` | Get sandbox info by ID |
| `list_sandboxes()` | List all running sandboxes |
| `get_sandbox_detail(id)` | Get detailed sandbox info (includes metadata) |
| `list_sandbox_events(id)` | List sandbox lifecycle events |
| `extend_sandbox(id, ttl_seconds)` | Extend a running sandbox's TTL |
| `delete_sandbox(id)` | Delete a sandbox |
| `connect_sandbox(id)` | Connect to an existing sandbox, returns `SandboxHandle` |

### Template

| Method | Description |
|--------|-------------|
| `list_templates()` | List all templates |
| `get_template(name)` | Get template by name |
| `apply_template(name, request)` | Create or update a template |
| `delete_template(name)` | Delete a template |
| `list_template_materializations(name)` | List template materializations across clusters |
| `resync_template_materialization(name)` | Trigger re-sync of a template materialization |

### Warm Pool

| Method | Description |
|--------|-------------|
| `list_warm_pools()` | List all warm pools |
| `get_warm_pool(name)` | Get warm pool by name |
| `apply_warm_pool(name, request)` | Create or update a warm pool |
| `delete_warm_pool(name)` | Delete a warm pool |

### SandboxHandle

| Method | Description |
|--------|-------------|
| `handle.info` | Current `SandboxInfo` snapshot |
| `handle.extend(ttl_seconds)` | Extend sandbox TTL from now |
| `handle.refresh()` | Refresh sandbox info from server |
| `handle.detail()` | Get sandbox detail |
| `handle.events()` | List sandbox events |
| `handle.destroy()` | Delete the sandbox |

### Session (Context Manager)

```python
with client.run_session(template_name="my-devbox", ttl_seconds=600) as session:
    print(session.info.ip)
    # sandbox auto-destroyed on exit
```

### History

| Method | Description |
|--------|-------------|
| `list_sandbox_history(page, page_size, status, template_name)` | Query sandbox history |
| `get_sandbox_history(id)` | Get historical sandbox detail with events |

### Platform Profiles

| Method | Description |
|--------|-------------|
| `list_clusters()` / `get_cluster(id)` / `create_cluster(req)` / `upsert_cluster(id, req)` / `delete_cluster(id)` | Cluster management |
| `list_runtime_profiles()` / `get_runtime_profile(id)` / `create_runtime_profile(req)` / `upsert_runtime_profile(id, req)` / `delete_runtime_profile(id)` | Runtime profile management |
| `list_build_profiles()` / `get_build_profile(id)` / `create_build_profile(req)` / `upsert_build_profile(id, req)` / `delete_build_profile(id)` | Build profile management |
| `list_registry_profiles()` / `get_registry_profile(id)` / `create_registry_profile(req)` / `upsert_registry_profile(id, req)` / `delete_registry_profile(id)` | Registry profile management |
| `list_secret_profiles()` / `get_secret_profile(id)` / `create_secret_profile(req)` / `upsert_secret_profile(id, req)` / `delete_secret_profile(id)` | Secret profile management |
| `list_secret_materializations(profile_id)` / `resync_secret_materialization(profile_id, mat_id)` / `rotate_secret_profile(profile_id, req)` | Secret materialization |

### Image Build

| Method | Description |
|--------|-------------|
| `create_image_build(req)` / `list_image_builds()` / `get_image_build(id)` | Image build management |
| `list_image_build_logs(build_id)` / `cancel_image_build(build_id)` | Build logs and cancellation |
| `list_image_assets()` / `create_image_asset(req)` / `get_image_asset(id)` / `upsert_image_asset(id, req)` / `delete_image_asset(id)` | Reusable image assets |

## Agent Access (Optional)

By default, the SDK only communicates with the control plane. If your sandbox image runs a compatible agent service (e.g., the AIO agent), you can opt into in-sandbox command execution by providing an `agent_client_factory`.

```python
from agent_sandbox import Sandbox
from kanyun_sandbox import Client, CreateSandboxRequest

client = Client(
    control_plane_url="http://127.0.0.1:8080",
    api_key="kanyun_xxx",
    agent_client_factory=lambda base_url: Sandbox(base_url=base_url),
)

handle = client.create_sandbox(CreateSandboxRequest(
    template_name="aio-devbox",  # Must be an image with agent service
    ttl_seconds=3600,
))

# Access the agent client
agent = handle.agent
result = agent.shell.exec_command(command="echo hello")
print(result)

handle.destroy()
```

**Important:**

- Agent access requires a specific sandbox image that runs the agent protocol (e.g., AIO images).
- The `agent_client_factory` receives the sandbox's internal `http://{ip}:{agent_port}` URL and returns a client instance.
- `handle.agent` is created lazily — sandbox creation and control-plane operations never require agent support.
- Accessing `.agent` without a factory raises `SandboxAgentUnavailableError`.
- Accessing `.agent` before the sandbox has an IP raises `SandboxNotReadyError`.

## Error Handling

| Error | Cause |
|-------|-------|
| `ValidationError` | Invalid request (400) |
| `AuthenticationError` | API key invalid (401) |
| `NotFoundError` | Resource not found (404) |
| `ConflictError` | State conflict (409) |
| `APIError` | Other control-plane errors |
| `SandboxAgentUnavailableError` | `.agent` accessed without `agent_client_factory` |
| `SandboxNotReadyError` | `.agent` accessed before sandbox has an IP |

## Authentication

All control-plane requests use `X-API-Key` header.
