03 — Public API¶
Design target: autocomplete-driven and AI-legible. A coding assistant reading only the type signatures should produce correct sandboxio code.
Module surface¶
sandboxio.create(...) # async, returns AsyncSandbox
sandboxio.create_sync(...) # sync facade
sandboxio.connect(...) # async, by sandbox id
sandboxio.register(name, path) # runtime backend registration
sandboxio.doctor() # programmatic environment diagnosis -> DoctorReport
sandboxio.Capability, sandboxio.IsolationTier, sandboxio.Resources, sandboxio.NetworkPolicy, sandboxio.ExecResult
sandboxio.errors.* # full error tree, also re-exported at top level
sandboxio.testing.* # FakeBackend, fixtures, BackendContractSuite
Importing sandboxio MUST NOT import any adapter or provider SDK
(ADR-0004).
create()¶
async def create(
target: str | BackendConfig | None = None, # DSN, typed config, or None → local Docker
*,
template: str | None = None,
resources: Resources = Resources(),
network: NetworkPolicy = NetworkPolicy(), # egress="deny" by default
env: dict[str, str] | None = None,
secrets: dict[str, str] | None = None, # separate from env; redacted everywhere
timeout: float = 300, # required cap; unbounded is refused
metadata: dict[str, str] | None = None, # tenant_id, session_id, run_id, …
require_isolation: IsolationTier | None = None,
) -> AsyncSandbox: ...
target=NoneMUST resolve to local Docker and MUST work with no API key, no account and no config file.- Backend names appearing as plain identifiers MUST be
Literal["docker","e2b","modal","fake"]. require_isolationMUST be checked before provisioning, raisingConfigurationErrorif the resolved backend is weaker (05).timeoutMUST NOT acceptNoneor a non-positive value.
Canonical usage¶
import sandboxio
async with await sandboxio.create() as sb: # zero-config, local Docker
res = await sb.run_code("print('hello')")
print(res.stdout)
sb = await sandboxio.create("docker://python:3.12-slim") # one-line backend swap
sb = await sandboxio.create("e2b://code-interpreter")
sb = await sandboxio.create("modal://base?gpu=T4") # v0.1.1 — not installable yet
res = await sb.run(["pytest", "-q"], timeout=120)
res = await sb.run_code("import pandas; print(pandas.__version__)")
await sb.files.upload("model.pkl", "/work/model.pkl")
data = await sb.files.read("/work/out.json")
async with sb.stream(["pytest", "-q"], timeout=300) as proc: # async with is required
async for chunk in proc:
log.write(chunk.data) # OutputChunk(stream=..., data=bytes)
res = await proc.wait() # res.streamed is True; stdout/stderr empty
try:
await sb.run("sleep 999", timeout=5)
except sandboxio.SandboxTimeout: # NOT builtin TimeoutError — see spec/04
await sb.kill()
if sandboxio.Capability.GPU in sb.capabilities: ...
if sb.isolation is not sandboxio.IsolationTier.MICROVM:
log.warning("weaker than microVM isolation for untrusted multi-tenant code")
sb.native.tunnels() # Modal-specific (v0.1.1) — outside the semver contract
The streaming
pip installexample from the input docs is removed: it cannot run under the default deny-egress policy, and Docker cannot express an allowlist to make it work (ADR-0023). Dependency patterns are in 05; streaming itself is specified in 02.
Sync facade¶
Resolved in ADR-0022.
- Mirrors the async surface 1:1. Entry is explicit;
create()MUST NOT auto-detect sync context and return a different type (ADR-0002). - Implemented as hand-written delegation across an anyio
BlockingPortal. It MUST contain no adapter logic — only delegation. - A parity test MUST assert that every public async member has a sync counterpart with a
matching
inspect.signatureonce coroutine-ness is discounted. Adding an async method without its sync counterpart MUST fail CI. - The portal is per-sandbox:
create_sync()starts it,__exit__/close()stops it. A shared module-level portal is forbidden as global state (ADR-0012). - N sync sandboxes means N threads. Docs MUST state this, and MUST name the async API as the answer for heavy concurrency.
- Sync streaming is supported. Each
OutputChunkcrosses the portal, so each chunk costs a thread round-trip; the docs MUST state the cost rather than hide it. - Errors raised through the facade MUST be the same classes with
__cause__intact. Portal frames in the traceback are acceptable.
Typing requirements¶
py.typedMUST be present in the wheel, asserted in CI. Without it, downstream mypy treats the whole package asAny.- Public API MUST type-check under pyright strict and mypy strict.
@overloadwhere return type depends on arguments. Dataclass returns, never raw dicts.- Every public symbol MUST carry a docstring with a runnable example.
Stability contract¶
Covered by semver: port protocols, error codes (04), DSN grammar
(07), ExecResult shape, Capability and IsolationTier members,
the contract suite.
Not covered: .native and everything reached through it; anything under
sandboxio.experimental.*; anything emitting ExperimentalWarning.
Deprecation: DeprecationWarning with correct stacklevel, plus PEP 702
@typing_extensions.deprecated, plus a changelog entry, plus a generous window. Warnings
alone do not reach users; the type-checker annotation is what does.
What each of these means in practice — including the deprecation window, and where a change is additive for callers but breaking for adapter authors — is the version policy.