<div align="center">

# @sema-agent/server

**Sema 技术栈的服务端/API 层 —— 装配引擎,服务舰队。**

[![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)](#许可协议)

[快速开始](#快速开始) · [配置](#配置) · [HTTP API](#http-api-概览) · [生态导航](#生态导航) · [许可协议](#许可协议)

[English](./README.md)

</div>

---

## 它是什么

`@sema-agent/server` 是 Sema 的服务端/API 实现层:把
[`@sema-agent/core`](https://www.npmjs.com/package/@sema-agent/core) 引擎、registry 配置、
模型网关与云端 agent 执行能力,装配到一套 HTTP/SSE 契约后面。

**它是:**

- **一个 HTTP/SSE 服务端 + 装配层。** 请求体只带内容(`objective`、`sessionId`、`scenario` 等);
  服务端按任务装配其余一切 —— 工具、子智能体角色册、提示词、skill、策略 ——
  身份/会话/安全策略由服务端注入,不信 body。凭据只活在服务端闭包里,
  不进请求体、不进模型、不进沙箱。
- **无状态设计。** 副本是 cattle:`docker run` 即起一个,挂在负载均衡后想开多少开多少。
  持久化状态 —— 会话、run、可重放事件日志、检查点、审批 —— 全在外部 SQL 存储
  (任何 MySQL 协议数据库:MySQL/TiDB/MariaDB,或 PostgreSQL;每条池连接自钉会话隔离级(MySQL 协议腿 `REPEATABLE READ`、PostgreSQL `READ COMMITTED`)并回读复核,TiDB 上会话切到悲观事务模式、切不成或复核不符才拒启,见 docs/DEPLOY-PREREQS.md;单机场景另有文件持久化的
  `local` 模式)。任意副本都能服务任意 run 的事件流;任务可以在一个副本上挂起、在另一个副本上恢复。
- **完整的服务端能力面**(可在 `GET /v1/capabilities` 发现):
  异步 run + 可重放 SSE、持久化检查点 + 人工审批(HITL)、会话与长期记忆、
  一份镜像服务多场景、任务级子智能体、确定性 workflow 编排,以及可插拔的沙箱执行通道
  (`host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`)。

**它不是:**

- **不是引擎本体。** agent 循环、工具 harness、记忆与检查点机制在
  [`sema-core`](https://github.com/sema-agent/sema-core);本仓以 npm 依赖的方式消费它,
  自己出装配、后端与服务契约。
- **不是 CLI。** 终端智能体是 [`sema`](https://github.com/sema-agent/sema);它(以及网页端)
  都是本服务的客户端。
- **不是部署工具。** 一键自托管部署(Docker 单机或 Kubernetes)在
  [`sema-deploy`](https://github.com/sema-agent/sema-deploy)。
- **不是持久化中心。** 真相在外部数据库里;服务进程随时可弃。

## 架构

<!-- TODO: 架构 SVG —— 视觉资产归门户仓(sema-agent/sema);下面是文字版。 -->

<details>
<summary>文字版</summary>

```
  HTTP / SSE API 面           装配层                       执行通道
  ────────────────            ──────                       ────────
  /v1/tasks   (同步) ──┐   ┌─ 任务级 spec 装配     ─┐   ┌─ host          (本机直跑)
  /v1/runs    (异步) ──┤   │  场景 · skill          │   ├─ local-docker  (任务级容器)
  /v1/sessions       ──┼──▶│  策略 · 审批门         │──▶├─ e2b           (Firecracker VM)
  /v1/approvals      ──┤   │  registry 配置         │   ├─ k8s           (Kata pod 沙箱)
  /v1/workflows      ──┘   │  模型网关              │   └─ ssh / adb     (真机 / 真设备)
                           └─ @sema-agent/core     ─┘
                                     │
                    持久化存储(MySQL/TiDB · PostgreSQL · 本地文件)
                    会话 · run · 事件日志 · 检查点 · 审批
```

</details>

## 快速开始

环境要求:Node ≥ 20(npm 路径)+ 一个 OpenAI 兼容模型网关。

> **关于安装时那条 `glob@11` 弃用警告。** `npm install` 会为 `glob@11.1.0` 打一条 deprecated 警告,它由
> `e2b`(E2B 沙箱 SDK)间接引入。这是**安装期噪声,在这里没有任何运行时曝露面**:`glob` 在 `e2b` 里的
> 唯一加载点是它 *template-build* 打包路径里的一处 `dynamicImport`,而本服务只使用 E2B 的**沙箱运行时**
> 接口。这一条是**跑出来的、不是读代码推断的**:import `e2b`、构造适配器、真调一次 `exec`,全程 `glob`
> 从未进入模块缓存。**我们是刻意不消它的。** 唯一真能消掉的办法是把 e2b 整棵子树 bundle 进本包
> (实测有效,警告确实消失),代价是解包体积 2.7 MB → 20.8 MB、`npm ls` 会把依赖树标成 `invalid`、
> 且你装到的 `e2b` 与 e2b 官方发布的那份脱钩 —— 为一条没有运行时影响的警告付这些,不划算。
> **顺带告诉你哪些做法没用**(免得你去试):我们写的 `overrides`(只在"被读的那份 package.json 正是
> npm 本次调用的项目清单"时生效),以及随包发布的 `npm-shrinkwrap.json`(本包作为依赖被安装时同样被
> 忽略)—— 两条都是实测。若这条警告碍事,把 `"overrides": { "glob": "^13" }` 加进**你自己项目**的
> package.json(那份才是 npm 会读的;`glob@13` 与 e2b 用到的 API 兼容,已验)。真正的修复在上游 `e2b`。

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

# B) 容器(零依赖,匿名可拉)
docker run -p 8090:8090 \
  -e MODEL_GATEWAY_BASEURL=https://api.deepseek.com -e MODEL_ID=deepseek-v4-flash \
  -e MODEL_API_KEY=<你的-key> -e SERVICE_AUTH_TOKEN=<自定> \
  ghcr.io/sema-agent/sema-server:latest              # 或 docker.io/claybobby/sema-server:latest

# 提交一个任务
curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>" \
  -H 'content-type: application/json' -d '{"objective":"用一句话回答:1+1 等于几?"}'
# 带指定模型:body 加 "model":"<catalog id>";可用能力见 GET /v1/capabilities
```

- **分发坐标**:npm = [`@sema-agent/server`](https://www.npmjs.com/package/@sema-agent/server)
  (npmjs 公开)· 镜像 = `ghcr.io/sema-agent/sema-server` + `docker.io/claybobby/sema-server`
  (均 public,`:latest` 滚动 / `:<sha>` 钉版)。
- **随包二进制**:包内带两个 `bin` —— `run-local`(单机本地 runner)与
  `sema-up`(部署引导脚本)。
  - `run-local` 在进程内启同一套引擎,把命令行给的目标跑完一次、打印结果、退出——没有 HTTP 服务,
    也没有提交鉴权门。
  - **部署治理旋钮在这条腿上同样生效**(7.5.0 起;此前这条腿自建任务时整条治理链缺席,旋钮静默失效):
    `AUTONOMY`、`runtime.commandPolicy`(来自 `config.d/governance.json`)与 `SENSITIVE_WRITE_PATTERNS`
    与 HTTP 腿同样地 tighten-only 编译进本地任务,裁决锚在 `--workspace` 目录。
  - **关闸通道**:`SENSITIVE_WRITE_PATTERNS=off` **或**留空(`SENSITIVE_WRITE_PATTERNS=`)都关掉守卫集;
    `AUTONOMY` 不设(或设 `auto`)= 不额外收紧。守卫集若给了**编译不出来**的值(例如 `/` 这种不含任何
    路径段的模式),启动即拒并指名旋钮,而不是每个任务炸一次。
  - **这条腿没有 durable 审批 park**:一次性 CLI 没有 `/v1/approvals/:id/decide` 可赎回。被门住的 `ask`
    (例如 `AUTONOMY=ask`,它把每条 shell 命令都送进审批链)当场结算:stdin 是 TTY 就 `y/N` 问人,
    否则 fail-closed **拒绝**并在 stderr 点名是哪个旋钮产的这道门。
- **一键全栈部署**(DB + 对象存储 + registry 网站 + 沙箱池,docker/k8s 双路径):
  [`sema-agent/sema-deploy`](https://github.com/sema-agent/sema-deploy)。
- **沙箱装包源**:缺省 = 官方源(pypi/npmjs/crates.io/…)。中国大陆部署配
  `SANDBOX_PKG_SOURCE=cn` 切国内镜像源(tuna/npmmirror/rsproxy/aliyun),自定义源用
  `SANDBOX_PKG_SOURCE=custom` + `SEMA_*_MIRROR/INDEX/REGISTRY` 显式 URL。

## 配置

服务完全由环境变量配置。最核心的一批:

| 变量 | 默认 | 说明 |
|------|------|------|
| `PORT` | `8090` | HTTP 监听端口 |
| `BIND_HOST`(别名 `HOST`) | 见说明 | 监听地址。显式 `BIND_HOST` **恒生效**。缺省:写面无鉴权时(`ALLOW_UNAUTHED_WRITES=true` **且**未配任何 service token)= `127.0.0.1`,否则全接口——配了 token 的部署不受影响。自 3.15.0 起 `HOST` 别名**不再完全等效**:shell 常把 `HOST` 设成机器名而 operator 并不知情,于是上述收窄条件成立时,收窄压过继承来的 `HOST`(告警事件 `bind_host_from_HOST_env_overridden`)。要暴露写面,显式设 `BIND_HOST`,或配一个 service token。 |
| `MODEL_GATEWAY_BASEURL` | `http://127.0.0.1:8000/v1` | OpenAI 兼容网关地址(不带 `/chat/completions`) |
| `MODEL_ID` | **必填** | 缺省模型 id ——**3.0.0 起无出厂缺省**。未设 = 启动即失败并指路该旋钮(旧的烤死缺省是内网模型名,外部部署必炸且炸在离根因最远处:网关 `400` + 标题 hook 连环告警)。填你的网关真正提供的模型名,或改由配置控制面下发目录 |
| `MODEL_API_KEY` | — | 网关 key(可选) |
| `SERVICE_AUTH_TOKEN` | — | 调用方需带 `Authorization: Bearer <token>` |
| `DB_BACKEND` | `local`* | SQL 引擎:`mysql`(任何 MySQL 协议库:MySQL/TiDB/MariaDB —— TiDB 走这个值,**没有** `tidb` 别名,写它启动即拒)/ `pg`(PostgreSQL)/ `local`(免 DB 文件持久化)/ `memory`(显式纯内存)。显式设置 `mysql`/`pg` 时 session 自动转 durable。*单租户裸 boot 缺省 `local`;`REQUIRE_PRINCIPAL=true` 的多租户裸 boot 缺省 `memory`(local 与多租户互斥) |
| `SESSION_BACKEND` | `memory`* | `memory` / `mysql`(durable 会话中心)/ `auto`。*显式 `DB_BACKEND=mysql/pg` 时默认转 durable。(`tidb` 是**退役公名**,设了拒启;7.57.0 起 `GET /v1/config/catalog` 的回显也归一成公名 `mysql`,不再吐内部标签 `tidb`) |
| `REMOTE_EXEC` | 未设 | 沙箱执行通道:`host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`;未设 = 进程内 stub(`CONFIG_PROVIDER=local` 时缺省转 `host`)。点名了通道但必需 env 不全(如 `e2b` 缺 `E2B_API_KEY`)或值不在闭集内 ⇒ **启动即拒**——不再静默降级到 host/进程内通道(fail-closed)。`CONFIG_PROVIDER=local` 下 `config.d/remote-exec.json` **压过**本 env(刻意:单机形的真源是那份文件);7.57.0 起这次抢占**响亮**——`remote_exec_lane_preempted_by_file` 点名被抢占的 lane、最终生效的 lane,以及它是不是一次**隔离降级**(隔离 lane 被换成不隔离的);文件点了尚未接线的隔离 lane(`e2b`/`k8s`/`ssh`/`adb`)时 env 腿照旧生效并说明 |
| `SSH_HOST_FINGERPRINT` / `SSH_KNOWN_HOSTS` | 未设 | **SSH 通道主机密钥校验**(仅 `REMOTE_EXEC=ssh`)。前者 = 主机公钥的 SHA256 base64 指纹(`SHA256:` 前缀与 `=` 填充都可选);后者 = `known_hosts` 文件路径,按 `host` / `[host]:port` **逐字**匹配行。**任一在场 ⇒ 严格校验,不符即拒连**(拒因带两半指纹 + `ssh-keyscan` 取指纹指路一行);两只同时在场 ⇒ 合取(都要过)。**两只全缺席 ⇒ 每连接一条响亮 warn(`ssh_host_key_unverified`,中间人风险)后照常连** —— 批量部署通道的既定姿态,已登记为 P-DEBT fail-open(`docs/FAIL-OPEN-CENSUS.md`)。指纹拼错 / `known_hosts` 路径读不到 ⇒ **启动即拒**。`known_hosts` 里**刻意不支持**:hashed(`\|1\|…`)行、通配符 pattern、`@cert-authority` —— 一律跳过,于是只有这些条目的主机会被**拒连**(`@revoked` 照常生效,且无论写在哪一行都优先于普通匹配行) |
| `HOOKS_TIMEOUT_MS` | 未设(引擎缺省 600000) | 每一只 hook 席位**单次调用**的时间上限(core `Hooks.timeoutMs`)。未设 = 用引擎自己的缺省(本服务不复制上游默认值)。`0` **按字面生效**(每个席位立即到期)。非整数 / 负数 / 超 `setTimeout` 上限(2147483647)⇒ **启动即拒** |
| `CONFIG_PROVIDER` | 未设 | 配置来源:`local`(单机文件 `config.d/`)/ `remote`(registry 控制面) |
| `DEFAULT_SCENARIO` | `code` | 请求体未指定场景时的缺省场景 |
| `SANDBOX_PKG_SOURCE` | `global` | 沙箱内装包源:`global`(官方源)/ `cn`(国内镜像)/ `custom` / `none` |
| `SENSITIVE_WRITE_PATTERNS` | core 推荐集 | 敏感路径写拒集;逗号分隔值为整体替换,`off` **或留空**关闭。在治理层无条件施加(与客户端权限模式/lane/settings 在场性无关),`run-local` 腿同样生效;编译不出守卫集的值(如 `/`)启动即拒 |
| `WRITE_PROTECTED_EXTRA` | 未设(引擎缺省表) | 给 core 的**写保护名表**加行(字面名表:命中即把幸存的 `allow` 降级成 `ask`,作用于 Write/Edit/NotebookEdit)。逗号分隔裸名(裸名匹配**任意路径段**,含 `/` 的名匹配连续段)或 JSON 数组(`"name"` 串 / `{name, kind}` 行,`kind`:`basename` \| `segment` \| `segment-run`)。值按 `[...core 缺省表, …]` 组合,**丢不掉任何缺省行**。未设 = 不铸座 = 引擎缺省表在岗(本仓从不复制那张表)。两形按**内容**判而不是猜首字符:值里出现 JSON 结构字符(`[ ] { } "`)即按 JSON 解析,且顶层**必须是数组**(少写一对方括号 ⇒ 拒启,而不是被拆成垃圾裸名静默收下)。空值 / 坏值(通配符、未知 kind、kind 与名字段数矛盾)/ 与 `WRITE_PROTECTED_TABLE_REPLACE` 同时设置 ⇒ **启动即拒**。读面:`GET /v1/capabilities` 的 `writeProtection.{armed,rows,replaced}`;`GET /v1/diagnostics/wiring` 的 `writeProtection.{rows,source,droppedDefaultRows}`(operator-only) |
| `WRITE_PROTECTED_TABLE_REPLACE` | 未设(引擎缺省表) | **整表替换**写保护名表(core 的座按契约就是整表)。只收 JSON 数组 —— 刻意不给逗号简写:一个手滑的裸串会把 51 行换成 1 行。`[]` = 显式「完全不要这张表」。替换时 boot 期发一条**响亮**日志逐名列出被丢的缺省行(`write_protection_table_replaced`;空表走 `write_protection_table_disabled`)—— 想「加两行」请用 `WRITE_PROTECTED_EXTRA`。拒启条件同姊妹键,外加:两根同写 = 一条语义面两个写者 ⇒ 拒启 |
| `MODEL_CONNECT_TIMEOUT_MS` | `30000` | 网关连接超时 |
| `MODEL_FIRST_TOKEN_TIMEOUT_MS` | `600000` | 首 token 超时(开流不吐字的唯一看门狗;`0`=关。7.71.0 起由 `120000` 提高 —— 自托管后端长 prefill 下首字合理地就要等几分钟;要旧姿态显式设 `120000`) |
| `MODEL_IDLE_TIMEOUT_MS` | `300000` | 流中 idle 超时(`0` 关) |
| `LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error`(结构化 JSON 日志) |

**布尔旋钮收 `true`/`false`/`1`/`0`/`yes`/`no`/`on`/`off`**(大小写不敏感;词表自 7.16.0 起放宽)。
词表**外**的值会让进程**直接拒启**——报错信息点名该 env 名、收到的原始值、可接受词表,打错的开关在启动
那一刻就现形,而不是静默退回默认(旧的「`config_env_invalid_using_default` 警告 + 保留缺省值」这条臂
已随本次改动退役,不再覆盖布尔旋钮;该事件现在只用于其他种类的越界配置,如数值旋钮被夹取)。
缺省值无法从名字推出,所以服务在 `LOG_LEVEL=debug` 下每个旋钮打一行 `config_knob_polarity`:
名字 → 极性(`opt-in`=缺省关、`opt-out`=缺省开、`posture`=由 `REQUIRE_PRINCIPAL` 推导)→ 生效值 → 来源。
该表描述**本配置下活着的旋钮**——挂在未激活通道上的旋钮(如非 k8s 通道下的 `K8S_INSECURE_TLS`、
未设时的 `E2B_ALLOW_NET`)没有行,含义是「在这里不生效」,不是「关」。

完整配置面 —— 成本/配额上限、断路器与 failover、多模型角色、审批门、可观测
(Prometheus `/metrics` + 可选 OTLP)、registry 控制面、各沙箱通道细项 ——
见 [`USAGE.md`](USAGE.md)。⚠️ 封顶花费的旋钮有**三根**,判的时刻和能停住什么各不相同
(单任务硬闸 / per-principal **进场门**——不打断已在跑的 run / 部署级治理窗——会在 turn 边界停住它);
选之前请先读 `USAGE.md` 里的「三道 $ 闸的分工」对照表。

Web search(部署 env `WEB_SEARCH_*` 七键、per-request `settings.webSearch`、两车道优先级、工具层错误形)
见 `USAGE.md` §9.5。

## 安全与隐私提示

> 🔴 **trace 脱敏 ≠ 出站控制。** trace 脱敏(你在账本 / SSE / turns / 审批预览里看到的 `«redacted»`
> 标记)只作用于**持久化与展示面**——**发往「该 run 实际选中的模型路由」的请求体是工具结果原文**。
> 这条路由不止 `MODEL_GATEWAY_BASEURL` 一根:还可能是备用网关列表(`MODEL_GATEWAY_FALLBACK_URLS`)、
> `provider:"anthropic"` 模型的独立直连路由(缺省根 `https://api.anthropic.com`,除非显式配
> `ANTHROPIC_BASEURL`)、或配置中心可对单个目录模型下发的 per-model `baseUrl` 覆盖(见
> `docs/ARCHITECTURE.md` §10)。要让内容不出机器,靠**读侧敏感路径门**,并核对**本部署会被选中的每一条
> 路由**都指向自己控制的基础设施——只把默认网关 env 指到私有端点是不够的。验法见 `USAGE.md` §9.6。

## HTTP API 概览

一行一个端点族(非全量):

| 端点族 | 服务什么 |
|--------|----------|
| `GET /health` · `GET /metrics` | 存活探针 + Prometheus 指标(`/metrics/summary`、`/metrics/plan-cache`) |
| `GET /v1/capabilities` | 部署能力发现 —— 本部署真正能做什么,客户端免 501 探测 |
| `GET /v1/capabilities/mcp` · `POST /v1/capabilities/mcp/probe` | **无需先跑一条 run** 的逐台 MCP 状态:现连、列工具、关掉 —— 前者答本部署自己申报的服务器,后者答调用方自带的 `.mcp.json`。行就是引擎装配名册里的那一行,所以一次性命令与交互会话读到同一个判定。它会真拨号,所以按调用方限速 + 短窗复用 |
| `GET /v1/models` | 模型目录(仅名字,不含网关 URL/key) |
| `POST /v1/tasks` · `/v1/tasks/stream` | 同步任务执行;SSE 变体逐 token 流式输出类型化 `TaskEvent` |
| `POST /v1/runs` · `GET /v1/runs/:id` | 异步 run:立即 `202`,后台续跑,轮询状态/结果 |
| `GET /v1/runs/:id/events` | 可重放 SSE(`Last-Event-ID` 续订);任意副本可服务任意 run |
| `POST /v1/runs/:id/cancel` / `steer` / `interrupt` / `compact` · `/v1/runs/:id/subagents/:target/steer` / `resume` | run 控制动词:协作式取消、运行中转向、turn 级中断(切当前在飞 turn,run 继续)、上下文压缩、子智能体转向/恢复 |
| `/v1/approvals`(list · decide · stream) | 人工审批中心,底座是持久化检查点;任意副本都能批复 |
| `/v1/sessions`(list · get · fork · init · settings · wake) | 会话列表/检索、审计回溯、fork、启动包 |
| `/v1/workflows`(list · get · stream · agents/:label/steer) | 确定性 workflow 编排 run,带实时流与逐 agent 转向 |
| `GET /v1/usage` · `GET /v1/policy` | 累计花费(配置配额时)与生效策略只读面 |

## 生态导航

| 仓库 | 是什么 |
|------|--------|
| [sema-agent/sema](https://github.com/sema-agent/sema) | `sema` CLI 门户 —— 属于你自己的 Claude Code 级智能体:终端、网页、你的云 |
| [sema-agent/sema-core](https://github.com/sema-agent/sema-core) | 智能体引擎,以库的形式发布 —— [`@sema-agent/core`](https://www.npmjs.com/package/@sema-agent/core) |
| [sema-agent/sema-sdk](https://github.com/sema-agent/sema-sdk) | 本服务的官方 TypeScript SDK(`@sema-agent/sdk`) |
| [sema-agent/sema-deploy](https://github.com/sema-agent/sema-deploy) | 一键部署 —— docker compose 或 k8s(helm),单机到多机 HA |
| [sema-agent/sema-web](https://github.com/sema-agent/sema-web) | 自托管网页控制台 + registry/配置中心 + orchestrator |

## 版本策略

1.x = 快速迭代期:**BREAKING 变更可能落在 minor**(记录于
[`MIGRATION.md`](MIGRATION.md))。生产部署请锁精确版本
(如 `@sema-agent/server@1.214.1`);GA 后切 2.0 起严格 semver。

## 许可协议

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

- 个人、教育、研究及非商业生产使用 **免费**。
- **商业生产使用需要商业授权**。
- **2030-07-13 起自动转为 Apache-2.0。**

≤ 1.180.1 的已发布副本仍受其发布时的 MIT 约束。
