# Extension SDK Author guide for sand-boxer backend adapters. Version 0.1 — SAND-WP-0005. ## Contract Every extension implements three methods: ```text provision(profile, inputs, host) → handle dict wait_ready(handle) → reachability dict teardown(handle) → cleanup report dict ``` Optional (SaaS): `estimate_cost(profile, duration) → MeterQuote` Optional (checkpoints): `supports_snapshots()`, `snapshot(handle)`, `restore_from_snapshot(profile, snapshot_meta, inputs, host)` Optional (owner-mediated execution): `supports_execution()`, then `execute(handle, command, credential_route_refs, execution_context, timeout_seconds, max_output_bytes, stdin_text)`. The default implementation fails closed. `ext.bwrap` is the reference implementation. An executing extension must validate that its workspace belongs to the exact sandbox handle, sanitize the child environment, use an argument vector, enforce the requested bounds, and must not fall back to a host workspace. Bounded stdin is content-bearing and must never be included in lifecycle logs or execution evidence. ### Base class ```python from sandboxer.extensions.base import SandboxExtension class MyExtension(SandboxExtension): def provision(self, profile, inputs, host): ... def wait_ready(self, handle): ... def teardown(self, handle): ... ``` Reference implementations: | Extension | Module | Mode | |-----------|--------|------| | `ext.compose-ssh` | `compose_ssh.py` | Remote compose stack + tar snapshots | | `ext.vm-packer` | `vm_packer.py` | Attach workspace or Packer build mode | | `ext.saas-stub` | `saas_stub.py` | Metered stub + metadata snapshots | | `ext.e2b` | `e2b.py` | E2B cloud adapter | | `ext.modal` | `modal.py` | Modal cloud adapter | | `ext.bwrap` | `bwrap.py` | Local namespaces + owner-mediated execution | ## Registration 1. Add `extensions/ext..yaml`: ```yaml id: ext.my-backend title: My Backend handler: sandboxer.extensions.my_backend:MyExtension capabilities: isolation_levels: [container] pricing_model: self-hosted config: key: value ``` 2. Add a profile binding in `profiles/profile..yaml` with `extension: ext.my-backend`. 3. Register capability metadata in `registry/` when ready for reuse-surface. Loader validates `capabilities.isolation_levels` and `capabilities.pricing_model` at startup (`sandboxer.extensions.registry`). ## Handle and reachability **Handle** (returned by `provision`, stored in manager): opaque dict passed to `wait_ready` and `teardown`. Include at minimum: - `sandbox_id` - `host` (placement host) - Fields your extension needs for SSH/API (e.g. `remote_dir`, `vm_target`) **Reachability** (returned by `wait_ready`): exposed on `SandboxStatus.reachability`: - `ssh` — SSH destination string - `remote_dir` — workspace path on remote - `host` — placement host - `compose_project` — compose-ssh only ## Inputs convention Profiles declare semantics; extensions validate required `inputs` keys: | Extension | Required inputs | Optional | |-----------|-----------------|----------| | compose-ssh | `repo` | `sandbox_id` | | vm-packer (attach) | `vm` or `ssh_target` | `repo`, `tunnel_port`, `ssh_port`, `workspace_dir` | | vm-packer (build) | `packer_template`, `vm_name` | `mode=build`, `packer_var_*` | Consumer attribution travels on `SandboxCreateRequest.consumer`, not extension inputs. ## Testing Mock SSH/subprocess in unit tests. See `tests/test_compose_ssh.py`, `tests/test_vm_packer.py`. Pattern: ```python with patch.object(SSHConfig, "run", return_value=(0, "ready")): ext = VMPackerExtension() handle = ext.provision(profile, {"vm": "haskell-build"}, "localhost") ``` ## Metered extensions (SAND-WP-0006) Implement `estimate_cost` and `meter_actual` on `SandboxExtension`. Register with `pricing_model: metered`. See `docs/payments.md` and `ext.saas-stub`. ## Deferred | Feature | Workplan | |---------|----------| | Packer build orchestration from `create` | Future WP | | Daytona OSS cloud adapter | Future WP | | fin-hub billing export | Future | | Cross-host snapshot transfer | Future |