# SBX documentation bundle Generated from the canonical documentation pages. For exact request and response schemas use /openapi.json; for the compact index use /llms.txt. # Introduction Source: /getting-started/introduction/ SBX runs **official provider CLIs** inside isolated executors and keeps the durable record of the work in PostgreSQL. Its durable identity is the **Session**; the machine that happens to run a Turn is a replaceable **ExecutorLease**; the provider boundary is a thin **Harness** adapter to the official CLI. ## Architecture at a glance | Part | Role | | --- | --- | | Control plane (`control/`) | One FastAPI application serving the `/api` business surface plus `/healthz` and `/readyz`. Run with `sbx serve` or `python -m control.composition serve`. | | PostgreSQL | The only business authority: journal, projections, Jobs, encrypted credential versions. | | Job workers | Run slow or recoverable effects (provisioning, validation, capture, delivery) under fenced claims. They run in-process with `serve`, or alone with `worker`. | | Executors | `modal` (production) and `local` (development). Both speak the same `sbx-runtime` protocol. | | `sbx-runtime` | Supervises operations, processes and files in the sandbox and spools evidence. It contains no reasoning loop. | | Console | A React web app that talks only to `/api`. | | Python SDK and `sbx` CLI | Clients of the same `/api` surface. | ## What SBX does - Accepts a Message, queues a Turn, provisions an executor if needed, runs the CLI, and records committed events for every step. - Keeps the Worktree across lease replacement by sealing private checkpoints. - Captures immutable ChangeSets, delivers them to GitHub as a branch and draft pull request, and gates merge on the exact reviewed subject. - Delegates review, test, research, security and integration work to ordinary child Sessions with validated ResultContracts. - Stores external credentials as write-only Connections, encrypted at rest. ## What SBX does not do - It does not call model APIs itself or implement a tool-selection loop. - It does not read credentials from the host environment or files. - It does not offer preview origins or OAuth sign-in with providers yet. Preview-grant requests return an error naming the missing capability, and credentials are entered manually. - Terminals poll for output; there is no WebSocket attach. ## Next Start with the [Quick start](/getting-started/quick-start/), then read [Concepts](/concepts/). # Quick start Source: /getting-started/quick-start/ This path runs everything on your machine except the sandbox, which runs on Modal. ## Prerequisites - Python 3.12+ with [uv](https://docs.astral.sh/uv/), and Node 22+ for the Console. - A reachable PostgreSQL database. - The repository checked out (`uv sync`). ## Minimum accounts and credentials | Needed | Why | | --- | --- | | Email and password account | Product sign-in. Self-hosted verification mail lands in a local directory unless you configure Resend. | | Modal token (`token_id`, `token_secret`) | Creates and tears down sandboxes. | | GitHub token | Clones repositories and delivers pull requests. | | OpenCode Zen API key | Inference for the OpenCode Harness (free models run only through the official OpenCode CLI). | | Codex `auth.json` (optional) | Only if you want the experimental Codex Harness. | ## 1. Configure and start the control plane ```bash export SBX_DATABASE_URL=postgresql://sbx:YOUR_PASSWORD@127.0.0.1:5432/sbx export SBX_VAULT_KEYS="k1:$(openssl rand -base64 32)" export SBX_RUNTIME_MASTER_KEY="$(openssl rand -hex 32)" export SBX_DATA_DIR=./.sbx-data # Console dev server origin (used in verification links and CSRF checks) export SBX_PUBLIC_URL=http://localhost:5174 uv run sbx migrate # optional: `serve` also migrates on start uv run sbx serve --port 8800 ``` `serve` applies migrations, starts the in-process workers and listens on `127.0.0.1:8800`. Check `curl http://127.0.0.1:8800/readyz`. See [Configuration](/self-hosting/configuration/) for every variable. ## 2. Start the Console `sbx serve` serves the API only. Run the Console from its own directory; it proxies `/api` to the control plane. ```bash cd console npm ci SBX_API_PROXY_TARGET=http://127.0.0.1:8800 npm run dev # http://localhost:5174 ``` ## 3. Create and verify your account Register at `/register`. Registration always answers `202`, whether or not the email exists. Without `SBX_RESEND_API_KEY` the verification email is written as a JSON file under `$SBX_DATA_DIR/mail/`; open the link it contains (`/verify-email?token=VERIFICATION_TOKEN`, valid 24 hours), then sign in. ## 4. Add Connections The Console's setup checklist (home page) lists what is missing. Open **Connections** and add, in any order: Modal, GitHub, OpenCode Zen, and optionally Codex. Secret fields are write-only; after submit SBX validates each Connection in the background and shows its health. From the CLI the same inputs are read from stdin or a file, never argv: ```bash uv run sbx auth login --email you@example.com # password via stdin or prompt printf '%s' "$GITHUB_TOKEN" | uv run sbx connections add github echo '{"token_id":"MODAL_TOKEN_ID","token_secret":"MODAL_TOKEN_SECRET"}' | uv run sbx connections add modal ``` ## 5. Run a first Turn Start a new Session from the Console home page (the composer defaults to the OpenCode Harness, the preferred free model and Modal), or from the CLI: ```bash uv run sbx execute "Add a CONTRIBUTING.md with one paragraph." ``` Follow the Turn in the Conversation and Activity tabs. When it succeeds in a Session with a repository, SBX captures a ChangeSet automatically; review it and request a Delivery from the Changes tab. A scripted version of the same flow is `examples/unified_mvp.py`; see the [Python SDK](/sdk/python/). ## Troubleshooting See [Troubleshooting](/troubleshooting/) for the most common first-run failures. # Concepts Source: /concepts/ IDs are opaque, prefixed identifiers (for example `sess_…`, `turn_…`). An ID conveys type, never authorization: every owned record carries a `workspace_id` and every lookup checks it. Access to another workspace's resource is reported as `not_found`. ## Work **Session** (`sess_`). The durable conversation and work. It has a lifecycle (`open`, `archived`, `closed`) and a role (`developer`, `review`, `test`, `research`, `security`, `integration`, `coordinator`). A Session may belong to a Project (pinned to an immutable ProjectVersion) or be projectless with an explicit repository. **Message** (`msg_`). An accepted piece of authored input. Routing is `note`, `queue` (default) or `steer`. Accepted user content is immutable. **Turn** (`turn_`). One accepted work request, ordered within the Session. States: `queued → preparing → running → succeeded | failed | cancelling → cancelled | interrupted`. At most one Turn is active per Session. A `reason` explains waiting or failure (for example `waiting_capacity`, `credential_invalid`, `outcome_unknown`). **Execution** (`exec_`). One attempt to perform a Turn on an ExecutorLease: operational evidence, not another unit of work. ## Compute and state **ExecutorLease** (`lease_`). A replaceable allocation of compute (`modal` or `local`). Many historical leases may exist; at most one is live per Session. Leases carry a generation that fences stale writers. **Worktree** (`wt_`). The Session's single logical mutable filesystem. It survives lease replacement because work is restored from a checkpoint, never reset to the repository's current main. **Snapshot** (`snap_`). An immutable manifest, either `environment` (reusable setup for a Project version) or `checkpoint` (private Session state). Credential files are excluded, and process, PTY and socket state is never claimed as restored. ## Results **ChangeSet** (`cs_`). An immutable captured subject with a manifest, per-file digests, a binary patch and a `subject_digest`. Capture runs after a successful Turn or on request. A capture from a failed, cancelled or interrupted Turn is recorded as `salvage`. **Delivery** (`dlv_`). The intent and outcome of delivering exactly one ChangeSet to an external target (a GitHub branch and draft pull request), with append-only step evidence. Merge is a separate, gated operation. **Delegation** (`del_`). A parent's assignment of work to an ordinary child Session with pinned inputs and a ResultContract. The validated, immutable **DelegationResult** is the only thing that counts as a review or test verdict. ## Credentials and infrastructure **Connection** (`con_`). One external authority of kind `modal`, `github`, `opencode_zen` or `codex`. Its secret material lives in encrypted **CredentialVersions**; replacing a credential appends a version. **Job** (`job_`). A durable internal continuation or effect, claimed by a worker with a fenced generation. Jobs are not user work; status is visible through `GET /api/jobs/{id}` and `GET /api/operations/{id}`. **Harness**. The thin adapter to an official CLI, described by a versioned capability manifest. See [Providers](/reference/providers/). ## Authority Only application commands mutate state, and each resource has one writer. Reads never settle or launch work: for example `GET` on files or services reports `executor_unavailable` instead of waking compute. The committed Session journal explains history; typed relational projections decide current state. # Connections Source: /guides/connections/ Every external account is a **Connection** with encrypted, versioned credentials. There are four kinds, all added by manual input: | Kind | Fields | Used for | Validation probe | | --- | --- | --- | --- | | `modal` | `token_id`, `token_secret` | Creating and releasing sandboxes. Only the executor worker sees it; the sandbox gets a lease-scoped key. | `App.lookup` with the token | | `github` | `token` (personal access token or GitHub App installation token) | Cloning repositories and Delivery (push, pull request, merge). | Repository access for Project repos (`GET /repos/…`); `GET /user` only supplies the identity, so installation tokens are accepted | | `opencode_zen` | `api_key` | Inference for the OpenCode Harness. Written to an isolated HOME and scrubbed after each Turn. | One minimal free-model chat request (this consumes quota and is recorded) | | `codex` (optional) | `auth_json` | The experimental Codex Harness, in an isolated `CODEX_HOME`. | Format check only | ## Secrets are write-only Responses never contain plaintext, ciphertext or fingerprints. They expose the credential version id, ordinal and format. Validation errors never echo your input. The Console clears secret fields after submit and never stores them in web storage. ## Health Configuration state is `configured`, `disabled` or `revoked`. Observed health is separate: `unverified`, `verifying`, `ready`, `degraded` or `reauth_required`. Validation runs as a background Job against the exact credential version, so a freshly added Connection starts `unverified` and settles shortly after. ```python conn = client.connections.add("github", {"token": token}) view = client.connections.wait_health(conn["id"]) print(view["health"]) ``` ## Replace and disconnect - **Replace** appends a new CredentialVersion with a compare-and-set on the Connection version (`expected_version`), invalidates old health and capability observations, and queues validation of the new version. A validation result that no longer matches the current version is discarded. - **Disconnect** is refused with `409 connection_in_use` while live or quarantined leases or live Executions depend on it. Once released it revokes the Connection, cancels queued Jobs that target it and makes the vault refuse to decrypt it. Revoking the token at the provider is separate and reported as `not_performed`. ## Selection A Session only considers Connections in its own workspace: those named explicitly, or the configured ones chosen by priority (skipping `reauth_required`). Environment variables, host files and other users' credentials are never consulted. A missing one surfaces as `connection_required` or `credential_invalid` on the Turn. ## Models `GET /api/models` crosses the Zen model list with public pricing. Free models are marked `usable_via: official_opencode_cli`, and the first free model in a fixed preference order is the `preferred_model`, used when a Session sets none. ## CLI ```bash printf '%s' "$TOKEN" | sbx connections add github --label work sbx connections list sbx connections validate CONNECTION_ID sbx connections replace CONNECTION_ID --credential-file new.json ``` # Sessions and Turns Source: /guides/sessions-and-turns/ ## Create and send `POST /api/workspaces/{workspace_id}/sessions` creates a Session, optionally with an initial Message in the same transaction. The response returns the Session, Message and Turn ids immediately; no request waits for a sandbox to boot. Further input goes to `POST /api/sessions/{id}/messages` with routing `queue` (default), `note` or `steer`. ```python result = client.execute( "Add a CONTRIBUTING.md with one paragraph.", repository={"full_name": "owner/repo"}, executor={"backend": "modal"}, ) print(result["turn"]["state"]) ``` Resolution order for settings is Project defaults, then the caller's explicit values. The OpenCode Harness defaults to the preferred free model. ## Turn lifecycle `queued → preparing → running → succeeded | failed | interrupted`, with `cancelling → cancelled | interrupted` after a cancel request. One Turn is active per Session; later Messages queue. A queued or preparing Turn may carry a `reason` such as `waiting_capacity`, `executor_unavailable` or `credential_invalid`. Only the control plane decides terminal state, never a process exit code. ## Events, SSE and watermarks Every committed fact is an event with a per-Session sequence number `seq`. Mutating responses include `event_watermark`, the highest committed sequence at that moment. See [Events](/api/events/) for the envelope and endpoint. - Read history with `GET /api/sessions/{id}/events?after=N` (JSON), or stream with `Accept: text/event-stream`. Resume with `after=N` or `Last-Event-ID`. - Dedupe by `seq`. If the stream drops, reconnect from the last `seq` you processed; reconnecting never creates another Turn. - A cursor ahead of the journal fails with `invalid_cursor`. ```bash sbx sessions events SESSION_ID --after 0 --follow ``` ## Cancel `POST /api/turns/{id}/cancellations` records the intent. A queued Turn is cancelled immediately; an active Turn moves to `cancelling` and ends `cancelled` or `interrupted`. If the Turn already finished, a committed result wins and the call is a no-op. ## Retry `POST /api/turns/{id}/retries` creates a new Turn from the original Message (linked with `retry_of`). Only `failed`, `cancelled` or `interrupted` Turns can be retried; otherwise the call returns `invalid_transition`. ## Unknown outcomes If SBX cannot prove how a Turn ended (for example the runtime restarted mid-operation), the Turn ends with reason `outcome_unknown` (state `interrupted`, or `failed` if it was still preparing). SBX never relaunches it and never reports success. 1. The SDK raises `OutcomeUnknown` from `turns.wait` and `execute`. 2. Inspect the Worktree and the Activity events to decide whether the work landed. 3. Acknowledge with `POST /api/turns/{id}/acknowledgements`. Until then, retry and the next queued Turn are held (`outcome_unknown`, action `acknowledge`), and acknowledgement itself waits while old compute is not yet confirmed isolated (retryable). ## Idempotent retries of requests Every mutation needs an `Idempotency-Key`. Send the same key when you retry after a network error; see [API overview](/api/overview/). ## Lifecycle commands `archives`, `unarchives` and `closures` move a Session between `open`, `archived` and `closed`. A closed Session rejects new work and closes its child Sessions. `executor/activations` and `executor/releases` wake or release compute explicitly; reads never do. # Changes and Delivery Source: /guides/changes-and-delivery/ ## ChangeSets A ChangeSet is an immutable snapshot of what changed in a Worktree. - **Capture** runs as the `changeset.capture` Job. It starts automatically after a successful Turn in developer, integration or coordinator Sessions that have a repository, or on request with `POST /api/sessions/{id}/changesets`. A capture whose source Turn failed, was cancelled or interrupted is recorded with origin `salvage` and is not eligible for automatic delivery. - The runtime writes the exact tree and reports a manifest, per-file SHA-256 digests, the patch and file blobs. Credential files such as `.env`, `auth.json` and private keys are excluded; templates such as `.env.example` are preserved. Selected credentials and obvious secret patterns fail capture. - A Turn completes only after its managed CLI descendants stop. Capture and checkpoint close managed terminals and stop services before copying state. An unconfirmed writer stop blocks the operation. - Before sealing, the control plane rebuilds the manifest from its own pinned repository and base commit, recomputes `subject_digest` and re-verifies every blob. A failed capture is `failed`; there is never a fake ready subject. - States include `ready` and `failed`. A sealed ChangeSet cannot be modified. ```python cs = client.changesets.wait_ready(session_id, source_turn_id=turn["id"]) print(client.changesets.diff(cs["id"])) ``` `GET /api/sessions/{id}/changes` shows live, uncaptured changes without waking compute. `POST /api/changesets/{id}/applications` applies a ChangeSet to another Session only if its working tree equals the ChangeSet baseline; otherwise nothing is written. ## Delivery `POST /api/changesets/{id}/deliveries` requests delivery of exactly that ChangeSet. The Delivery Job performs three steps and records each as evidence: 1. **Materialize**: fetch the pinned base, apply the patch, verify the tree equals the sealed tree, and create a deterministic commit. 2. **Push** to `sbx/SESSION/CHANGESET` using `--force-with-lease`. If the remote branch holds a different head the Delivery blocks with `remote_head_changed`; there is no plain force-push. 3. **Pull request**: find the existing PR for the head ref or open a **draft** PR, then verify its head equals the commit. Use `retries` for a same-intent transport retry and `refreshes` to reconcile with the remote. `delivery_unresolved` means the remote result could not be proven yet. ## Merge gate Merge is a separate operation, `POST /api/deliveries/{id}/merge-requests`. The merge worker reloads the current Project policy and enforces it alongside the Delivery's pinned policy and a fresh remote observation. Tightening a policy also constrains existing Deliveries. All of the following must hold: - The ChangeSet is sealed, the Delivery is verified, and the remote head equals the mapped commit equals `expected_head_sha`. - The expected Delivery `version` matches and the merge method is allowed. - Every required DelegationResult is pinned to this exact `subject_digest`, comes from an independent child Session and is valid; no `request_changes` result exists for this subject. - Required checks pass on the exact head and the PR is open. A draft PR merges only if the request sets `mark_ready`. - `require_base_unchanged` is unsupported, so a policy that needs it blocks. Otherwise the call fails with `gate_blocked` and reasons. The provider merge is called with `sha=expected_head_sha`, so a head that moved after the check is refused by the provider too. A new ChangeSet is a new subject: earlier approvals do not carry over (`stale_subject`). The SDK sends the server's pins for you: ```python delivery = client.deliveries.wait(client.deliveries.request(cs["id"])["id"]) client.deliveries.merge(delivery["id"], method="squash") ``` Humans cannot post verdicts. Review and test results come only from [child Sessions](/guides/child-sessions/). # Child Sessions Source: /guides/child-sessions/ A **Delegation** creates an ordinary child Session with its own Worktree and native context. There is no special review subsystem: a review is a child Session whose result is validated against a contract. ## Spawn `POST /api/sessions/{id}/delegations` takes a `role` (`review`, `test`, `research`, `security` or `integration`), optional `changeset_id` (the subject to pin), optional context, and optionally an explicit `result_kind`. In one transaction SBX creates the child Session, the Delegation, the pinned inputs and the first Message and Turn carrying the ResultContract, then dispatches it. The input ChangeSet is applied and committed locally as the child's baseline. Limits: depth 3 and 10 children per parent. ```python d = client.delegations.spawn(session_id, "review", changeset_id=cs["id"]) result = client.delegations.wait_result(d["delegation_id"]) ``` ## ResultContracts | Role | Default contract | | --- | --- | | `review`, `security` | `ReviewAssessment`: `verdict` of `approve`, `request_changes` or `comment`, with findings and checks | | `test` | `TestResult`: `status` of `pass`, `fail` or `unknown`, with checks | | `research` | `ResearchResult`: summary and sources | | `integration` | `IntegrationResult`: `integrated`, `conflict` or `failed` | | other | `GenericResult` | Subject-bound contracts echo the pinned `subject_digest`. ## Publishing a result When the child's Turn ends, `delegation.publish_result` extracts the **last fenced JSON block** from its output and validates kind, schema and subject pin. - Exactly one immutable result is published. Missing or malformed output fails the Delegation (`output_contract_invalid`) and never counts as approval. - For `TestResult`, the platform also runs the Project's declared checks in the child sandbox and overrides the status to `fail` if any check fails. - Waiting for a Turn (`turns.wait`) and waiting for a validated result (`delegations.wait_result`) are different calls. ## Waits and cancel `POST /api/delegations/{id}/waits` registers a wait; an already-available result is checked in the same transaction, and satisfaction can queue a Message to the parent. Cancelling a Delegation or closing the parent cancels the subtree. Results feed the [merge gate](/guides/changes-and-delivery/) only when pinned to the exact ChangeSet subject. ## Agent tools Agents inside a Session reach these operations through the private tool gateway (`/internal/tools/{tool}`) with Session-scoped grants, action allowlists and the same depth and child budgets. The grant ends with the Session. Product clients use `/api`, not this gateway. # Console Source: /guides/console/ The Console is a React/Vite app in `console/` that talks only to `/api`. It is not served by `sbx serve`; run it next to the control plane. ```bash cd console npm ci SBX_API_PROXY_TARGET=http://127.0.0.1:8800 npm run dev # http://localhost:5174 npm run typecheck && npm test && npm run build ``` Set `SBX_PUBLIC_URL` and `SBX_ALLOWED_ORIGINS` on the control plane to the origin you open in the browser; cookie mutations from other origins are rejected with `csrf_failed`. ## Pages - **Home**: the setup checklist (account, Modal, GitHub, OpenCode Zen; Codex optional) and the composer for a new Session. It defaults to the OpenCode Harness, the preferred free model and Modal once a Modal Connection exists. - **Sessions**: the list and the Session view with Conversation, Activity, Changes (ChangeSets, Deliveries, merge eligibility), Files, Terminal, Services and Child Sessions tabs. - **Projects**: reusable repository and environment configuration. - **Connections**: add, validate, replace and disconnect, with write-only secret fields. - **Settings**: identity, API keys (plaintext shown once) and preferences. ## Rules the UI follows - It renders server fields and never re-derives turn outcomes, connection health, merge eligibility or review validity. Merge sends the server's pins. - One store holds the snapshot and its watermark, replays events by sequence, dedupes by `seq` and resyncs on gaps. - Secrets never enter web storage; caches are purged on logout. - Compute is never started implicitly. When the executor is unavailable, Files, Terminal and Services show a diagnosis instead. - Retry buttons name what they retry: Turn, Delivery or validation. ## Limits The Terminal polls for output. Service preview is not available because no preview origin is configured. # API overview Source: /api/overview/ There is **one business API**, under `/api`. `/healthz` and `/readyz` are operational endpoints. There are no versioned or alternate API prefixes. The reference is generated from `docs/specs/unified/openapi.yaml` and is browsable under [REST API (/api)](/reference/api/). ## Authentication Two methods resolve to the same principal and permissions: | Method | How | Extra requirements on mutations | | --- | --- | --- | | API key | `Authorization: Bearer sbx_key_…` | The key's scope must allow the call (see below) | | Cookie session | `POST /api/auth/login` sets `sbx_session` (HttpOnly) and `sbx_csrf` | `X-CSRF-Token` header equal to the `sbx_csrf` cookie, and an allowed `Origin` | Create a key with `POST /api/api-keys` (cookie session). The plaintext is returned once and only a hash is stored. `scopes` is a non-empty subset of `["*", "write", "read"]` (default `["*"]`; anything else is `422`): | Scope | Allows | | --- | --- | | `read` | `GET` routes only; every mutation returns `403 forbidden` | | `write` | reads and all resource mutations | | `*` | everything, including minting/revoking API keys and changing the password | Cookie sessions are always full scope. Login requires a verified email, is rate limited to 10 failures per email per 15 minutes (`429 rate_limited`), and changing a password revokes cookie sessions. ## Idempotency-Key Every mutation requires an `Idempotency-Key` header (up to 200 characters), or the call fails with `validation_failed`. - Same key and same body returns the original committed response. - Same key and a different body returns `409 idempotency_conflict`. - Records are kept for 7 days. - After a network error, retry with the **same** key. The SDK does this for you. ## Responses - `201` for immediate creates. - `202` for accepted long operations, with resource ids, `operation_id` or `job_id` when meaningful, and `event_watermark`. Poll `/api/operations/{id}` or follow events. - `204` for logout, which has no body. - Responses carry `X-Request-Id` and `Cache-Control: no-store`. ## Errors ```json {"error": {"code": "version_conflict", "category": "concurrency", "message": "session version changed", "retryable": true, "details": {"current_version": 4}, "request_id": "req_ab12cd34ef56ab12"}} ``` `retry_after` (also sent as `Retry-After`) and `action` appear when relevant. Request bodies are never echoed. Branch on `code`, not on `message`. See [Errors](/api/errors/) for all codes. ## Concurrency and pagination Updates carry an `expected_version`; a stale value returns `version_conflict`. Lists are bounded and ordered deterministically. Event listings use `after` and `limit` instead; see [Events](/api/events/). ## Reads do not do work `GET` requests never provision compute or settle state. Files, terminals and services return `executor_unavailable` when no live lease exists; use `POST /api/sessions/{id}/executor/activations` to wake one. ## Not available `POST …/services/{name}/preview-grants` exists in the schema but returns an error because no dedicated preview origin is configured. Terminals use polling (`…/terminals/{id}/input` and `…/output`). # Events Source: /api/events/ Each Session has an append-only journal. Sequence numbers (`seq`) are allocated inside the same transaction as the state change, so the journal and the projections never disagree. Runtime and browser caches are never authoritative. ## Endpoint `GET /api/sessions/{session_id}/events` | Parameter | Meaning | | --- | --- | | `after` | Return events with `seq` greater than this (default 0). | | `limit` | Page size, 1 to 1000 (default 500). | | `types` | Comma-separated event types to include. | | `turn_id` | Only events for one Turn. | | `max_seconds` | SSE only: how long the stream stays open (max 600). | Without `Accept: text/event-stream` the response is JSON: ```json {"items": [], "next_after": 12, "event_watermark": 12} ``` With `Accept: text/event-stream` each event is an SSE frame with `id` set to `seq`, `event` set to the type and `data` set to the JSON envelope. Comment lines (`: watermark N`, `: heartbeat`) are keep-alives to ignore. `Last-Event-ID` resumes like `after`; if both are given they must agree. ## Watermarks `event_watermark` is the highest committed `seq`. Initial reads return the snapshot and its watermark from one database snapshot, so replay from `after=W` is gap-free. Clients dedupe by `seq`. ## Envelope `id`, `workspace_id`, `session_id`, `seq`, `type`, `schema_version`, `recorded_at`, `observed_at`, `actor`, `source`, `causation_id`, `correlation_id`, `turn_id`, `execution_id`, `executor_lease_id`, `lease_generation`, `delegation_id`, `changeset_id`, `delivery_id`, `runtime_epoch`, `local_seq` and `payload`. ## Event types | Family | Types | | --- | --- | | `session.` | `created`, `settings_changed`, `archived`, `unarchived`, `closed` | | `message.` | `accepted`, `routed`, `part_added`, `part_updated`, `completed` | | `turn.` | `queued`, `preparing`, `started`, `cancel_requested`, `succeeded`, `failed`, `cancelled`, `interrupted` | | `execution.` | `preparing`, `started`, `native_bound`, `observed_terminal`, `stopped` | | `tool.` | `started`, `updated`, `completed` | | `usage.`, `diagnostic.` | `usage.observed`, `diagnostic.reported` | | `executor.` | `bound`, `quiescing`, `released`, `unavailable` | | `worktree.` | `restored`, `changed`, `apply_requested`, `applied` | | `snapshot.` | `requested`, `ready`, `failed` | | `changeset.` | `capture_requested`, `ready`, `capture_failed` | | `delivery.` | `requested`, `progressed`, `blocked`, `succeeded`, `failed`, `cancelled`, `merge_requested`, `merged`, `merge_failed` | | `delegation.` | `created`, `waiting`, `result_published`, `failed`, `cancel_requested`, `cancelled` | | `service.` | `requested`, `ready`, `degraded`, `failed`, `stopped` | `message.part_updated` replaces a part's content by revision; apply it by replacement, never by appending, to avoid duplicating cumulative text. A turn is terminal at `turn.succeeded`, `turn.failed`, `turn.cancelled` or `turn.interrupted`; `execution.observed_terminal` with verdict `unknown` corresponds to an `outcome_unknown` Turn. ## Errors `invalid_cursor` (400) for a negative cursor, a cursor ahead of the journal, or mismatched `after` and `Last-Event-ID`. The Console treats `invalid_cursor` and `history_reset_required` as the signal to fetch a fresh snapshot instead of replaying. # Errors Source: /api/errors/ All errors use the shape `{"error": {"code", "category", "message", "retryable", "details", "request_id"}}`. This table is the canonical vocabulary. | Code | Category | Status | Retryable | | --- | --- | --- | --- | | `not_found` | access | 404 | no | | `forbidden` | access | 403 | no | | `unauthenticated` | authentication | 401 | no | | `csrf_failed` | authentication | 403 | no | | `rate_limited` | throttle | 429 | yes | | `validation_failed` | request | 422 | no | | `version_conflict` | concurrency | 409 | yes | | `idempotency_conflict` | request | 409 | no | | `invalid_transition` | state | 409 | no | | `unsupported_capability` | capability | 422 | no | | `credential_invalid` | credential | 409 | no | | `connection_revoked` | credential | 409 | no | | `connection_in_use` | credential | 409 | no | | `connection_required` | credential | 409 | no | | `waiting_capacity` | capacity | 409 | yes | | `quota_exhausted` | capacity | 429 | no | | `runtime_incompatible` | runtime | 409 | no | | `executor_unavailable` | runtime | 409 | yes | | `context_unavailable` | context | 409 | no | | `context_mismatch` | context | 409 | no | | `outcome_unknown` | outcome | 409 | no | | `output_contract_invalid` | outcome | 422 | no | | `capture_failed` | changes | 409 | yes | | `stale_subject` | changes | 409 | no | | `remote_head_changed` | delivery | 409 | no | | `delivery_unresolved` | delivery | 409 | yes | | `gate_blocked` | delivery | 409 | no | | `history_reset_required` | events | 409 | no | | `invalid_cursor` | events | 400 | no | | `stale_fence` | fencing | 409 | no | | `operation_conflict` | runtime | 409 | no | | `internal_error` | platform | 500 | yes | # Python SDK Source: /sdk/python/ The `sbx` package ships a synchronous client over the `/api` surface, built on `httpx`. ```python from sbx import SBXClient client = SBXClient("http://127.0.0.1:8800", api_key="sbx_key_YOUR_KEY") print(client.me()["workspaces"]) ``` `api_key` is sent as a Bearer token. To use a cookie session instead, call `client.login(email, password)`; the client then sends the CSRF header for you. `workspace_id` defaults to the first workspace of the principal. ## `execute` ```python result = client.execute( "Add a CONTRIBUTING.md with one paragraph.", repository={"full_name": "owner/repo"}, executor={"backend": "modal"}, deadline=1800, on_event=lambda e: print(e["type"]), ) session_id, turn = result["session_id"], result["turn"] ``` `execute` creates a Session (or sends a Message to `session_id`), then follows committed events until the accepted Turn is terminal. It accepts `project_id`, `project_version_id`, `harness`, `executor`, `repository`, `deadline` and `on_event`. It contains no agent loop and never treats a process exit as the outcome. It raises `OutcomeUnknown` if the Turn ended with `outcome_unknown` and `DeadlineExceeded` if the deadline passes. ## Namespaces | Namespace | Methods | | --- | --- | | `projects` | `list`, `create`, `get`, `publish` | | `connections` | `list`, `add`, `get`, `replace`, `validate`, `disconnect`, `wait_health` | | `sessions` | `create`, `list`, `get`, `send`, `close`, `archive`, `executor`, `activate`, `release`, `export`, `events`, `stream` | | `messages` | `list` | | `turns` | `list`, `get`, `cancel`, `retry`, `acknowledge`, `wait` | | `changesets` | `list`, `capture`, `get`, `diff`, `apply`, `wait_ready` | | `deliveries` | `request`, `get`, `retry`, `refresh`, `merge`, `wait` | | `delegations` | `spawn`, `get`, `result`, `cancel`, `wait_result` | | `operations` | `get` | Top-level helpers: `login`, `register`, `verify_email`, `me`, `create_api_key`, `models` and the raw `get`, `post` and `request`. ## Events `sessions.events(session_id, after=0, follow=False)` replays the journal by sequence using the JSON endpoint; with `follow=True` it polls until the deadline. `sessions.stream(session_id, after=0)` reads the SSE stream. ## Waiting helpers `turns.wait` (a Turn is terminal), `delegations.wait_result` (a validated DelegationResult exists), `changesets.wait_ready`, `deliveries.wait` and `connections.wait_health` are separate, explicit-deadline calls. Waiting for a Turn and waiting for a result are never interchangeable. ## Errors and retries API failures raise `SBXError` with `code`, `message`, `status`, `details`, `retryable`, `request_id` and `action`. `OutcomeUnknown` and `DeadlineExceeded` subclass it. Network failures are retried with the same `Idempotency-Key` (3 retries with backoff), and 5xx responses to mutations are retried the same way. After the last attempt the client raises `SBXError` with code `transport_error`. Pass `idempotency_key=` to `sessions.send` to control the key yourself. ## Example `examples/unified_mvp.py` runs the whole flow: pick the preferred model, execute a Turn, wait for the ChangeSet, spawn a review, deliver and print the pull request URL. ```bash SBX_BASE_URL=http://127.0.0.1:8800 SBX_API_KEY=sbx_key_YOUR_KEY \ python examples/unified_mvp.py owner/repo ``` # CLI Source: /reference/cli/ The exact generated help for every command is published as [cli-help.txt](/cli-help.txt). Client defaults are listed in [config-reference.json](/config-reference.json). ## Client configuration | Field | Environment | Default | | --- | --- | --- | | `base_url` | `SBX_BASE_URL` (or `--base-url`) | `http://127.0.0.1:8800` | | `api_key` | `SBX_API_KEY` | none; stored by `sbx auth login` | | `workspace_id` | `SBX_WORKSPACE_ID` | first workspace of the principal | `sbx auth login --email you@example.com` reads the password from stdin or a prompt, signs in, mints an API key and writes it to `$XDG_CONFIG_HOME/sbx/client.json` (default `~/.config/sbx/client.json`) with mode `0600`. Environment variables override the file. ## Commands | Command | Purpose | | --- | --- | | `auth login` | Sign in and store an API key. | | `projects list`, `projects create SLUG --spec-file F` | Projects and ProjectVersions. | | `connections list`, `add KIND`, `replace ID`, `validate ID`, `disconnect ID`, `show ID` | Connections. Kinds: `modal`, `github`, `opencode_zen`, `codex`. Secrets come from stdin or `--credential-file`, never argv. | | `sessions create`, `list`, `show`, `send`, `events`, `cancel`, `continue`, `close`, `export` | Sessions and Turns. | | `execute PROMPT [--session ID] [--project ID]` | Create or continue a Session and follow the Turn. | | `changesets capture`, `apply`, `show` | ChangeSets. `capture --salvage` marks a salvage capture. | | `deliveries request`, `retry`, `show`, `merge` | Deliveries and gated merge. | | `delegations spawn`, `wait`, `result`, `cancel` | Child Sessions. Roles: `review`, `test`, `research`, `security`, `integration`. | | `operations show ID` | Long-operation status. | | `serve [--host H] [--port P]` | Run the control plane and workers (operator). | | `migrate` | Apply database migrations (operator). | Output is JSON. Errors print a JSON object with `code`, `message` and `action` to stderr and exit with status 1. `serve` and `migrate` read the control-plane variables described in [Configuration](/self-hosting/configuration/); they are equivalent to `python -m control.composition serve` and `migrate`. # Providers Source: /reference/providers/ Harness support is declared in pinned manifests. The machine-readable table is [provider-reference.json](/provider-reference.json). | Provider | Support tier | Credentials | | --- | --- | --- | | `opencode` | supported | `opencode_zen` Connection | | `codex` | experimental | optional `codex` Connection | | `claude` | disabled | n/a | | `devin` | disabled | n/a | | `grok` | disabled | n/a | | `antigravity` | disabled | n/a | A disabled provider cannot be selected. Capability values are `supported`, `unsupported` or `unknown`, and `unknown` is never treated as supported. For OpenCode, `event_stream`, `model_discovery`, `native_resume`, `native_state_export` and `usage` are supported; `interrupt`, `steer`, `interactive_approval` and `credential_writeback` are not. Use `GET /api/harnesses` for the installed catalog, `GET /api/models` for Connection-scoped model availability and `GET /api/executor-backends` for executors. These reads have no side effects. A Harness switch creates a new linked Session with an explicit summary Message. SBX never silently translates hidden context between CLIs. # Configuration Source: /self-hosting/configuration/ The control plane reads its configuration from the environment at start-up. ```bash sbx serve --host 127.0.0.1 --port 8800 # API + workers in one process python -m control.composition serve # same thing python -m control.composition worker # workers only python -m control.composition migrate # apply migrations and exit ``` Each command applies pending migrations first, under an advisory lock. `serve` runs `SBX_WORKER_THREADS` job workers and a one-minute timer that releases idle executors. ## Required | Variable | Meaning | | --- | --- | | `SBX_DATABASE_URL` | PostgreSQL connection string. The only business authority. | | `SBX_VAULT_KEYS` | Credential keyring, `KID:BASE64KEY,KID2:BASE64KEY2`. The first key encrypts; older keys still decrypt after rotation. Keep it outside the database and backups. | | `SBX_RUNTIME_MASTER_KEY` | Hex-encoded master key from which per-lease runtime keys are derived. Not stored in the database. | The process exits at start-up if any of these is missing. ## Optional | Variable | Default | Meaning | | --- | --- | --- | | `SBX_DATA_DIR` | `./.sbx-data` | Local blobs, git work area, local executor state and the mail sink. | | `SBX_PUBLIC_URL` | `http://localhost:8800` | Base URL used in verification and reset links. | | `SBX_ALLOWED_ORIGINS` | the public URL | Comma-separated origins allowed for cookie mutations. | | `SBX_COOKIE_SECURE` | `0` | Set to `1` to mark cookies `Secure` (use behind HTTPS). | | `SBX_EXECUTORS` | `local,modal` | Enabled executor backends. | | `SBX_RESEND_API_KEY` | unset | Send email through Resend. Without it, mail is written to `$SBX_DATA_DIR/mail/` (directory `0700`, files `0600`). | | `SBX_MAIL_FROM` | unset | Sender address for Resend email, on a domain verified in Resend. Required when `SBX_RESEND_API_KEY` is set; startup fails without it. | | `SBX_WORKER_THREADS` | `4` | In-process job worker threads. | Generate keys with `openssl rand -base64 32` for a vault key and `openssl rand -hex 32` for the master key. ## Health `GET /healthz` returns `{"ok": true}`. `GET /readyz` returns `503` when the database cannot be read. ## Client variables `SBX_BASE_URL`, `SBX_API_KEY` and `SBX_WORKSPACE_ID` configure the CLI and SDK only; see [CLI](/reference/cli/). ## Running in production Run behind HTTPS and set `SBX_COOKIE_SECURE=1`. `sbx serve` does not serve the Console; host the built `console/` bundle and route `/api` to the control plane, and set `SBX_PUBLIC_URL` and `SBX_ALLOWED_ORIGINS` to the Console's origin. Back up PostgreSQL and the vault keys separately: the database alone cannot decrypt credentials, and the keys alone hold no data. # Security Source: /self-hosting/security/ ## Identity - Passwords use argon2id; unknown accounts cost the same work as known ones. - Email verification and reset tokens are single use and stored only as hashes. Verification expires after 24 hours. - Cookie sessions are `HttpOnly`, `SameSite=Lax` and `Secure` when configured. Cookie mutations need a CSRF token and an allowed `Origin`. - API keys (`sbx_key_` prefix) are stored hashed; plaintext is shown once. - Changing or resetting a password bumps `auth_epoch` and revokes cookie sessions. - Cross-workspace access is reported as `not_found`; holding an ID grants nothing. ## Vault CredentialVersions are encrypted with AES-256-GCM. The associated data binds the workspace, Connection, version and format, so ciphertext cannot be moved between records. The keyring (`SBX_VAULT_KEYS`) lives outside the database. Replacing or disconnecting a Connection bumps a revocation epoch that invalidates grants, and the broker refuses to decrypt revoked material. ## No ambient credentials SBX never reads provider or cloud credentials from the host environment, host files or other users. A Session uses only Connections in its own workspace. The runtime builds the CLI's environment from an explicit allowlist rather than inheriting `os.environ`. | Secret | Where plaintext goes | | --- | --- | | Modal token | the executor worker only | | GitHub token | the delivery worker, and a clone helper that reads it from a private file (never a URL or argv) | | OpenCode Zen key | the CLI's isolated HOME, scrubbed after each Turn | | Codex `auth.json` | an isolated `CODEX_HOME`, scrubbed after each Turn | ChangeSets and checkpoints exclude credential files, including `.env`, `auth.json` and private keys. Safe templates such as `.env.example` remain ordinary files. Checkpoints also exclude files containing selected credentials or obvious secret patterns; selected credentials in Git metadata refuse the checkpoint. Restore filters credential files and secret content from older archives, and rejects traversal or link escapes. Native state is checked for selected credential values so ordinary transcript examples remain usable. ## Redaction Known secrets and secret-looking structured fields are redacted before runtime evidence is spooled, and before public errors are emitted. Request validation errors never echo input. File reads refuse credential paths with `403 forbidden` and redact selected credentials and secret patterns in ordinary files. ChangeSet capture refuses selected credentials and secret patterns. Worker tracebacks, runtime output and stored fault messages are scrubbed before exposure. Audit records store the target, version, purpose, actor and result, never secret material. ## Runtime channel The control plane talks to each runtime with HMAC-signed grants derived from `SBX_RUNTIME_MASTER_KEY`, scoped to one lease and generation with a 5-minute TTL. A stale generation is rejected (`stale_fence`). The runtime offers no generic exec; mutating operations are a fixed set. ## Limits to know Project code and the official CLI can read any credential explicitly placed in their environment, so SBX does not claim tamper-proof attestation. Provider-side token revocation on disconnect is not performed. Preview origins and OAuth sign-in are not implemented. # Troubleshooting Source: /troubleshooting/ | Symptom | Cause and action | | --- | --- | | Process exits: `SBX_DATABASE_URL, SBX_VAULT_KEYS and SBX_RUNTIME_MASTER_KEY are required` | Export all three before `serve`, `worker` or `migrate`. | | `GET /readyz` returns `503` | The control plane cannot read PostgreSQL; check `SBX_DATABASE_URL`. | | Cannot sign in after registering | Email verification is required. Without `SBX_RESEND_API_KEY` the link is in `$SBX_DATA_DIR/mail/*.json`. Tokens expire after 24 hours. | | `429 rate_limited` on login | 10 failures per email in 15 minutes. Wait and retry. | | `csrf_failed` in the Console | The browser origin is not in `SBX_ALLOWED_ORIGINS`, or the CSRF token is missing. Set the public URL and origins to the Console address. | | `validation_failed` mentioning `Idempotency-Key` | Every mutation needs the header. The SDK and CLI add it. | | Connection stays `unverified` | Validation is a background Job; make sure workers run (`serve` runs them). `reauth_required` means the provider rejected the credential: replace it. | | Turn waits with `connection_required` or `credential_invalid` | Add or replace the Modal, GitHub or OpenCode Zen Connection. | | Turn waits with `waiting_capacity` | Connection capacity is in use; the Turn continues when a slot frees. | | `executor_unavailable` on Files, Terminal or Services | No live executor. Reads never wake compute; activate it from the Session. | | Turn ended `outcome_unknown` | SBX could not prove the result. Inspect the Worktree, then acknowledge the Turn before retrying. See [Sessions and Turns](/guides/sessions-and-turns/). | | Delivery blocked with `remote_head_changed` | The remote branch holds another head. Reconcile with a refresh; SBX never force-pushes. | | Merge fails with `gate_blocked` | A required result is missing, stale or for a different `subject_digest`, checks are not green, or the head moved. Read the reasons in the response. | | `409 connection_in_use` on disconnect | Live leases or Executions use the Connection. Release them first. | | Preview grant fails | No preview origin is configured; preview is not available. | | `invalid_cursor` on events | The cursor is ahead of the journal or `after` and `Last-Event-ID` disagree. Fetch a fresh snapshot and resume from its watermark. |