Capsule SDK
The Capsule SDK is the recommended client surface for registering workloads, triggering builds, allocating runners, and interacting with running Capsule sandboxes from Python.
Requirements
- Python
>= 3.10 - access to a running Capsule control plane
- a GCP KMS attestation key for authenticated requests (auto-derived from tenant ID by default)
Installation
pip install capsule-sdk
For local development:
cd sdk/python
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
Configuration
The SDK can be configured directly in code or through environment variables.
| Parameter | Env var | Default |
|---|---|---|
control_plane_addr |
CAPSULE_CONTROL_PLANE_ADDR |
http://localhost:8080 |
kms_key_name |
CAPSULE_KMS_KEY_NAME |
auto-derived from tenant_id |
request_timeout |
CAPSULE_REQUEST_TIMEOUT |
30.0 |
startup_timeout |
CAPSULE_STARTUP_TIMEOUT |
45.0 |
operation_timeout |
CAPSULE_OPERATION_TIMEOUT |
120.0 |
Example:
export CAPSULE_CONTROL_PLANE_ADDR="http://localhost:8080"
# KMS key name is auto-derived from tenant_id if not set:
# export CAPSULE_KMS_KEY_NAME="projects/{tenant}/locations/global/keyRings/capsule/cryptoKeys/capsule-attestation/cryptoKeyVersions/1"
Quickstart
The fastest way to get started is the high-level workloads API.
from capsule_sdk import CapsuleClient, RunnerConfig
cfg = (
RunnerConfig("My dev sandbox")
.with_base_image("ubuntu:22.04")
.with_commands(["apt-get update", "apt-get install -y python3"])
.with_tier("m")
.with_ttl(3600)
.with_auto_pause(True)
.with_auto_rollout(True)
)
with CapsuleClient(control_plane_addr="http://localhost:8080", tenant_id="my-tenant") as client:
workload = client.workloads.onboard(cfg)
with client.workloads.start(workload) as runner:
output, code = runner.exec_collect("python3", "-c", "print('hello')")
print(output, code)
runner.write_text("/workspace/hello.txt", "hello")
print(runner.read_text("/workspace/hello.txt"))
Onboard From YAML
You can also onboard directly from an onboard.yaml-style file:
from capsule_sdk import CapsuleClient
with CapsuleClient(control_plane_addr="http://localhost:8080", tenant_id="my-tenant") as client:
workload = client.workloads.onboard_yaml(
"examples/afs/onboard.yaml",
name="afs-sandbox",
)
with client.workloads.start("afs-sandbox") as runner:
print(runner.read_text("/etc/hostname"))
The AFS example is an example workload name, not a special SDK mode. See
examples/afs/ for the underlying config shape.
Async Quickstart
Use the async client in event-loop-native applications:
import asyncio
from capsule_sdk import AsyncCapsuleClient, RunnerConfig
async def main() -> None:
cfg = (
RunnerConfig("My async sandbox")
.with_base_image("ubuntu:22.04")
.with_commands(["echo async-ready"])
.with_tier("m")
.with_ttl(3600)
.with_auto_pause(True)
)
async with AsyncCapsuleClient(control_plane_addr="http://localhost:8080", tenant_id="my-tenant") as client:
workload = await client.workloads.onboard(cfg)
runner = await client.workloads.start(workload)
async with runner:
result = await runner.exec_collect("sh", "-lc", "printf hello")
print(result.stdout, result.exit_code)
asyncio.run(main())
Low-Level APIs
For finer control, work directly with the resource clients:
from capsule_sdk import CapsuleClient
with CapsuleClient(control_plane_addr="http://localhost:8080", tenant_id="my-tenant") as client:
with client.runners.allocate_ready("my-workload-key") as runner:
for event in runner.exec("echo", "hello"):
if event.type == "stdout":
print(event.data, end="")
Key low-level surfaces:
client.runnersclient.workloadsclient.snapshotsclient.runner_configs
Multi-Tenant Builder Config
When running builds in a tenant's own GCP project, set the tenant GCE config
on RunnerConfig. Builds will launch in the tenant's project while pulling
the base builder image from central.
cfg = (
RunnerConfig("my-sandbox")
.with_base_image("ubuntu:22.04")
.with_commands(["pip install -e ."])
.with_tier("m")
.with_tenant_gce_config(
project="tenant-project-123",
zone="us-central1-a",
network="projects/tenant-project-123/global/networks/default",
subnet="projects/tenant-project-123/regions/us-central1/subnetworks/default",
service_account="builder@tenant-project-123.iam.gserviceaccount.com",
)
)
Individual fields can also be set separately:
cfg = (
RunnerConfig("my-sandbox")
.with_tenant_gcp_project("tenant-project-123")
.with_tenant_gcp_zone("us-central1-a")
)
Two tenants can register the same display name without conflict — the server qualifies the config ID with the tenant project automatically.
Credential Broker Proxy
To route runner traffic through the credential broker, register a sandbox with
QE to obtain a sandbox_token, then pass it as proxy_token during allocation.
The control plane resolves the per-tenant proxy address from the
TF-managed capsule-credential-broker-addrs ConfigMap (mounted at
/etc/capsule/broker_addrs.json, reloaded ~1 min).
with CapsuleClient() as client:
runner = client.runners.allocate_ready(
"my-workload-key",
session_id="session-abc",
proxy_token=sandbox_token,
)
This configures the runner to:
- Route all egress through the tenant's broker proxy (deny-all except proxy IP)
- Fetch the proxy CA certificate from Secret Manager
- Use
proxy_tokenas the basic-auth username in proxy requests
The same parameters are available on allocate(), allocate_ready(),
from_config(), and workloads.start() / workloads.allocate().
Key Concepts
| SDK concept | Server primitive | Description |
|---|---|---|
RunnerConfig |
LayeredConfig |
Declarative workload shape |
workloads.onboard() |
create + build | Register a workload from Python or YAML |
workloads.start() |
allocate + wait | Start a ready runner by workload name |
runners.allocate_ready() |
/runners/allocate |
Allocate and wait for a usable runner |
RunnerSession |
runner handle | High-level exec, file, shell, pause, and resume API |
| Host VM I/O | /api/v1/sessions/{session_id}/... |
Low-level Runners.file_*, exec, and shell use this path prefix on the host; the host maps session_id to the runner |
| Control-plane lifecycle | /runners/status, /pause, /release |
runners.status, pause, and release accept either runner_id= or session_id= |
Runner and session IDs
- After allocation, prefer
session_idfor anything that hits the host HTTP API directly:Runners.file_download,file_upload,file_read,file_write,file_list,file_stat,file_remove,file_mkdir,exec, andshelltakesession_idas the first argument (or embed it in the URL). Those requests go to paths such as/api/v1/sessions/{session_id}/files/...or/api/v1/sessions/{session_id}/execon the host address returned by the control plane. The legacy/api/v1/runners/{runner_id}/...paths remain available for callers that key offrunner_idonly. - On the control plane,
runners.status,runners.release, andrunners.pauseacceptrunner_id=orsession_id=(exactly one). The SDK caches the host address bysession_idwhen the allocate/status response includes one.
RunnerConfig Builder Methods
| Method | Field | Description |
|---|---|---|
with_base_image(img) |
base_image |
Docker image URI for layer 0 |
with_commands(cmds) |
layers[0].init_commands |
Shell commands for the main layer |
with_layers(layers) |
layers |
Full multi-layer definitions |
with_tier(tier) |
config.tier |
VM size tier (s, m, l) |
with_ttl(secs) |
config.ttl |
Runner time-to-live in seconds |
with_auto_pause(bool) |
config.auto_pause |
Auto-pause idle runners |
with_auto_rollout(bool) |
config.auto_rollout |
Auto-rollout new builds |
with_session_max_age(secs) |
config.session_max_age_seconds |
Max session age |
with_rootfs_size_gb(gb) |
config.rootfs_size_gb |
Root filesystem size |
with_workspace_size_gb(gb) |
config.workspace_size_gb |
Workspace drive size |
with_runner_user(user) |
config.runner_user |
Non-root user for commands |
with_network_policy_preset(p) |
config.network_policy_preset |
Named network policy |
with_network_policy(policy) |
config.network_policy |
Custom network policy JSON |
with_start_command(cmd) |
start_command |
Long-running service command |
with_auth(auth) |
config.auth |
Auth/proxy config |
with_tenant_gce_config(...) |
config.tenant_* |
All tenant GCE fields at once |
with_tenant_gcp_project(p) |
config.tenant_gcp_project |
Tenant GCP project for builds |
with_tenant_gcp_zone(z) |
config.tenant_gcp_zone |
Tenant GCE zone |
with_tenant_network(n) |
config.tenant_network |
Tenant VPC network |
with_tenant_subnet(s) |
config.tenant_subnet |
Tenant VPC subnet |
with_tenant_service_account(sa) |
config.tenant_service_account |
Tenant builder SA |
Retry And Timeout Behavior
request_timeoutapplies to a single HTTP requeststartup_timeoutcovers "get me a usable runner"operation_timeoutapplies to host-side file, PTY, and stream operationsallocate()retries transient control-plane and capacity errors untilstartup_timeoutworkloads.start()is the preferred high-level path for named workloadsfrom_config()waits for runner readiness by default; usewait_ready=Falsefor lower-level control
Host Reconnection
The SDK caches host addresses returned by allocate() and connect(), keyed
by session_id when known (otherwise by runner_id). If a host proxy
becomes unavailable during a safe retryable operation, the SDK will refresh the
host via connect() and retry once when possible.
Live End-To-End Test
The repository includes an explicit live SDK E2E at sdk/python/tests/e2e_live.py.
It exercises config registration, build enqueue, allocation, exec, file ops,
PTY, pause/resume, release, and config cleanup against a real control plane.
Run it with:
make sdk-python-e2e
If you are not using the default address:
CAPSULE_BASE_URL="http://localhost:8080" make sdk-python-e2e
Development Checks
python -m ruff check src/capsule_sdk/ tests/
python -m ty check
python -m pytest tests/ -v --ignore=tests/e2e_live.py --ignore=tests/e2e_live_async.py
For contract tests against a live control plane:
CAPSULE_BASE_URL=http://localhost:8080 CAPSULE_TENANT_ID=test-tenant \
python -m pytest tests/test_contract.py -v -m contract
Release files for capsule-sdk 0.4.17
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| capsule_sdk-0.4.17.tar.gz | 58.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| capsule_sdk-0.4.17-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 116.5 kB
Release files / capsule_sdk-0.4.17.tar.gz
| Download URL | capsule_sdk-0.4.17.tar.gz |
|---|---|
| Size | 58.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
55f92c0ed6e36cf89b2905281672ef130331b032fcd5532c0ef141084e7791a3
|
|
BLAKE2b-256 checksum How to use checksums |
40350e5c48247fc7ffd9683e517a4dda1bbfdbf7aeb288d1acf98b1996039d5c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 2, 2026.
Transparency logRelease files / capsule_sdk-0.4.17-py3-none-any.whl
| Download URL | capsule_sdk-0.4.17-py3-none-any.whl |
|---|---|
| Size | 57.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f9c914bee6d31bd6c03891fb576413d71810ee4a5e92f343c2baa2a2fe5ef3d8
|
|
BLAKE2b-256 checksum How to use checksums |
16887324ad833c2d3dd47ef6a810f1c9846d99ba6a437cf2e555421f4b13d1c9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 2, 2026.
Transparency log