sand-boxer/docs/extension-sdk.md
tegwick d79e3fe358 Add owner-mediated bwrap execution boundary
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a06def-6490-7033-8448-2eab2d12ed44
2026-09-04 22:12:19 +02:00

3.9 KiB

Extension SDK

Author guide for sand-boxer backend adapters. Version 0.1 — SAND-WP-0005.

Contract

Every extension implements three methods:

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). 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.

Base class

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.<name>.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
  1. Add a profile binding in profiles/profile.<slug>.yaml with extension: ext.my-backend.
  2. 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:

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