# 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)` ### 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 on pre-built VM | | `ext.saas-stub` | `saas_stub.py` | Metered stub + metadata snapshots | | `ext.e2b` | `e2b.py` | E2B cloud adapter | | `ext.modal` | `modal.py` | Modal cloud adapter | ## 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 | `vm` or `ssh_target` | `repo`, `tunnel_port`, `ssh_port`, `workspace_dir` | 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 |