<div align="center">

# @sema-agent/server

**The server/API layer of the Sema stack — wire the engine, serve the fleet.**

[![npm](https://img.shields.io/npm/v/%40sema-agent%2Fserver)](https://www.npmjs.com/package/@sema-agent/server)
[![license: BUSL-1.1](https://img.shields.io/badge/license-BUSL--1.1-blue)](#license)

[Quick start](#quick-start) · [Configuration](#configuration) · [HTTP API](#http-api-overview) · [Ecosystem](#ecosystem) · [License](#license)

[中文](./README.zh-CN.md)

</div>

---

## What it is

`@sema-agent/server` is the server/API implementation layer of the Sema stack: it wires the
[`@sema-agent/core`](https://www.npmjs.com/package/@sema-agent/core) engine, registry configuration,
model gateways, and cloud agent execution behind an HTTP/SSE contract.

**It is:**

- **An HTTP/SSE server plus assembly layer.** Requests carry content (`objective`, `sessionId`,
  `scenario`, …); the server assembles everything else per task — tools, sub-agent rosters,
  prompts, skills, policy — and injects identity, sessions, and safety policy server-side.
  Credentials live in server-side closures, never in the request body, the model, or the sandbox.
- **Stateless by design.** Replicas are cattle: `docker run` starts one, start as many as you need
  behind a load balancer. Durable state — sessions, runs, replayable event logs, checkpoints,
  approvals — lives in an external SQL store (any MySQL-protocol database such as MySQL/TiDB/MariaDB,
  or PostgreSQL; a file-backed `local` mode covers single-machine use). Each pooled connection pins its
  own session isolation (`REPEATABLE READ` on the MySQL-protocol leg, `READ COMMITTED` on PostgreSQL) and
  verifies it by reading back; on TiDB the session is switched to pessimistic transaction mode, and boot is
  refused only when that switch cannot be made or verified — see docs/DEPLOY-PREREQS.md. Any replica can serve any
  run's event stream; a task can suspend on one replica and resume on another.
- **The full server-side capability surface** (discoverable at `GET /v1/capabilities`):
  asynchronous runs with replayable SSE, durable checkpoints with human-in-the-loop approvals,
  sessions and long-term memory, multi-scenario assembly from one image, per-task sub-agents,
  deterministic workflow orchestration, and pluggable sandbox execution lanes
  (`host`, `local-docker`, `e2b`, `k8s`, `ssh`, `adb`).

**It is not:**

- **Not the engine.** The agent loop, tool harness, memory, and checkpoint machinery are
  [`sema-core`](https://github.com/sema-agent/sema-core); this repository consumes it as an npm
  dependency and contributes wiring, backends, and the service contract.
- **Not the CLI.** The terminal agent is [`sema`](https://github.com/sema-agent/sema); it (and the
  web UI) are clients of this server.
- **Not the deployment tooling.** One-command self-hosted deployment (Docker single node or
  Kubernetes) is [`sema-deploy`](https://github.com/sema-agent/sema-deploy).
- **Not a persistence center.** The truth lives in the external database; the server processes are
  disposable.

## Architecture

<!-- TODO: architecture SVG — visual assets live with the portal repo (sema-agent/sema); text version below. -->

<details>
<summary>Text version</summary>

```
  HTTP / SSE API              Assembly                     Execution lanes
  ──────────────              ────────                     ───────────────
  /v1/tasks   (sync) ──┐   ┌─ per-task spec       ─┐   ┌─ host          (this machine)
  /v1/runs   (async) ──┤   │  scenarios · skills   │   ├─ local-docker  (per-task container)
  /v1/sessions       ──┼──▶│  policy · approvals   │──▶├─ e2b           (Firecracker VM)
  /v1/approvals      ──┤   │  registry config      │   ├─ k8s           (Kata pod sandbox)
  /v1/workflows      ──┘   │  model gateway(s)     │   └─ ssh / adb     (real host / device)
                           └─ @sema-agent/core    ─┘
                                     │
                     Durable stores (MySQL/TiDB · PostgreSQL · local file)
                 sessions · runs · event logs · checkpoints · approvals
```

</details>

## Quick start

Requirements: Node ≥ 20 (npm path) and an OpenAI-compatible model gateway.

> **A note on the `glob@11` deprecation warning at install time.** `npm install` prints a deprecation
> warning for `glob@11.1.0`, pulled in transitively by `e2b` (the E2B sandbox SDK). It is **install-time
> noise with no runtime exposure here**: `glob` has exactly one load site inside `e2b` — a `dynamicImport`
> in its *template-build* file-packing path — and this server only ever touches E2B's *sandbox runtime*
> API. Verified by execution, not by reading: importing `e2b`, constructing the adapter and driving a real
> `exec` never puts `glob` in the module cache. **We deliberately do not silence it.** The only thing that
> actually works is bundling e2b's whole subtree into this package (measured: the warning does go away) —
> and that costs 2.7 MB → 20.8 MB unpacked, marks the tree `invalid` in `npm ls`, and decouples the `e2b`
> you install from the one e2b publishes. Not worth it for a warning with no runtime reach. Things that
> do **not** work, in case you were about to try: our `overrides` (they only apply when the package.json
> being read *is the project npm was invoked on*), and a published `npm-shrinkwrap.json` (also ignored when
> this package is a dependency — both measured). If the warning bothers you, add
> `"overrides": { "glob": "^13" }` to **your own** project's package.json — that is the one npm reads, and
> `glob@13` is API-compatible with what e2b uses. The real fix is upstream in `e2b`.

```bash
# A) npm
npm install @sema-agent/server
MODEL_GATEWAY_BASEURL=https://api.deepseek.com MODEL_ID=deepseek-v4-flash \
  MODEL_API_KEY=<your-key> SERVICE_AUTH_TOKEN=<pick-one> \
  node node_modules/@sema-agent/server/dist/main.js        # → :8090

# B) container (zero local deps, anonymous pull)
docker run -p 8090:8090 \
  -e MODEL_GATEWAY_BASEURL=https://api.deepseek.com -e MODEL_ID=deepseek-v4-flash \
  -e MODEL_API_KEY=<your-key> -e SERVICE_AUTH_TOKEN=<pick-one> \
  ghcr.io/sema-agent/sema-server:latest              # or docker.io/claybobby/sema-server:latest

# Submit a task
curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>" \
  -H 'content-type: application/json' -d '{"objective":"Answer in one sentence: what is 1+1?"}'
# To pin a model: add "model":"<catalog id>" to the body; see GET /v1/capabilities for what is available
```

- **Where your files have to be**: a caller-supplied `cwd` is a path on the machine the **server process**
  runs on, and it is honored only on the single-user `REMOTE_EXEC=host` lane — that lane is by definition
  *this* box (no container boundary). So "server on box A, my repo on box B" does not work: nothing maps
  B's paths into A. Run the server on the box that holds the files (or `run-local` there), or use a
  container lane (`e2b` / `k8s` / `local-docker`) where the workspace is the sandbox's own and `cwd` is
  ignored.
- **Distribution coordinates**: npm = [`@sema-agent/server`](https://www.npmjs.com/package/@sema-agent/server)
  (public on npmjs) · images = `ghcr.io/sema-agent/sema-server` + `docker.io/claybobby/sema-server`
  (both public; `:latest` rolling, `:<sha>` pinned).
- **Bundled binaries**: the package ships two `bin` entries — `run-local` (single-machine local
  runner) and `sema-up` (deployment bootstrap script).
  - `run-local` boots the same engine in-process, runs one task from the CLI objective, prints the
    result and exits — no HTTP server, no submission auth gate.
  - **The deployment governance knobs apply here too** (since 7.5.0 — before that this leg assembled its
    task with the governance chain absent, so the knobs were silently inert): `AUTONOMY`,
    `runtime.commandPolicy` (from `config.d/governance.json`) and `SENSITIVE_WRITE_PATTERNS` are compiled
    onto the local task exactly as they are on the HTTP leg, tighten-only, anchored at the
    `--workspace` directory.
  - **Off switches**: `SENSITIVE_WRITE_PATTERNS=off` **or** an empty value (`SENSITIVE_WRITE_PATTERNS=`)
    disables the guard set; unset `AUTONOMY` (or set it to `auto`) for no extra tightening. A guard-set
    value that cannot compile (e.g. a pattern with no path segment, `/`) refuses to start with a message
    naming the knob, instead of failing once per task.
  - **Approvals have no durable park on this leg** — a one-shot CLI has no `/v1/approvals/:id/decide` to
    resume from. A gated `ask` (e.g. `AUTONOMY=ask`, which routes every shell command through approval)
    is answered inline: a `y/N` prompt when stdin is a TTY, otherwise a fail-closed **deny** with a
    stderr line naming the knob that produced the gate.
- **Full-stack, one command** (DB + object store + registry web + sandbox pool; Docker and k8s
  paths): [`sema-agent/sema-deploy`](https://github.com/sema-agent/sema-deploy).
- **Sandbox package sources**: default = official upstreams (pypi/npmjs/crates.io/…). For
  deployments in mainland China set `SANDBOX_PKG_SOURCE=cn` (tuna/npmmirror/rsproxy/aliyun
  mirrors); for custom mirrors use `SANDBOX_PKG_SOURCE=custom` plus explicit
  `SEMA_*_MIRROR/INDEX/REGISTRY` URLs.

## Configuration

The server is configured entirely through environment variables. The most important ones:

| Variable | Default | What it does |
|----------|---------|--------------|
| `PORT` | `8090` | HTTP listen port |
| `BIND_HOST` (alias `HOST`) | see note | Listen address. An explicit `BIND_HOST` **always wins**. Default: `127.0.0.1` when the write face is unauthenticated (`ALLOW_UNAUTHED_WRITES=true` **and** no service token configured), otherwise all interfaces — deployments with a token are unaffected. Since 3.15.0 the `HOST` alias is **not** fully equivalent: shells commonly set `HOST` to the machine name without the operator knowing, so when the narrowing condition above holds it wins over an inherited `HOST` (logged as `bind_host_from_HOST_env_overridden`). Set `BIND_HOST` explicitly, or configure a service token, to expose the write face. |
| `MODEL_GATEWAY_BASEURL` | `http://127.0.0.1:8000/v1` | OpenAI-compatible gateway base URL (without `/chat/completions`) |
| `MODEL_ID` | **required** | Default model id — **no factory default since 3.0.0**. Unset ⇒ the server refuses to boot with a message naming the knob (the old baked-in default was an internal-only model name, so every external deployment failed later and further from the cause: a gateway `400` plus a cascade of title-hook warnings). Set it to whatever model name your gateway serves, or supply the catalog via the config-center control plane |
| `MODEL_API_KEY` | — | Gateway API key (optional) |
| `SERVICE_AUTH_TOKEN` | — | Callers must send `Authorization: Bearer <token>` |
| `DB_BACKEND` | `local`* | `mysql` (any MySQL-protocol DB: MySQL/TiDB/MariaDB) / `pg` (PostgreSQL) / `local` (file-backed, no DB) / `memory` (explicit in-memory: nothing survives a restart, durable-runs faces 501). *Bare boot (no DB env at all) defaults to `local` so a single-user machine keeps its runs across restarts; any SQL signal (`SESSION_BACKEND` or `MYSQL_/PG_HOST`) keeps the `mysql` engine default, and `REQUIRE_PRINCIPAL=true` bare boots stay `memory`. (`TIDB_HOST`/`_PORT`/`_USER`/`_PASSWORD`/`_DATABASE`/`_POOL_SIZE` are retired names, not an alias — since 5.0.0 setting any of them refuses to boot, pointing at the `MYSQL_*` replacement; TiDB itself connects fine through `MYSQL_*`, since it speaks the MySQL protocol.) (the local file store has no tenant isolation — a warning says so). A DEFAULT-derived `local` that cannot create its data root degrades to memory with a warning + the `store_backend_degraded` gauge; an EXPLICIT `DB_BACKEND=local` fails loud instead. Setting `mysql`/`pg` explicitly also switches sessions to durable |
| `SESSION_BACKEND` | `memory`* | `memory` / `mysql` (durable session center) / `auto`. *Defaults to durable when `DB_BACKEND` is explicitly `mysql`/`pg`. (`tidb` is a RETIRED public name — setting it refuses to boot; since 7.57.0 `GET /v1/config/catalog` also echoes the public word `mysql` instead of the internal `tidb` label) |
| `REMOTE_EXEC` | unset | Sandbox execution lane: `host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`; unset = in-process stub (with `CONFIG_PROVIDER=local` the default becomes `host`). Naming a lane without its required env (e.g. `e2b` without `E2B_API_KEY`) or an unrecognized value **refuses to start** — no silent downgrade to the host/in-process lane (fail-closed). Under `CONFIG_PROVIDER=local` a `config.d/remote-exec.json` **wins over** this env (deliberate: the file is the single-machine source of truth). Since 7.57.0 that preemption is **loud** — `remote_exec_lane_preempted_by_file` names the preempted lane, the effective one, and whether it is an **isolation downgrade** (an isolated lane replaced by a non-isolated one); a file naming a not-yet-wired isolated lane (`e2b`/`k8s`/`ssh`/`adb`) leaves the env lane in force and says so |
| `SSH_HOST_FINGERPRINT` / `SSH_KNOWN_HOSTS` | unset | **SSH lane host-key verification** (`REMOTE_EXEC=ssh` only). `SSH_HOST_FINGERPRINT` = the host key's SHA256 base64 fingerprint (`SHA256:` prefix and `=` padding both optional); `SSH_KNOWN_HOSTS` = a path to a `known_hosts` file, matched literally on `host` / `[host]:port`. **Either one present ⇒ strict verification, a mismatch REFUSES the connection** (the error carries both fingerprints plus an `ssh-keyscan` line); both set ⇒ both must pass. **Both unset ⇒ a loud per-connection warning (`ssh_host_key_unverified`, man-in-the-middle risk) and the connection proceeds** — the deliberate posture for the batch-deploy lane, registered as a P-DEBT fail-open (`docs/FAIL-OPEN-CENSUS.md`). A malformed fingerprint or an unreadable `known_hosts` path **refuses to start**. Not supported in `known_hosts`, on purpose: hashed (`\|1\|…`) lines, wildcard patterns, `@cert-authority` — all skipped, so a host whose only entries are those is REFUSED (`@revoked` is honored and wins over any plain line, whatever the order) |
| `HOOKS_TIMEOUT_MS` | unset (engine default 600000) | Per-invocation time bound for every hook seat (core `Hooks.timeoutMs`). Unset = the engine's own default (this server does not copy upstream defaults). `0` is honored as written (every seat expires immediately). A non-integer, negative, or above-`setTimeout`-ceiling (2147483647) value **refuses to start** |
| `CONFIG_PROVIDER` | unset | Config source: `local` (file-backed `config.d/`, single machine) / `remote` (registry control plane) |
| `DEFAULT_SCENARIO` | `code` | Default scenario when the request body names none |
| `SANDBOX_PKG_SOURCE` | `global` | Package sources inside sandboxes: `global` (official upstreams) / `cn` (China mirrors) / `custom` / `none` |
| `SENSITIVE_WRITE_PATTERNS` | core's recommended set | Sensitive-path write deny list; comma-separated value replaces the set, `off` **or an empty value** disables. Applied unconditionally at the governance layer (independent of client permission mode, lane or settings presence) — including on the `run-local` leg. A value that cannot compile into a guard set (e.g. `/`, a pattern with no path segment) refuses to start |
| `WRITE_PROTECTED_EXTRA` | unset (engine default table) | **Adds** rows to core's write-protection table (the literal-name table whose hit demotes a surviving `allow` to `ask` on Write/Edit/NotebookEdit). Comma-separated bare names (a bare name matches ANY path segment; a name containing `/` matches a consecutive segment run) or a JSON array of `"name"` strings / `{name, kind}` rows (`kind`: `basename` \| `segment` \| `segment-run`). The value is composed as `[...WRITE_PROTECTED_DEFAULT_TABLE, …]`, so **no default row can be lost**. Unset = no seat is wired = the engine's default table is in force (this server never copies that table). Form discrimination is by content, not by first character: a value containing any JSON structural character (`[ ] { } "`) is parsed as JSON and **must** be a top-level array (a missing pair of brackets refuses to start instead of being split into junk bare names). An empty value, a malformed entry (glob metacharacter, unknown kind, kind/name mismatch) or setting this together with `WRITE_PROTECTED_TABLE_REPLACE` **refuses to start**. Read faces: `GET /v1/capabilities` → `writeProtection.{armed,rows,replaced}`; `GET /v1/diagnostics/wiring` → `writeProtection.{rows,source,droppedDefaultRows}` (operator-only) |
| `WRITE_PROTECTED_TABLE_REPLACE` | unset (engine default table) | **Replaces the whole** write-protection table (core's seat is whole-table by contract). JSON array only — deliberately no comma shorthand, because a slipped bare string would swap 51 default rows for one. `[]` = the explicit "no write-protection table at all" posture. Replacing logs one **loud** boot line naming every default row you dropped (`write_protection_table_replaced`; the empty posture logs `write_protection_table_disabled`) — use `WRITE_PROTECTED_EXTRA` when you meant to ADD. Same refuse-to-start conditions as its sibling, plus: both knobs set = two writers on one surface = refuses to start |
| `MANUAL_MODE_SHELL_GATE` | unset | `always`\|`classify` — tighten `Bash` into the approval chain, applied unconditionally at the governance layer (≥7.1.0: independent of client permission mode, lane, or settings presence). **Unset is not "off"**: since 7.12.0 the caller's explicit `permissionMode` supplies the baseline this knob tightens from (`bypassPermissions` → `off`, `auto`/`default`/`acceptEdits`/`plan` → `classify`; **no** mode stated → core's `off` default). Since 7.73.0 (core 7.15.0) `off` no longer means "no gate at all": the READ BOUNDARY (built-in read-deny tiers + workspace containment) is judged under **every** doctrine, and `shellGate` governs only the RESIDUAL shell risk. Since 7.92.0 (core 7.25.0) that boundary is ONE read station judged before approval, and its three outcomes differ: a **deny-listed read is REFUSED** on the `off` lane as on every other face — no card, no stored rule and no approver's yes can release it (`tool_end.errorCode`/`structured` = `read_path_denied` with the matched `target`/`pattern`, `gate.disposition.deniedBy = "read_boundary"`); a **recursive read form is ENUMERATED** — it runs with zero asks when no deny row sits under the tree, is refused naming that row when one does, and still raises ONE mandated approval when the tree is too large to enumerate; an **out-of-root read** is unchanged and still raises exactly ONE mandated approval (no stored rule and no auto-mode classifier can clear it). Non-readers (`rm`/`curl`/`git`/`npm`) and ordinary in-workspace reads stay unasked on that lane. This knob only ever raises that baseline — it has no relax half, so `off` is accepted as an explicit **no-op** (a boot line says so; not symmetric with `SENSITIVE_WRITE_PATTERNS=off`, which really does clear a set). **Any other value refuses to start** (7.12.0, BREAKING for a deployment that had a typo: it was previously treated as unset, i.e. silently no gate at all) |
| `PERMISSIONS_DISABLE_AUTO_MODE` | `false` | Local mirror of CC `permissions.disableAutoMode` — the **org deny** bit for `permissionMode:"auto"` (paired with core's intent-arming rule). **Tighten-only**: `true` folds every principal's `runtimeCaps.autoMode` to `false` (a center grant cannot flip it back); unset leaves caps untouched, so on a center-less box a shell asking for `auto` **arms** the classifier once the paired core (intent-arming rule "requested ∧ classifier seat ∧ `autoMode !== false`" — absence is not a deny) is installed; on core 7.2.0 the engine still uses the old "org grant" rule (`permissionModeAuto.intentArming:false`), so the self-check answers `armed:true` only for a center-granted principal and `deployment_incapable` on a center-less box. Boolean word table; any other value refuses to start. Self-check: `GET /v1/capabilities?permissionMode=auto` → `permissionModeAuto.{armed, reason, model}` (USAGE §9.4) |
| `SCRATCHPAD_SWEEP_TTL_MS` | 7 days | Idle-reap window for per-session scratchpad dirs (by dir mtime; `0` disables). The scratchpad is **ephemeral by contract**: replica-local disk, NOT part of the durable-suspend persistence set — a resume on a different replica, or after a sweep, starts with an empty dir (same two-track posture as the Agent SDK hosting doc: conversation persists, working-directory artifacts don't). Raise/disable only on single-replica deployments that park approvals for longer than the window |
| `MODEL_CONNECT_TIMEOUT_MS` | `30000` | Gateway connect timeout |
| `MODEL_FIRST_TOKEN_TIMEOUT_MS` | `600000` | First-token timeout (the only watchdog for a stream that opens and never emits; `0` = off. Raised from `120000` in 7.71.0 — a self-hosted backend's long prefill legitimately takes minutes to the first byte; set `120000` to restore the old posture) |
| `MODEL_IDLE_TIMEOUT_MS` | `300000` | Mid-stream idle timeout (`0` = off) |
| `LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error` (structured JSON logs) |

**Boolean knobs** accept `true`/`false`/`1`/`0`/`yes`/`no`/`on`/`off` (case-insensitive; word table widened
in 7.16.0). Any other value makes the process **refuse to boot** — the error names the env var, the value it
received, and the accepted word list — so a mistyped switch is caught at start, not silently defaulted (the
older `config_env_invalid_using_default`-warn-and-keep-default behavior no longer applies to booleans; that
event now fires only for other knob kinds, e.g. a numeric knob out of range). The default is not guessable
from the name, so the server prints one
`config_knob_polarity` line per knob at `LOG_LEVEL=debug`: name → polarity (`opt-in` = default OFF,
`opt-out` = default ON, `posture` = derived from `REQUIRE_PRINCIPAL`) → effective value → where it came from.
The table describes the knobs live in *this* configuration — a knob gated on an inactive lane (e.g.
`K8S_INSECURE_TLS` outside the k8s provider, `E2B_ALLOW_NET` when unset) has no row, which means
"not live here", not "off".

The full surface — cost/quota ceilings, circuit breaker and failover, multi-model roles, approval
gates, observability (Prometheus `/metrics` + optional OTLP), registry control plane, per-lane
sandbox settings — is documented in [`USAGE.md`](USAGE.md). Three separate knobs cap spend and they
stop different things at different moments (per-task ceiling / per-principal ADMISSION gate that does
not interrupt a run already executing / deployment usage window that does stop one at a turn
boundary) — the side-by-side table is in `USAGE.md`, worth reading before picking one.

Web search (deployment `WEB_SEARCH_*` env, per-request `settings.webSearch`, the two-lane precedence
and the tool-level error forms) is documented in `USAGE.md` §9.5.

## Security & privacy note

> 🔴 **Trace redaction ≠ egress control** (anchor: 脱敏 ≠ 出站控制). Trace redaction (the `«redacted»` markers you see in the
> ledger / SSE / turns / approval previews) only rewrites the **persistence and display** surfaces —
> **the request body carrying a tool result to the model's actual route is the original, unredacted
> text.** That route is not just `MODEL_GATEWAY_BASEURL`: it can also be the fallback gateway list
> (`MODEL_GATEWAY_FALLBACK_URLS`), an independent Anthropic route for `provider:"anthropic"` models
> (defaults to `https://api.anthropic.com` unless `ANTHROPIC_BASEURL` is set), or a per-model `baseUrl`
> override the registry can hand a single catalog model (see `docs/ARCHITECTURE.md` §10). To keep
> content from leaving the machine, rely on the **read-side sensitive-path gate** and confirm **every
> route this deployment can select** points at infrastructure you control — pointing only the default
> gateway env at a private endpoint is not sufficient. Verification method: `USAGE.md` §9.6.

## HTTP API overview

One row per endpoint family (not exhaustive):

| Endpoint family | What it serves |
|-----------------|----------------|
| `GET /health` · `GET /metrics` | Liveness + Prometheus metrics (`/metrics/summary`, `/metrics/plan-cache`) |
| `GET /v1/capabilities` | Deployment capability discovery — what this deployment can actually do, so clients never probe 501s |
| `GET /v1/capabilities/mcp` · `POST /v1/capabilities/mcp/probe` | Per-server MCP status with **no run required**: dial each declared server, list its tools, close it again — the first for this deployment's own declarations, the second for a caller's `.mcp.json`. Rows are the engine's own wiring-manifest rows, so a one-shot command and an interactive session read the same verdict. Rate-capped per caller and cached briefly (it really dials) |
| `GET /v1/models` | Model catalog (names only; no gateway URLs or keys) |
| `POST /v1/tasks` · `/v1/tasks/stream` | Synchronous task execution; SSE variant streams typed `TaskEvent`s token by token |
| `POST /v1/runs` · `GET /v1/runs/:id` | Asynchronous runs: immediate `202`, background execution, poll for status/result |
| `GET /v1/runs/:id/events` | Replayable SSE (`Last-Event-ID` resume); any replica can serve any run |
| `POST /v1/runs/:id/cancel` / `steer` / `interrupt` / `compact` · `/v1/runs/:id/subagents/:target/steer` / `resume` | Run control verbs: cooperative cancel, mid-run steering, turn-level interrupt (with `text`: cut the in-flight turn + steer, run continues; bare body: halt — cut + stop, the run ends completed with `haltedByUser:true`, session resumable), context compaction, sub-agent steering/resume |
| `/v1/approvals` (list · decide · stream) | Human-in-the-loop approval center backed by durable checkpoints; decisions from any replica |
| `/v1/sessions` (list · get · fork · init · settings · wake) | Session listing/search, audit inspection, fork, startup bundle |
| `/v1/workflows` (list · get · stream · agents/:label/steer) | Deterministic workflow orchestration runs with live streams and per-agent steering |
| `GET /v1/usage` · `GET /v1/policy` | Cumulative spend (when quotas are configured) and effective policy read surface |

## Ecosystem

| Repository | What it is |
|------------|------------|
| [sema-agent/sema](https://github.com/sema-agent/sema) | The `sema` CLI portal — your own Claude Code-grade agent: terminal, web, your cloud |
| [sema-agent/sema-core](https://github.com/sema-agent/sema-core) | The agent engine, as a library — published as [`@sema-agent/core`](https://www.npmjs.com/package/@sema-agent/core) |
| [sema-agent/sema-sdk](https://github.com/sema-agent/sema-sdk) | Official TypeScript SDK for this server (`@sema-agent/sdk`) |
| [sema-agent/sema-deploy](https://github.com/sema-agent/sema-deploy) | One-command deployment — docker compose or k8s (helm), single machine to multi-node HA |
| [sema-agent/sema-web](https://github.com/sema-agent/sema-web) | Self-hosted web console + registry/config center + orchestrator |

## Versioning

1.x is a fast-iteration line: **breaking changes may land in minor versions** (tracked in
[`MIGRATION.md`](MIGRATION.md)). Production deployments should pin an exact version
(e.g. `@sema-agent/server@1.214.1`). Strict semver starts with 2.0 after GA.

## License

[BUSL-1.1](LICENSE) (Business Source License):

- **Free** for personal, educational, research, and non-commercial production use.
- **Commercial production use requires a commercial license** from the licensor.
- **Converts to Apache-2.0 on 2030-07-13.**

Published copies at version ≤ 1.180.1 remain under the MIT terms they shipped with.
