@sema-agent/server 7.101.1 → 7.103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/MIGRATION.md +84 -0
  2. package/README.md +4 -4
  3. package/README.zh-CN.md +4 -4
  4. package/USAGE.md +156 -9
  5. package/dist/approval-card.d.ts +35 -2
  6. package/dist/approval-card.js +19 -1
  7. package/dist/background-session-reap.d.ts +58 -0
  8. package/dist/background-session-reap.js +90 -0
  9. package/dist/bake-runner/main.d.ts +21 -3
  10. package/dist/bake-runner/main.js +18 -11
  11. package/dist/boot/config-center.d.ts +21 -4
  12. package/dist/boot/config-center.js +62 -19
  13. package/dist/boot/exit-boundary.js +17 -0
  14. package/dist/boot/runner-deps.js +1 -0
  15. package/dist/boot/runtime-caps.js +3 -2
  16. package/dist/boot/session-faces.js +3 -4
  17. package/dist/boot/shutdown.d.ts +2 -1
  18. package/dist/boot/shutdown.js +33 -12
  19. package/dist/boot/stage-01-config.js +2 -0
  20. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  21. package/dist/boot/stage-07-capability-layer.js +6 -4
  22. package/dist/boot/stage-08-reapers.d.ts +1 -1
  23. package/dist/boot/stage-09-leader.d.ts +1 -1
  24. package/dist/boot/stage-10-http-server.d.ts +1 -1
  25. package/dist/boot/stage-10-http-server.js +13 -4
  26. package/dist/brain.d.ts +28 -8
  27. package/dist/brain.js +20 -4
  28. package/dist/capabilities/builtin-tools.d.ts +14 -1
  29. package/dist/capabilities/builtin-tools.js +3 -0
  30. package/dist/capabilities/center-plugins.d.ts +9 -3
  31. package/dist/capabilities/center-plugins.js +5 -10
  32. package/dist/capabilities/scenarios.d.ts +1 -1
  33. package/dist/capabilities/scenarios.js +3 -3
  34. package/dist/center-credential-renewer.d.ts +76 -0
  35. package/dist/center-credential-renewer.js +359 -0
  36. package/dist/center-credential.d.ts +76 -22
  37. package/dist/center-credential.js +119 -43
  38. package/dist/config-catalog.js +17 -8
  39. package/dist/config-center/apply-effective.d.ts +16 -0
  40. package/dist/config-center/apply-effective.js +26 -21
  41. package/dist/config-center/center-request.d.ts +13 -0
  42. package/dist/config-center/center-request.js +23 -3
  43. package/dist/config-center/facade.d.ts +1 -1
  44. package/dist/config-center/http-client.js +2 -5
  45. package/dist/config-center/mcp-revocation.d.ts +2 -2
  46. package/dist/config-center/mcp-revocation.js +1 -1
  47. package/dist/config-center/read-warnings.d.ts +19 -48
  48. package/dist/config-center/read-warnings.js +7 -49
  49. package/dist/config-center/restart-signal.d.ts +8 -21
  50. package/dist/config-center/restart-signal.js +2 -12
  51. package/dist/config-center/types.d.ts +4 -2
  52. package/dist/config-provider.js +1 -48
  53. package/dist/config-types.d.ts +107 -17
  54. package/dist/config-types.js +7 -2
  55. package/dist/config.d.ts +7 -0
  56. package/dist/config.js +91 -29
  57. package/dist/device-store.d.ts +1 -1
  58. package/dist/env-name-allowlist-knobs.d.ts +21 -7
  59. package/dist/env-name-allowlist-knobs.js +7 -2
  60. package/dist/fleet-client.d.ts +27 -7
  61. package/dist/fleet-client.js +43 -12
  62. package/dist/fleet-lease.d.ts +1 -1
  63. package/dist/host-lsp-manager.d.ts +4 -3
  64. package/dist/host-lsp-manager.js +2 -6
  65. package/dist/http/approval-sweeps.js +5 -20
  66. package/dist/http/dispatch.js +24 -14
  67. package/dist/http/readiness.d.ts +87 -0
  68. package/dist/http/readiness.js +54 -0
  69. package/dist/http/resume-legs.js +5 -4
  70. package/dist/http/route-table.d.ts +7 -0
  71. package/dist/http/route-table.js +6 -0
  72. package/dist/http/routes/a2a-serve.js +6 -14
  73. package/dist/http/routes/approvals-assistant.js +1 -0
  74. package/dist/http/routes/background.d.ts +80 -0
  75. package/dist/http/routes/background.js +94 -0
  76. package/dist/http/routes/capabilities.js +2 -0
  77. package/dist/http/server.d.ts +25 -8
  78. package/dist/http/server.js +2 -2
  79. package/dist/leader/wire.d.ts +8 -1
  80. package/dist/leader/wire.js +4 -3
  81. package/dist/model-entry-refusal.d.ts +42 -19
  82. package/dist/model-entry-refusal.js +34 -19
  83. package/dist/model-route-endpoint.d.ts +10 -0
  84. package/dist/model-route-endpoint.js +6 -2
  85. package/dist/observability/fail-open.d.ts +2 -2
  86. package/dist/observability/fail-open.js +2 -2
  87. package/dist/observability/secret-env-scrub.d.ts +12 -0
  88. package/dist/observability/secret-env-scrub.js +12 -0
  89. package/dist/outbound-dispatcher.d.ts +125 -0
  90. package/dist/outbound-dispatcher.js +211 -0
  91. package/dist/parked-decide.d.ts +11 -5
  92. package/dist/parked-decide.js +4 -1
  93. package/dist/parked-revive-surface.d.ts +47 -0
  94. package/dist/parked-revive-surface.js +20 -0
  95. package/dist/plugins/background-shell-support.d.ts +20 -1
  96. package/dist/plugins/background-shell-support.js +26 -0
  97. package/dist/plugins/host-platform.d.ts +16 -1
  98. package/dist/plugins/host-platform.js +11 -0
  99. package/dist/plugins/remote-env-host.d.ts +14 -1
  100. package/dist/plugins/remote-env-host.js +15 -10
  101. package/dist/project-memory.js +3 -6
  102. package/dist/run-local.js +2 -0
  103. package/dist/server-secret-env.d.ts +97 -24
  104. package/dist/server-secret-env.js +37 -1
  105. package/dist/tool-approval.d.ts +30 -1
  106. package/dist/tool-approval.js +31 -8
  107. package/dist/trace/core-keyset-guard.d.ts +9 -4
  108. package/dist/trace/engine-notice-wire.d.ts +1 -1
  109. package/dist/trace/engine-notice-wire.js +2 -0
  110. package/dist/trace/project.js +13 -0
  111. package/dist/trace/redact.d.ts +11 -0
  112. package/dist/trace/redact.js +1 -0
  113. package/package.json +4 -3
package/MIGRATION.md CHANGED
@@ -7,6 +7,90 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.103.0(车OY 部分)—— server 进程出网认 `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`(S-776)
11
+
12
+ - **出网跟着代理变量走**:server 进程(HTTP 服务与 `run-local`)的出网 —— 引擎 → 网关、中心拉取 / 公告 / 凭证续签、WebSearch、对象存储、OTLP … ——
13
+ 自本版起在 **Node 运行时**按 `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`(小写同认且优先)走;回环目标恒直连。`NO_PROXY` 不支持 CIDR。**谁受伤**:env 里因别的原因带着 `HTTP(S)_PROXY`、此前
14
+ 靠 server 无视它的部署 —— 不在 `NO_PROXY` 里的内网目标现在会被送进代理。**迁移**:把内网目标写进 `NO_PROXY`,或只给 server 进程去掉这几只变量。
15
+ - **无 scheme 的代理值拒启(Node 运行时)**:`proxy.corp:3128` 这类 curl 式写法(以及 `ftp:`、带路径 / 查询)undici 用不了,自本版起拒启,码
16
+ `[config.outbound_proxy_invalid]`(此前 Node 下被无视)。**迁移**:写成 `http://proxy.corp:3128`。Bun 运行时(发布镜像)不校验,且 Bun 的 fetch
17
+ 此前就读这几只变量 —— 镜像形上本条与上一条都不是行为变化,只需把 `localhost,127.0.0.1,::1` 写进 `NO_PROXY`(Bun 不豁免回环)。
18
+ - **启动日志多一行 `outbound_proxy`**(info;Bun 运行时配了代理时是 warn);配置目录多三行 `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`。
19
+ `dependencies` 多一只 `undici`(= core 的同版精确钉)。
20
+
21
+ ## 7.103.0(车OR 部分)—— core 7.33.1 提货:模型调用超时按段重排(`MODEL_CONNECT_TIMEOUT_MS` 退役 / `MODEL_FIRST_BYTE_TIMEOUT_MS` / failover 拓扑首字节 lane / 看门狗 `0` 义 / `wiring_manifest.hands`)
22
+
23
+ - **`MODEL_CONNECT_TIMEOUT_MS` 删除,设了即拒启**(S-753;任何值,含 `0`;空串 = 未设):拒句带码 `[config.env_key_retired]`,点名替代键。
24
+ 它名义「建连」、实际计到**响应头**:本仓自带的 30 000 缺省把自托管网关的 prefill(发生在响应头之前)在 30 s 处掐断、同 body 重发、
25
+ 报「connection lost」。core 7.33.0 起建连(TCP + TLS)是传输层自己的钟,不再有旋钮。**迁移**:删掉这个键;要给响应头等待设上限,
26
+ 改设下一条的新键(值语义变了,别照抄旧值)。
27
+ - **替代键 `MODEL_FIRST_BYTE_TIMEOUT_MS`(`fetch()` → 响应头的等待,毫秒)**:**不设 = core 缺省**(本仓不抄那个数;数值与上传宽限、
28
+ 加宽重试见 `@sema-agent/core` 7.33.x 的 MIGRATION),而不是旧键的 30 s。单路由部署:到点 core 自有一次加宽重试(不吃
29
+ `GATEWAY_MAX_RETRIES`),期间流上一帧 `status {phase:"reconnecting", errClass:"stall"}`,终局句点名 `firstByteTimeoutMs`。合法域
30
+ `[0, 2147483647]` 整数,其余拒启。
31
+ - **failover 拓扑(`MODEL_GATEWAY_FALLBACK_URLS`)的首字节 lane = core 缺省**:每条网关路由首窗到点后都有 core 缺省的一次加宽重试,之后才
32
+ 交给 failover。server **不铸** core 7.33.1 #1128 的 `firstByteRetries` 座(任何拓扑;S-753 车PA):它只在构造期,而调用方可按请求写
33
+ `resilience.allowFailover: false` 关掉 failover(那一次只用主网关、身后没有备)—— 按拓扑铸 0 会让那一次少一次加宽重试。**谁受伤**:主网关接了
34
+ TCP 却不回响应头时,缺省下要等完 core 的首字节 lane(缺省首窗 + 一次加宽重试;以 core 7.33.x 缺省计约 15 分钟;core 改缺省即变)才切备,
35
+ **时间预算短于那一段的调用切不到备**(code-review council 每个镜头 180 s 与 team discuss 600 s:lane 没走完就被取消,备网关一次都没被尝试 ——
36
+ 600 s 在主网关的加宽重试窗里到点;codex 复审 C1 / 车PA r1 实测)。这类部署**升级前**显式设首字节窗(如 `MODEL_FIRST_BYTE_TIMEOUT_MS=30000`,约 30 s + 宽限、再 60 s + 宽限
37
+ 之后切备),并让调用预算容得下「两窗 + 上传宽限 + 备网关作答」;一只只是慢(排队 + prefill 超过窗与加宽窗)的主网关也会被切走。没设的
38
+ failover 部署起服时打一条 `failover_first_byte_lane` warn。
39
+ - **`MODEL_FIRST_BYTE_TIMEOUT_MS` / `MODEL_FIRST_TOKEN_TIMEOUT_MS` / `MODEL_IDLE_TIMEOUT_MS` 设 `0` = 该段不设引擎钟,传输层的响应头 / 响应体钟也关**
40
+ (core 7.33.0 每次请求把传输层的 headers / body 钟置 0;此前 `0` 只关引擎的钟,传输层 300 s 仍会切;TCP + TLS 建连仍归传输层自己的钟)。
41
+ failover 拓扑上首字节段设 `0` ⇒ 挂住的主网关**永不**被首字节 lane 切走(只剩 run 级时限收尾)。依赖「设 0 也还有 300 s 兜底」的部署改设
42
+ 一个大数。启动日志 `listening` 行 `brain` 段:`connectTimeoutMs` 键删、`firstByteTimeoutMs` 键加(`null` = core 缺省),三只看门狗键设 `0` 时
43
+ 显示 `0`(此前显示 `null`)。
44
+ - **`wiring_manifest` 帧多一段 `hands: { mounted, reason? }`**(core 7.33.1 #1039,租户可见,与 `lsp` 同形):`mounted: false` +
45
+ `reason: "no_execution_env"` = **这条腿**没有手(按腿的正面陈述 —— 同一台部署上 scan / council / discuss 这类无手车道也读 false);
46
+ `mounted: true` ≠ shell 可达。operator 面的 `configFingerprint` **一次性变值**
47
+ (同装配各腿仍相等)—— 持久化 / 钉了指纹的读者会见一次跳变。
48
+ - **`GIT_CONFIG_KEY_<i>` 不再被剥**(core 7.33.1 #1129):宿主子进程(host shell / git 腿 / 语言服务器 / bake-runner)里,普通的 git env
49
+ 配置组原样到达;只有某个 KEY 的值带 `<scheme>://<userinfo>@`(令牌 insteadOf)时该组整组扣下(7.103.0 组不变量)。🔴 **安全轴披露(core
50
+ KL-1129a)**:`GIT_CONFIG_VALUE_<i>` **不在** core 的凭据形状判据里 —— 组里 KEY 是 `http.extraHeader`、VALUE 是 `Authorization: Bearer …`
51
+ 这一形会**原样**进宿主子进程(含模型能驱动的 host shell),并在 `isolateConfig` 的 git 腿上作为命令级配置**生效**(`GIT_CONFIG_GLOBAL /
52
+ SYSTEM=/dev/null` 挡不住 env 这一层)。**迁移**:不要把凭据放进服务进程 env 的 git 配置组;要给 git 带凭据,走 credential helper / ssh key /
53
+ 写在 `durableRemote` 里(见 DEPLOY-PREREQS)。⤷ **7.103.0 已随 core 7.33.2 修掉**(车PA 提货):core 给凭据侧加了**值族表**,
54
+ `GIT_CONFIG_VALUE_<i>` 的值是 `Authorization:` / `Proxy-Authorization:` / `Cookie:` 头(任一行、含折行续写)、带 userinfo 的 URL
55
+ (`<scheme>:/+<user>[:<secret>]@…`),或 proxy 地址键(`http.proxy` / `http.<url>.proxy` / `remote.<name>.proxy`)下的 `user[:secret]@host`
56
+ ⇒ 按凭据剥(遥测 `secret_env_scrubbed_total{kind="family-rule"}`),本仓组不变量随即把残组**整组**扣下(与 7.32.1 同姿态;该组里别的配置
57
+ 一并不到子进程)。值族表之外的形(裸令牌写在不叫头 / URL / proxy 的配置值里)仍会原样到达 —— 迁移句不变:凭据别放进服务进程 env 的 git 配置组。
58
+ ⤷ **core 7.33.4(7.103.0 提货;经 7.33.3,车PB / 车PC)**:KEY 与 VALUE 共用的那只 URL 判据(7.33.2 起共用)不再解析 authority —— 值里有 `:/`、
59
+ 其后任意位置有 `@` 或 `%40` 即剥;proxy 地址键下值里有 `@` / `%40` 即剥(密码含未转义 `/`、括号形、单斜杠 `https:/u:t@h`、`ssh://t%40h/…` 等此前
60
+ 漏过的写法一并剥 ⇒ 整组扣)。**谁受伤**:git env 配置组的键或值里 `:/` 之后带 `@` / `%40` 的(含 `@scope` 包路径写法、`%40` 编码形;不限 URL ——
61
+ scp 绝对路径 `host:/repo@v2.git`、`http.extraHeader = Referer: https://…/@scope`、`includeIf.gitdir:/home/a@corp/.path` 同)整组不到宿主子进程
62
+ (fail-closed 多剥,core KL-1129b)。**出路**(只对不带凭据的这类配置;凭据照旧走 credential helper / ssh key / `durableRemote`,`~/.gitconfig`
63
+ 模型可读):不隔离全局配置的腿(host shell / 语言服务器 / bake-runner / leader 推送)读宿主全局 git 配置,写进 `~/.gitconfig` 即可;center-plugins
64
+ 克隆腿每次在新目录克隆、且隔离全局 / 系统配置,这类配置在该腿上**没有替代通道**,只能改写成 `:/` 之后不带 `@` / `%40` 的形,或候 core 收窄
65
+ KL-1129b;project-memory 只跑 `git log` / `git status`,不读这类配置。不受影响:值里没有 `:/` 的 scp 相对路径(`git@host:org/repo.git`)与 `:/`
66
+ 之后没有 `@` / `%40` 的值。
67
+ 🔴 **另一条 git env 通道不在剥密族表里(既有缺口 S-783,本版不修码)**:`GIT_CONFIG_PARAMETERS`(git 的命令级配置 env,形如
68
+ `'http.extraheader'='Authorization: Bearer …'`)不被 core 与本仓任何一层剥 —— 值**原样**进宿主子进程(含模型能驱动的 host shell),并在
69
+ 隔离了全局 / 系统 git 配置的腿上照样**生效**(它与 `git -c` 同层)。**不要**把凭据放进它;core 族表纳入它之后再收。
70
+
71
+ ## 7.102.0(车OI 部分)—— core 7.32.0 提货带来的四条(后台 shell 退出硬收 / Esc 不收后台 / 读根授权与 artifact store / parked 继承工具新失败形)
72
+
73
+ - **后台 shell 在进程退出与会话删除时被硬收(core #1107,行为面)**:
74
+ - **进程退出**:屏蔽 SIGTERM 的后台循环、崩溃出口上宽限跑不完的后台进程,升级后在退出边界被组 SIGKILL 收掉(改前活过引擎,成孤儿)。
75
+ - **`DELETE /v1/sessions/:id`**:7.101.1 只走软梯子(SIGTERM → `HOST_BG_KILL_GRACE_MS` → 组 SIGKILL —— 屏蔽 SIGTERM 的 shell 在宽限后同样死);
76
+ 7.102.0 起是「软 → 等一次宽限 → 硬」:SIGTERM 处理器照样有一次宽限跑完,宽限之后再补硬面一枪组 SIGKILL 并出受据。有后台行的会话,
77
+ DELETE 的应答在硬面之后返回(慢一次宽限,缺省 1000 ms)。
78
+ - **声明了保留的活服务不受影响**(`retained_skipped`)。会话 reap 不再「只结算不杀」env 级保留的行(core NARROWING:保留的行原样留着、行仍
79
+ `running`,别的一律杀)。
80
+ - **谁受伤**:① 依赖「引擎挂了后台进程还在跑」而又没有声明保留的部署 —— 迁移:给那条任务写 `retainBackgroundProcesses: true`(或用 env
81
+ 级保留),或者干脆把服务交给进程管理器;② **受控退(SIGTERM 排空)时没有开着的连接** —— 退出很快落到退出边界,硬面的组 SIGKILL 紧跟
82
+ 早收割的 SIGTERM,后台进程的**优雅退出可能被截断**(7.101.1 在这里只发 SIGTERM,进程作为孤儿把处理器跑完)。这是 core 7.32 契约
83
+ 「每个退出终点软后硬」带来的;缓解(受控退在早收割 + 一次宽限之后再退出)候 S-758,本版不做。要保住优雅退出的长活服务,声明保留。
84
+ - **声明读根落在本地 artifact store 里 ⇒ Artifact 挂载拒(core #1093,部署面 NARROWING,本服务今天零影响)**:core 7.32.0 起
85
+ `additionalReadDirectories` / `additionalDirectories` 里**任何一条**在 artifact store 之内(或就是它、或包含它),挂载期
86
+ `config.artifact_host_invalid`(改前只拒「store 在根内」一个方向)。本服务今天**没有** artifact host,这条到达不了;将来接 host 时,别把
87
+ 读根声明到 store 目录里。同批:读根按身份判 —— 声明的根在 run 首次 shell 判定前被换成软链,不再为链的目标作保。
88
+ - **parked 子代停在「继承工具」上的 `/decide` allow ⇒ 422 `parked_resume.startup_failed`(core #1110,新失败形,非回归)**:core 7.32.0 起
89
+ 子代继承场景层工具,停在这类工具上的审批在赎回交不出继承座时被拒(审批留 pending,`deny` 可结)。本版对一层行交座(不再落这一形;前提是行的根会话现在仍属于这张卡的租户),孙代行 / 根会话已删或易主的行仍落
90
+ (core #1112 候)。7.31.x 的子代根本没有这些工具,所以这不是旧卡的回归。
91
+ - **类型面(本仓内部 API,三句)**:删 `ServiceDeps.parkedReviveTool` / `ParkedDecideDeps.reviveTool`,改 `parkedReviveSurface` /
92
+ `reviveSurface`(按行解析赎回面的工厂,零别名);受害 = 手铸这两个 deps 的嵌入方,tsc 红即通告;补偿 = 固定工具形 `() => ({ reviveTool })`。
93
+
10
94
  ## 7.101.0 —— 十四条(`run-local` 执法三张审批名单 / 人停下的 run 退 1 / 模型目录 fail-closed 三条 + 类型面 `ServiceConfig.modelRefusals` · `ServiceConfig.lspEnvAllow` / 记忆抹除面 409 撤 + 存量只读位一次性 `chmod`(滚动窗内会被旧副本再锁,起服 warn 点名)/ settings 层 `crossSessionInbound: accept` 改答 hold / 子进程剥名收窄 / SQL `session_meta` 新列 / 沙箱名单形错拒启 + 空串值照注入 / `WEB_SEARCH_ENDPOINT` 带 userinfo 拒启 / 配置目录两行 `csv` → `secret` / 被拒名撞档位别名 ⇒ 模型面组拒;S-690 / S-694 / S-699 / S-704 / S-705 / S-712 / S-713 / S-716 / S-723)
11
95
 
12
96
  - **`run-local` 上 `APPROVAL_REQUIRE` / `APPROVAL_DENY` / `APPROVAL_NEVER_AUTO` 生效**(改前:这条一次性 CLI 腿不读三张名单,名单工具照跑,只有启动日志一条 `tool_policy_only_sensitive_baseline` warn)。改后与 HTTP 腿同一只策略:`APPROVAL_DENY` 命中 ⇒ 当场拒;`APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO` 命中 ⇒ stdin 是 TTY 就 `y/N` 问人(问句多一行这次调用的参数),否则当场拒并在 stderr 点名三张名单键(`APPROVAL_NEVER_AUTO` 不吃 `APPROVAL_AUTO_BUDGET` 预算)。**谁受伤**:无 TTY(CI)跑 `run-local`、而 `.env` 与服务共用并带着名单的脚本 —— 名单工具从「照跑」变「当场拒」。**迁移**:给 CLI 环境单独一份 `.env`(不带名单),或设 `APPROVAL_AUTO_BUDGET=<N>` 让 require 名单前 N 次自动批,或在终端里跑(TTY 问人)。`tool_policy_only_sensitive_baseline` 在 `run-local` 上不再出现(名单已执法)。
package/README.md CHANGED
@@ -184,9 +184,9 @@ The server is configured entirely through environment variables. The most import
184
184
  | `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) |
185
185
  | `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) |
186
186
  | `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 |
187
- | `MODEL_CONNECT_TIMEOUT_MS` | `30000` | Gateway connect timeout |
188
- | `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) |
189
- | `MODEL_IDLE_TIMEOUT_MS` | `300000` | Mid-stream idle timeout (`0` = off) |
187
+ | `MODEL_FIRST_BYTE_TIMEOUT_MS` | unset (core's own default) | The wait from `fetch()` to the response headers, ms (7.103.0; replaces the retired `MODEL_CONNECT_TIMEOUT_MS`, which now **refuses to start** — the TCP + TLS connect is the transport's own clock since core 7.33.0). Unset = no key is passed, core's default applies (plus an upload allowance by request size, and one widened retry of its own). `0` = no engine clock on that segment and the transport's header / body clocks off too (the TCP + TLS connect keeps the transport's own clock). A whole number in `[0, 2147483647]`, anything else refuses to start. With `MODEL_GATEWAY_FALLBACK_URLS` set every gateway route likewise gets the first window plus one widened retry before failover (the server does not pass core's `firstByteRetries`: a caller can turn failover off per request, and that call has no backup behind it). A failover topology must set this knob explicitly: unset, a primary that accepts the connection but never answers is cut over only after core's first-byte lane (default window plus the widened retry) runs out, and a call with a shorter time budget (code-review council lenses, 180 s) is cancelled before the backup is tried; at boot such a deployment logs one `failover_first_byte_lane` warn |
188
+ | `MODEL_FIRST_TOKEN_TIMEOUT_MS` | `600000` | First-token timeout (the only watchdog for a stream that opens and never emits; `0` = no engine clock on that segment, the transport's body clock off too. 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) |
189
+ | `MODEL_IDLE_TIMEOUT_MS` | `300000` | Mid-stream idle timeout (`0` = no engine clock on that segment, the transport's body clock off too) |
190
190
  | `LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error` (structured JSON logs) |
191
191
 
192
192
  **Boolean knobs** accept `true`/`false`/`1`/`0`/`yes`/`no`/`on`/`off` (case-insensitive; word table widened
@@ -230,7 +230,7 @@ One row per endpoint family (not exhaustive):
230
230
 
231
231
  | Endpoint family | What it serves |
232
232
  |-----------------|----------------|
233
- | `GET /health` · `GET /metrics` | Liveness + Prometheus metrics (`/metrics/summary`, `/metrics/plan-cache`) |
233
+ | `GET /health` · `GET /readyz` · `GET /metrics` | Liveness, readiness (`/readyz`: 200 / 503 for status-code-only probes such as a k8s `readinessProbe`) + Prometheus metrics (`/metrics/summary`, `/metrics/plan-cache`) |
234
234
  | `GET /v1/capabilities` | Deployment capability discovery — what this deployment can actually do, so clients never probe 501s |
235
235
  | `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) |
236
236
  | `GET /v1/models` | Model catalog (names only; no gateway URLs or keys) |
package/README.zh-CN.md CHANGED
@@ -158,9 +158,9 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
158
158
  | `SENSITIVE_WRITE_PATTERNS` | core 推荐集 | 敏感路径写拒集;逗号分隔值为整体替换,`off` **或留空**关闭。在治理层无条件施加(与客户端权限模式/lane/settings 在场性无关),`run-local` 腿同样生效;编译不出守卫集的值(如 `/`)启动即拒 |
159
159
  | `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) |
160
160
  | `WRITE_PROTECTED_TABLE_REPLACE` | 未设(引擎缺省表) | **整表替换**写保护名表(core 的座按契约就是整表)。只收 JSON 数组 —— 刻意不给逗号简写:一个手滑的裸串会把 51 行换成 1 行。`[]` = 显式「完全不要这张表」。替换时 boot 期发一条**响亮**日志逐名列出被丢的缺省行(`write_protection_table_replaced`;空表走 `write_protection_table_disabled`)—— 想「加两行」请用 `WRITE_PROTECTED_EXTRA`。拒启条件同姊妹键,外加:两根同写 = 一条语义面两个写者 ⇒ 拒启 |
161
- | `MODEL_CONNECT_TIMEOUT_MS` | `30000` | 网关连接超时 |
162
- | `MODEL_FIRST_TOKEN_TIMEOUT_MS` | `600000` | 首 token 超时(开流不吐字的唯一看门狗;`0`=关。7.71.0 起由 `120000` 提高 —— 自托管后端长 prefill 下首字合理地就要等几分钟;要旧姿态显式设 `120000`) |
163
- | `MODEL_IDLE_TIMEOUT_MS` | `300000` | 流中 idle 超时(`0` 关) |
161
+ | `MODEL_FIRST_BYTE_TIMEOUT_MS` | 未设(core 缺省) | `fetch()` → 响应头的等待,毫秒(7.103.0 起;取代退役的 `MODEL_CONNECT_TIMEOUT_MS`,设了旧键**拒启** —— core 7.33.0 起建连(TCP + TLS)是传输层自己的钟)。未设 = 不传键,core 缺省当家(另加按请求体大小的上传宽限,到点 core 自有一次加宽重试)。`0` = 该段不设引擎钟、传输层的响应头 / 响应体钟也关(TCP + TLS 建连仍归传输层自己的钟)。须为 `[0, 2147483647]` 内整数,其余拒启。配了 `MODEL_GATEWAY_FALLBACK_URLS` 时每条网关路由同样是首窗 + 一次加宽重试之后才切备(server 不铸 core `firstByteRetries`:调用方可按请求关 failover,那一次身后没有备)。failover 拓扑须显式设:未设时接了连接却不回头的主网关要等 core 的首字节 lane(缺省窗 + 加宽重试)走完才切备,预算更短的调用(code-review council 镜头 180 s)在备网关被尝试之前就被取消;这类部署起服时打一条 `failover_first_byte_lane` warn |
162
+ | `MODEL_FIRST_TOKEN_TIMEOUT_MS` | `600000` | 首 token 超时(开流不吐字的唯一看门狗;`0` = 该段不设引擎钟、传输层响应体钟也关。7.71.0 起由 `120000` 提高 —— 自托管后端长 prefill 下首字合理地就要等几分钟;要旧姿态显式设 `120000`) |
163
+ | `MODEL_IDLE_TIMEOUT_MS` | `300000` | 流中 idle 超时(`0` = 该段不设引擎钟、传输层响应体钟也关) |
164
164
  | `LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error`(结构化 JSON 日志) |
165
165
 
166
166
  **布尔旋钮收 `true`/`false`/`1`/`0`/`yes`/`no`/`on`/`off`**(大小写不敏感;词表自 7.16.0 起放宽)。
@@ -197,7 +197,7 @@ Web search(部署 env `WEB_SEARCH_*` 七键、per-request `settings.webSearch`
197
197
 
198
198
  | 端点族 | 服务什么 |
199
199
  |--------|----------|
200
- | `GET /health` · `GET /metrics` | 存活探针 + Prometheus 指标(`/metrics/summary`、`/metrics/plan-cache`) |
200
+ | `GET /health` · `GET /readyz` · `GET /metrics` | 存活探针 + 就绪探针(`/readyz`:200 / 503,给只认状态码的探针,如 k8s `readinessProbe`)+ Prometheus 指标(`/metrics/summary`、`/metrics/plan-cache`) |
201
201
  | `GET /v1/capabilities` | 部署能力发现 —— 本部署真正能做什么,客户端免 501 探测 |
202
202
  | `GET /v1/capabilities/mcp` · `POST /v1/capabilities/mcp/probe` | **无需先跑一条 run** 的逐台 MCP 状态:现连、列工具、关掉 —— 前者答本部署自己申报的服务器,后者答调用方自带的 `.mcp.json`。行就是引擎装配名册里的那一行,所以一次性命令与交互会话读到同一个判定。它会真拨号,所以按调用方限速 + 短窗复用 |
203
203
  | `GET /v1/models` | 模型目录(仅名字,不含网关 URL/key) |
package/USAGE.md CHANGED
@@ -48,6 +48,25 @@ docker compose up --build --scale service=4 # 4 个无状态 worker,nginx
48
48
 
49
49
  > 拓扑:LB(入口)→ N 个一样的 worker → 共享 SQL 库(数据中心;MySQL 协议或 PG)+ 模型网关。无中央调度器,跨 worker 靠 DB 协调。
50
50
 
51
+ **探针:存活看 `/health`,就绪看 `/readyz`(7.102.0+)**。`/health` 恒 200(liveness:进程在答);只认状态码的**摘流**
52
+ 探针(k8s `readinessProbe` / Traefik / 负载均衡健康检查)读不到它体里的 `ready:false`,要摘流就指 `GET /readyz`:就绪 ⇒ `200 {"ready":true}`;未就绪 ⇒ `503 {"ready":false,"reasons":[…]}` + `Retry-After: 5`。
53
+ `reasons` 闭集两词:`draining`(SIGTERM 后优雅排空中)、`model_roster_pending`(`CONFIG_REQUIRE_ROSTER=true`
54
+ 且中心模型名册尚未落地)。两者都免鉴权(与 `/health` 同位,探针带不了 token)。🔴 **库连不上不算未就绪**:
55
+ `storeLive:false` 只在 `/health` 体里披露 —— 一次库抖若进就绪门,整队副本会在同一拍被摘光(共模失效)。
56
+ ⚠️ **重启语义的探针别指 `/readyz`**(镜像 `HEALTHCHECK` 配 Swarm / autoheal、k8s `livenessProbe`):中心宕机时整队副本
57
+ 同卡 `model_roster_pending`,指 `/readyz` 就会一起被重启、再一起卡住 —— 同一种共模失效。它们留在 `/health`(本仓
58
+ `Dockerfile` 的 `HEALTHCHECK` 就是 `/health`)。
59
+ ```yaml
60
+ # k8s:liveness 打 /health(进程死才重启),readiness 打 /readyz(不就绪只摘流,不重启)
61
+ livenessProbe:
62
+ httpGet: { path: /health, port: 8090 }
63
+ periodSeconds: 10
64
+ readinessProbe:
65
+ httpGet: { path: /readyz, port: 8090 }
66
+ periodSeconds: 5
67
+ failureThreshold: 1 # 排空 / 名册未落要立刻摘,不等三拍
68
+ ```
69
+
51
70
  **可选 — 多网关 failover + Anthropic 路由(消除单网关 SPOF)**
52
71
  ```bash
53
72
  # ① 同协议冗余:主网关挂(连不上/上游开始前就失败)→ 按序切到备网关(同一 model id)
@@ -77,17 +96,25 @@ ANTHROPIC_API_KEY=sk-ant-… # 可选:ANTHROPIC_BASE_URL / ANTHROPIC_VERSION /
77
96
 
78
97
  **可选 — 韧性栈:分级超时 + 断路器(core 1.38,叠在 failover 之下)**
79
98
  ```bash
80
- # ③ 分级超时:任务级 limits.maxWalltimeMs(6.0.0 起,毫秒)之下补两段,各记为可重试的 [network] → 触发重试/断路器/failover
81
- MODEL_CONNECT_TIMEOUT_MS=8000 # fetch 迟迟不返响应头(网关连不上)→ 中止
82
- MODEL_FIRST_TOKEN_TIMEOUT_MS=30000 # SSE 已开但迟迟不吐第一个 delta(网关 hang);reasoning 的首个 thinking 也算首 token
99
+ # ③ 分级超时(core 7.33.0 按一次调用的段重排;任务级 limits.maxWalltimeMs 之下):建连(TCP + TLS)是传输层自己的钟,不是旋钮。
100
+ # 三个旋钮的 0 = 该段不设引擎钟,**传输层的响应头 / 响应体钟也关**(7.103.0 起;此前 0 只关引擎的钟,传输层 300 s 仍在;
101
+ # TCP + TLS 建连仍归传输层自己的钟)。
102
+ MODEL_FIRST_BYTE_TIMEOUT_MS=8000 # fetch() → 响应头的等待(7.103.0 起;取代退役的 MODEL_CONNECT_TIMEOUT_MS —— 设了旧键即拒启)。
103
+ # 不设 = core 缺省(本仓不抄那个数,见 core 7.33.x MIGRATION);窗上另加 core 按 body 大小给的上传宽限,
104
+ # 到点 core 自有一次加宽重试(不吃重试上限;配了备用网关也一样,之后才切备 —— server 不铸 firstByteRetries)、终局句点名本键。自托管慢网关(prefill 在响应头之前)
105
+ # 按排队 + prefill 留足;failover 拓扑须显式设(未设时预算短的调用切不到备;见 DEPLOY-PREREQS)
106
+ MODEL_FIRST_TOKEN_TIMEOUT_MS=30000 # 响应头之后迟迟不吐第一个 delta(网关 hang);reasoning 的首个 thinking 也算首 token
83
107
  # 缺省 600000(7.71.0 起,原 120000):自托管后端长 prefill 下首字合理地要等几分钟,
84
- # 120s 会把一次正常的慢启动判成 [network] 失败去重试。快失败部署显式设回 120000;0=关
85
- MODEL_IDLE_TIMEOUT_MS=20000 # (1.40.1)出过 token 后中途卡死:每个 delta 重置,静默超时→中止(补 first-token 只管首字)
108
+ # 120s 会把一次正常的慢启动判成失败去重试。快失败部署显式设回 120000
109
+ MODEL_IDLE_TIMEOUT_MS=20000 # (1.40.1)出过 token 后中途卡死:每个 delta 重置,静默超时→中止(补 first-token 只管首字);缺省 300000
86
110
  # ④ 断路器:主网关连败 N 次即"开路"→ 快速失败,让 failover 立刻切备(不再逐个等超时)
87
111
  MODEL_CIRCUIT_BREAKER=true MODEL_CB_FAILURE_THRESHOLD=5 MODEL_CB_COOLDOWN_MS=30000
88
112
  # ⑤ 每次调用的重试上限(0–20)。**不设 = 用引擎默认**(server 不再写死它)
89
113
  GATEWAY_MAX_RETRIES=10 # openai 兼容腿(第三方限流 provider 走的就是这条)
90
114
  ANTHROPIC_MAX_RETRIES=10 # 云 Anthropic 腿
115
+ # ⑥ 慢后端上开 auto 模式(permissionMode:"auto"):分类器那一级也在等同一个排队 + prefill 的后端,到点没裁 ⇒ 这次工具调用被拒。
116
+ # 按后端实测留足(整数 ms,[1000, 2147483647];未设 = core 缺省;坏值拒启)。见 §9.4
117
+ AUTO_MODE_CLASSIFIER_TIMEOUT_MS=180000
91
118
  ```
92
119
  - **全部默认关**(超时=0、断路器=false)→ 不设就和以前**逐字节一致**。
93
120
  - 断路器**只在配了 `MODEL_GATEWAY_FALLBACK_URLS`(≥2 路)时才有意义**——它的价值是"开路即快速失败 → failover 立刻切备";单网关下它是 no-op(启动日志 `circuitBreakerNoop` 会提示)。只 `network/server/rate_limit` 计入连败,`auth`/`invalid_request` 不计(坏 key 熔断整网关无意义)。备用网关(最后一路)不套断路器。
@@ -386,6 +413,21 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
386
413
  连接类异常惯于回声整条 DSN(`mysql://user:pass@host:3306/db`)。要读全文去**日志**:探针在
387
414
  live→dead 翻转拍打 `store_probe_dead`,`error` 字段是原文。
388
415
  钉:`test/health-store-live.test.ts` / `test/store-live-probe.test.ts`。
416
+ - 就绪位(**7.102.0+**,S-739):`/health` 与 `GET /readyz` 读**同一只**就绪判。就绪 ⇒ `/health` 不带 `ready`
417
+ 键(正常形状不变)、`/readyz` 200 `{"ready":true}`;未就绪 ⇒ `/health` 带 `ready:false` + `readyReason`(首条原因
418
+ 的人读句,名册未落那句逐字沿用旧句)+ `reasons`(闭集 `draining` / `model_roster_pending`,按优先序,与 `/readyz`
419
+ 逐字同一份),`/readyz` 503 `{"ready":false,"reasons":[…]}` + `Retry-After: 5`(体只这两键,不带身份字段)。
420
+ ⚠️ 7.102.0 起 **draining 也是未就绪**:此前 draining 形的 `/health` 只有 `draining:true`、不带 `ready` 键(读成
421
+ 「就绪」),与同一刻计费提交的 503 自相矛盾。`storeLive:false` **不进**就绪判(见上一条:披露不代裁)。fleet 心跳的
422
+ `ready` 同一只判(名册未落时心跳 `ready:false`,落地当刻即再报一次)。fleet 心跳另带 **`restart`**(**7.102.0+**,S-741):
423
+ 与 `/health.restart` 同一只取值口、逐字同一个对象(形 = settings-schema `RestartSignal`:`reasons` / `version` / `since`;
424
+ 在场即「须重启才能应用配置」,缺席即不需要 —— 中心整行替换);重启信号出现 / 消失 / 变化的当刻即再报一次,不等下一拍心跳。
425
+ 钉:`test/health-readyz.test.ts` / `test/health-readiness-single-source.test.ts`。
426
+ - 模型拒因(**7.103.0+**,S-768):`/health.modelRefusals` 与 fleet 心跳 `modelRefusals` 同一只取值口、同一个对象(形 = settings-schema
427
+ 6.2.0 `ModelRefusalsSignal`:`entries` = 目录条目名 → 拒因码,`plane` = 模型面组拒码;值只码)。描述最近一次被判的中心候选目录;
428
+ 没有可报的拒 ⇒ 键缺席。名字是秘密形 / 空串 / `__proto__` 的不可用条目不上这两处(出口过名筛;引擎内部照旧拒它)。消费方读 `version`
429
+ 判「缺席 = 没有拒」还是「老引擎、不可知」;读值用 settings-schema `readModelRefusals`。契约段:`docs/ASSISTANT-WIRE-CONTRACT.md`「模型拒因」。
430
+ 钉:`test/model-refusals-s768.test.ts`。
389
431
  - 数据驻留提示:`DB_BACKEND=local` 下显式 `SESSION_BACKEND=memory` 会被收编为 **durable(local)**
390
432
  (1.292+ 裸 boot 默认 durable;/health 的 `sessionBackend` 报 `durable(local)`)——session 行落盘在
391
433
  数据根下,清数据/隐私预期要按「sessions 在 engine-data 里」来做,不要按「只在内存」。
@@ -508,10 +550,24 @@ SEND_USER_FILE_SANDBOX_PUT_ENDPOINT=… # 可选:沙箱直传 PUT 的端
508
550
  **可选 — 接配置控制面(中心化模型/角色/团队配置)**
509
551
  ```bash
510
552
  # 新名(orchestrator 现注入);旧名 CONFIG_CENTER_* 在场即 boot 拒启,只认 SEMA_REGISTRY_*
553
+ # 凭证两键名 SEMA_REGISTRY_TOKEN / SEMA_REGISTRY_TOKEN_FILE 与「二选一」判官的属主 = @sema-agent/settings-schema
554
+ # `CENTER_CREDENTIAL_ENV` / `judgeCenterCredentialDeclaration`(6.1.0 起;管理面 / 壳 / 本服务 import 同一处)
511
555
  SEMA_REGISTRY_URL=http://<config-center-host>:3100 # 启动拉 GET /api/config/effective(Bearer+ETag),覆盖 env 兜底
512
556
  SEMA_REGISTRY_TOKEN=<SERVICE_PULL_TOKEN 的值> # 取自配置控制面主机 .env;只读拉取令牌
513
557
  # SEMA_REGISTRY_TOKEN_FILE=/path/to/token # 或:凭证文件(与上一行二选一,都设=拒启)。每条中心请求都重读 ⇒ 轮换 / 换账号改写文件即可,
514
- # 中心请求面不重启;文件不可读 = error 级 center_credential_unreadable,不当作无凭证。
558
+ # 中心请求面不重启;文件不可读 / 为空 = error 级 center_credential_unreadable,不当作无凭证。
559
+ # 内容两种类(S-698,7.102.0;判别 = settings-schema parseCenterCredentialFile):
560
+ # · 裸行 = 静态 bearer(worker 拉取令牌 / 用户 JWT),不续签,行为同 7.101.0;
561
+ # · JSON 信封 {"kind":"user-session",accessToken,refreshToken,expiresAt,issuer}(壳登录写)
562
+ # = 人的会话:引擎在到期前 CENTER_CREDENTIAL_RENEW_LEAD_MS 内自己续签并原子改写同一文件
563
+ # (续签只向与 SEMA_REGISTRY_URL 同源的 issuer 发;400 先重读文件再判,壳与引擎两写者不打架)。
564
+ # 🔴 拒启两形:内容读不成([config.center_credential_malformed]:`{` 开头坏 JSON / 信封多键缺键 /
565
+ # issuer 非 http(s) / 裸行不是令牌)、多租户(REQUIRE_PRINCIPAL=true)读到信封
566
+ # ([config.center_credential_user_session_multi_tenant])。运行期同形 = 该次取值 fail-closed + 一次 warn。
567
+ # CENTER_CREDENTIAL_RENEW_LEAD_MS=300000 # 续签提前量(ms;缺省 5 min)。0 = 关续签、只读信封(壳负责续);其余须为 [8000, 1800000] 内整数,
568
+ # 坏值拒启。续签成功 info center_credential_renewed;真失效(refresh token 已作废)warn
569
+ # center_credential_renew_failed 一次 + /health.centerCredential.renewal.state=failed,到期后中心请求
570
+ # 走各自既有的失败臂、不带过期令牌出站;issuer 不同源 warn center_credential_issuer_mismatch 一次、不发。
515
571
  # ⚠️ 换账号(凭证身份变化)后,只在启动期物化的面 —— 技能 / MCP / A2A / 场景 —— 仍是上一个
516
572
  # 身份的,直到重启(`/health` 报 restartRequired,reasons 含 skills);热面(models / roles /
517
573
  # limits / 审批名单 …)在新身份第一份候选到达那一拍换代。「换账号即降档 + 当场拉取」候 S-667(不在 7.100.0)。
@@ -960,6 +1016,11 @@ ENGINE_ORPHAN_LINGER_MS=2000 # 可选,默认 2s:成孤儿之后还要静多久
960
1016
  生效值**为准:`GET /v1/config/catalog` 里 `DRAIN_GRACE_MS` 那一行的 `effectiveValue` 就是本进程真在用的
961
1017
  数(它读的是活 config,不是这份文档里的默认值),启动期的夹取/警告行同源。本文档其余各处谈这条排空腿
962
1018
  时都不带数字,数字只有这一处 —— 照抄默认值去算壳形预算会差一个数量级。
1019
+ - **排空期的第二发信号(`DRAIN_SECOND_SIGNAL_MIN_GAP_MS`,缺省 `250`,7.102.0+)**:排空开始后再来一发 `SIGTERM` /
1020
+ `SIGINT`,只有与排空起点相隔 ≥ 这个毫秒数才算「别等了,硬停」;更近的是同一拍被投递了两遍(壳心跳误判、编排器
1021
+ 重试),打一行 `drain_duplicate_signal_ignored`(带 `signal` / `sinceFirstMs` / `minGapMs`)后照常排空。真要立刻
1022
+ 停:隔过这个间隔再发一次,或 `SIGKILL`;设 `0` 回到「每个第二发都硬停」的旧行为。合法域 `[0, 10000]` 整数毫秒
1023
+ (上界 = `DRAIN_GRACE_MS` 的地板),坏值拒启。不在排空时 `SIGINT` 仍是立即硬停。
963
1024
  - **自退留痕 `<数据根>/exit-last.json`**:自退在 **drain 开始那一刻**就把判据写进数据根(`orphan` /
964
1025
  `reason` / `idleForMs` / `attached` / `inflight` / `lingerMs` + `version` / `at` / `pid`)。壳的 exit
965
1026
  watch 往往在流断之后才跑,只看退出码分不清"它自己退的"与"它崩了";读到这个文件就能把这个 pid 归入
@@ -999,7 +1060,7 @@ ENGINE_ORPHAN_LINGER_MS=2000 # 可选,默认 2s(父没了之后的窗;见上一
999
1060
  两键都缺席 ⇒ 整件不装配,零定时器。
1000
1061
  - **自退判据 = 三合取,且必须连续满**「当前窗」(父还活着 = `ENGINE_LINGER_MS`;父没了 =
1001
1062
  `ENGINE_ORPHAN_LINGER_MS`。判据一条,只有窗长两态):① 没有附着的 SSE 流;② 没有在飞的 run
1002
- (与 `/health` 的 `inflight` **同一个读数**);③ 没有非探针 HTTP 触点(`/health`、`/metrics*` 不算 ——
1063
+ (与 `/health` 的 `inflight` **同一个读数**);③ 没有非探针 HTTP 触点(`/health`、`/readyz`、`/metrics*` 不算 ——
1003
1064
  探针不是「有人在用」),**且**最后一条附着流断开也已满窗。任何一条不满足即把计时**清零**。
1004
1065
  (最后那半句不是修辞:一条活得比 15s 拍频还短的流整个生命周期都落在两拍之间,自查腿从没看见过它 ——
1005
1066
  只有在**关闭点**记一笔离场时刻,「已经没人附着满 N 秒」才是精确的。)
@@ -1140,14 +1201,14 @@ invalid_using_default` 警告 + 退回缺省值」这条臂随词表放宽一并
1140
1201
  **鉴权 / 身份(两个不同的头,按需):**
1141
1202
  | 头 | 什么时候必须 | 含义 |
1142
1203
  |---|---|---|
1143
- | `Authorization: Bearer <SERVICE_AUTH_TOKEN>` | 设了 `SERVICE_AUTH_TOKEN` 时,所有非 `/health` 请求 | **谁有权调本服务**(OA 后端持有,服务到服务) |
1204
+ | `Authorization: Bearer <SERVICE_AUTH_TOKEN>` | 设了 `SERVICE_AUTH_TOKEN` 时,除 `/health` / `/readyz` 外的所有请求 | **谁有权调本服务**(OA 后端持有,服务到服务) |
1144
1205
  | `x-agent-principal: user:42` | 设了 `REQUIRE_PRINCIPAL=true` 时 | **代表哪个终端用户**(决定 session 归属 + 记忆隔离;**绝不从 body 取**) |
1145
1206
 
1146
1207
  | env 键 | 缺省 | 说明 |
1147
1208
  |---|---|---|
1148
1209
  | `PRINCIPAL_HEADER` | `x-agent-principal` | 上面那个身份头的**名字**。🔴 **缺省值是跨仓 wire 常量,不是给部署方自定义的**:这个名字**不在任何 wire 面上广播**(`/v1/capabilities`、`/health` 都没有它),所以没有下游能在运行期问 server「你叫它什么」——SDK 有一个 `principalHeader` 逃生舱但 cli/client-core 都没接线,浏览器端 BFF 是逐字硬编码,`mcp-oa` 侧车自己读同名 env(各读各的)。本旋钮只服务于「入口网关已经把身份写进别的头名」这一种特殊部署(让 server 迁就既有网关),不是给部署方起新名字用的。**改名即断下游**,但后果分四形(逐条对过代码):`REQUIRE_PRINCIPAL=true` 下提交腿(`POST /v1/tasks|/v1/runs`)是 `401` + **粗码 `auth.unauthorized`**(authorizer 抛的无 code HttpError 走状态码兜底映射),属主寻址的读/动词面是 `401 auth.principal_required`(路由级细码);两者的文案都逐字回显**你配的头名**,那是现场唯一能指认改名的线索。`REQUIRE_PRINCIPAL` 未开时更坏:**新会话照常成功**、每一条被当成**匿名**(session 归属 / 记忆隔离 / 规则车道 / operator 判定静默走无身份分支,无任何错误码),而**接续一条已属主的会话**会 `401`(`session is principal-owned; missing principal header …`)。⚠️ **SSO 与 direct-door 两条身份来源不经过这个头**(`verifiedPrincipal`),改名对它们无影响 —— 混合形部署因此会出现「一半调用方有身份、一半静默变匿名」的混着长。改了后果自负,且必须同批把每一家下游改到同名。取值须是**单个**合法 header 名(带空格/逗号 ⇒ 启动期拒启:它会撕裂 CORS `allow-headers`)。成文契约见 [`docs/ASSISTANT-WIRE-CONTRACT.md` §0.5](docs/ASSISTANT-WIRE-CONTRACT.md) |
1149
1210
 
1150
- > ⚠️ **Bearer 是"每个请求",不只是 POST。** 配了 `SERVICE_AUTH_TOKEN` 后,**`GET /v1/runs/:id`、`GET /v1/runs/:id/events`(SSE)、`/metrics`** 等所有非 `/health` 路由都要带 `Authorization: Bearer`——漏带一律 `401`。下面示例为简洁**省略了 Bearer**,真实调用请逐个补上。
1211
+ > ⚠️ **Bearer 是"每个请求",不只是 POST。** 配了 `SERVICE_AUTH_TOKEN` 后,**`GET /v1/runs/:id`、`GET /v1/runs/:id/events`(SSE)、`/metrics`** 等除 `/health` / `/readyz` 两只探针外的路由都要带 `Authorization: Bearer`——漏带一律 `401`。下面示例为简洁**省略了 Bearer**,真实调用请逐个补上。
1151
1212
 
1152
1213
  **请求体字段(常用;受理**闭集**以 `docs/ASSISTANT-WIRE-CONTRACT.md` §1 为准 —— 本表不再重抄一份,S-710):**
1153
1214
  | 字段 | 必填 | 说明 |
@@ -1286,6 +1347,8 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
1286
1347
  | `GET /v1/config/catalog` | **配置目录自描述**(operator-only —— `explicitOperatorOk`,空名单=谁都不是)。逐 env 键一行:`{name, domain, type, semantics, writeLanes, effectiveLane, envHeld, envTiming, staticDefault?/derivedDefaultNote?, effectiveValue, valueClass, danger, center?{domain,path,precedence,timing,restartSlice?,domainExists,managedNow,effectiveNow}}`;顶层带 `restartSlices`(重启片闭集)与 `centerDomains`(settings-schema 现行域表)。**两轴不混**:`writeLanes`=谁能写,`effectiveLane`=现在谁在生效(env-wins 族 env 占位 ⇒ center 腿 `effectiveNow:false`)。secret 类值**永不回传**(只回在场位;opaque 回在场位+长度;URL 剥 userinfo)。完备性由仓内 AST 双向对账门执法(分母=全 `src/` 生产 env 读取点)。旋钮 `CONFIG_CATALOG_ENABLED=false`(合规部署)⇒ 路径 404 + 能力位 `configCatalog:false`。响应 `Cache-Control: no-store`。**7.57.0 起**:`enum` 行的 `effectiveValue` **恒落在该行自己的 `enumValues` 闭集内**(仓内机器门执法)——此前 `SESSION_BACKEND` 回的是**内部标签** `tidb`(自 5.0.0 起是**退役公名**,写回去 boot 拒),现归一成公名 `mysql`;`MODEL_DEFAULT_THINKING` 补上一直被解析器接受、却漏在目录闭集外的 `xhigh`,并改为回**实算值**(`off`/未设 ⇒ `null`)而不是 env 原文。 |
1287
1348
  | `POST /v1/admin/config/refresh` | **配置手动刷新**(operator-only,与上一行同门:空 `OPERATOR_PRINCIPALS` ⇒ 恒 403)。改完中心配置 / `config.d` 之后不想等 60s 轮询就调它:触发**一次既有**的 refresh 拍(在飞则汇入那一拍,绝不并发双拍),返回时该拍已落地。200 体 `{triggered:true,targetVersion,appliedVersion}` —— 两个世代号与 `/health` 的 `configTargetVersion`/`configAppliedVersion` 同源。本部署没有配置管道(纯 env worker:既没配 `CONFIG_CENTER`、也不是 `CONFIG_PROVIDER=local`)⇒ 409 `config.refresh_unavailable` 指路,不会回一个「触发了但什么都没发生」的 200。 |
1288
1349
  | `GET /health` | 健康(无需鉴权)。**7.67.0+** 恒带 `startedAt`(引擎进程起点 epoch ms,一次铸;与 `pid` 并列的换代锚——值变了就是换了一条命)。7.37.0+ 配置管道在场时带世代账键:`configTargetVersion`(本副本**最后见到**的配置版本,身份标)、`configAppliedVersion`(最后一次真落地的版本)、`configTargetOrdinal`/`configAppliedOrdinal`(7.38.0-rc.2 起,**收货序数**——`CONFIG_PROVIDER=local` 下 version 是内容哈希不承诺序,「改了没生效」看序数对不对齐)、`configApplyStaleMs`(target≠applied **持续**时长;收敛时键缺席)—— 编排器据此摘掉「配置持续落后」的副本 |
1350
+ | `GET /v1/sessions/:id/background` · `POST /v1/sessions/:id/background/stop` `{"includeRetained":true\|false}` | **会话后台任务的读口与停止口**(**7.102.0+**,S-745;能力位 `background.listFace` / `background.exitFaces`,老 server 404)。读口列**本会话**、**本副本**上的后台任务行(`{ rows: [{ id, description?, type, status, createdAt, sessionScoped, retained? }] }`,`retained:true` = 声明了保留的活服务,正常退出不会停它);停止口「软 → 等一次 `HOST_BG_KILL_GRACE_MS` → 硬」收掉本会话的后台 shell(SIGTERM 处理器先有一次宽限跑完,之后组 SIGKILL,屏蔽 SIGTERM 的循环也活不过去;有后台行时应答慢一次宽限)并逐字返回受据 `{ receipts: [{ id, outcome }] }`:`killed` / `already_gone` / `retained_skipped`(`includeRetained:false` 留下的活服务)/ `no_pgid`(远端 / 沙箱车道没有本机进程组可发,已派普通 kill,「可能还在跑」;host 车道的 `already_gone` 只由观察得出 —— 引擎已收割组长或内核答组已空,7.103.0 起,S-769;darwin 上梯子那一枪先落地、引擎还没收割的那一拍如实答 `no_pgid`,判「收掉没有」读进程)。`includeRetained` 必填:`true` = 连活服务一起停(壳的「Exit and stop tasks」),`false` = 活服务照留(`sema kill`)。鉴权 = 会话属主或显式 operator(未知与他人同 404);停止口在无凭据写门内(没配 service 凭证且未开 `ALLOW_UNAUTHED_WRITES` ⇒ 503 `auth.service_token_required`)。`DELETE /v1/sessions/:id` 走同一只收敛(`includeRetained:false`);run 取消(Esc)**不**收后台 shell。契约 §16 |
1351
+ | `GET /readyz` | **就绪探针**(**7.102.0+**,无需鉴权)。就绪 ⇒ `200 {"ready":true}`;未就绪 ⇒ `503 {"ready":false,"reasons":[…]}` + `Retry-After: 5`,`reasons` ⊆ {`draining`, `model_roster_pending`}(按优先序)。与 `/health` 的 `ready` / `readyReason` / `reasons` 同一只判;库连不上**不算**未就绪(`storeLive` 只在 `/health` 披露)。给只认状态码的**摘流**探针用(k8s `readinessProbe.httpGet`、LB 健康检查;重启语义的 `HEALTHCHECK` / `livenessProbe` 留在 `/health`),见 §0「探针」 |
1289
1352
  | `GET /metrics` | Prometheus 指标(有 token 时需带) |
1290
1353
 
1291
1354
  > **场景可用性(7.7.0 起)**:场景详情里 `enabled` 与 `available` 是**两件事**。`enabled` = 这条场景
@@ -1700,6 +1763,7 @@ armed ⟺ 本次请求 permissionMode 是 "auto" ∧ 本部署装配了分类
1700
1763
  | 旋钮 | 缺省 | 语义 |
1701
1764
  |---|---|---|
1702
1765
  | `PERMISSIONS_DISABLE_AUTO_MODE` | `false`(opt-in) | CC/center/settings 键名 `permissions.disableAutoMode` 的 **env 腿**(config catalog 登记 center 键名 `permissions.disableAutoMode`,settings-schema 现无 `permissions` 域 ⇒ 目录行 `domainExists:false`):**tighten-only** 棘轮 —— `true` ⇒ 本部署每个 principal 的 `caps.autoMode` 折成 `false`(center 授予 `true` **翻不回**);未设 ⇒ 不动 caps。布尔词表(`true/false/1/0/yes/no/on/off`),词表外的值**拒启**(安全轴旋钮不许静默回默认)。目录行见 `GET /v1/config/catalog`(approval 域,security 轴) |
1766
+ | `AUTO_MODE_CLASSIFIER_TIMEOUT_MS` | 未设(⇒ core 缺省) | 7.102.0 起(S-757):auto 分类器**一级梯子**的判决上限(整数 ms)→ core `RunnerDeps.autoMode.timeoutMs`。一级到点没裁 ⇒ 走下一级(`AUTO_MODE_CLASSIFIER_FALLBACK` 决定有没有下一级),走完仍没裁 ⇒ 这次门控调用**整条拒**、不出卡;最坏等待 ≈ 级数 × 本值 + 一次装配。**自托管慢网关**(排队 + prefill 以分钟计)在缺省上限内答不出 ⇒ auto 下工具一律被拒 —— 按后端实测留足(如 `180000`)。未设 / 空串 ⇒ 本仓**不铸键**,由引擎落它自己的缺省(不复制上游数字);合法域 `[1000, 2147483647]` 内的十进制整数,`0`(本座没有「关」)/ 亚秒 / 小数 / `1e5` 这类拼法 / 越 Node 定时器上界一律**拒启**。旋钮无条件(与 auto 是否武装无关);不影响 `permissionModeAuto` 读面。目录行见 `GET /v1/config/catalog`(approval 域) |
1703
1767
  | `CROSS_SESSION_INBOUND` | 未设 | 跨终端会话设计线:CC settings 键名 `crossSessionInbound` 的**部署/组织层**(引擎层形的 `managed` 层,CC `policySettings` 对位)—— 本部署对**入站跨会话消息**的治理四态(词表取自引擎闭集,core 7.30.0 起):`wake` 投递且**宿主**可在收件箱增长时起一个 turn / `next-turn` 在下一个 turn 边界投递 / `hold` 停在收件会话的待审队列(模型看不见、不能据此行动)/ `refuse` 本部署整体退出这条车道。⚠️ 本服务宿主**不起 turn**,所以在本服务上 `wake` 按引擎契约的降级臂读作 `next-turn`。🔴 **旧词 `accept` 自 7.100.0(core 7.30.0 提货)起拒启** —— 它的原义就是 `next-turn`,改成那个词即可(零别名,拒启即通告)。🔴 **未设 ≠ `next-turn`**:未设 = 这一层不表态,由其余层与引擎的 mode-parity 判定决定。词表外的值**拒启**。⚠️ **生效前提**:引擎的 peer 目录席(`RunnerDeps.peerDirectory`)在场 —— 7.91.0 起已接线(会话枚举面 ∧ mailbox 店 ∧ 收件人生命周期面三合取),缺任一合取项的部署上本旋钮仍「就位待命」;目录行见 `GET /v1/config/catalog`(approval 域,security 轴) |
1704
1768
  | `CROSS_SESSION_DIALOG_EXPIRY` | 未设(⇒ 引擎缺省 `5m`) | 同族:CC settings 键名 `dialogExpiry` 的部署层 —— 被 hold 的跨会话消息等待人审多久后按**安全缺省**结算(过期丢弃,**带回执**告知发送方,不静默吞)。词表 `60s`/`5m`/`10m`/`never`(`never` = 不设期限);未设 ⇒ 本仓**不铸键**,由引擎落它自己的缺省(不复制上游缺省值)。词表外拒启。⚠️ 生效前提同上。另注:CC 的同名键还有第二个消费面(转发到远端客户端的审批对话框停靠时长),本旋钮**今天不驱动**那一面 |
1705
1769
 
@@ -1849,3 +1913,86 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
1849
1913
  不同,即验证「脱敏只发生在持久化/展示面,不发生在出站面」这句话为真——**若代理本身继续把请求转发到外部
1850
1914
  模型服务,即便截获成功,也不满足「内容不出机器」这个更高的目标,截获只证明了脱敏的射程,不证明数据没有
1851
1915
  离开你控制的边界**。
1916
+
1917
+ ### 9.7 bake-runner(构建宿主守护进程)的 env(7.103.0 起成文)
1918
+
1919
+ `bake-runner`(`npm run start:bake-runner` = `node dist/bake-runner/main.js`)是跑在构建宿主上的**独立进程**:它向 image-api
1920
+ 认领 bake、起 `build.sh`、推镜像。它**不读**服务进程的配置(配置目录 `GET /v1/config/catalog` 里的 `BAKE_*` 行是服务侧 bake API 的
1921
+ 旋钮,与本表无关)。在此之前 runner 的 env 表只写在 `src/bake-runner/main.ts` 的 `loadEnv` 头注里,本节是它的成文处(两处同文)。
1922
+
1923
+ | env | 缺省 | 说明 |
1924
+ |---|---|---|
1925
+ | `IMAGE_API_BASE` | 必填 | image-api 根(尾斜杠去掉) |
1926
+ | `BAKE_RUNNER_TOKEN` | 必填 | claim / heartbeat 的运维主体 bearer。不进日志,**7.103.0 起也不进任何子进程** |
1927
+ | `BAKE_RUNNER_ID` | `bake-runner-<HOSTNAME>` | 认领时报给 image-api 的 runner 名 |
1928
+ | `RECIPE_REF` | `HEAD` | 配方 checkout 的 ref(`HEAD` = 用现成 checkout,不 fetch) |
1929
+ | `RECIPE_DIR` | `/opt/recipes` | 配方仓 checkout 根(build.sh 的 cwd) |
1930
+ | `BUILD_SH_PATH` | `e2b-template/dev-sandbox/build.sh` | 相对 `RECIPE_DIR` 的 build.sh 路径 |
1931
+ | `BAKE_DATA_DIR` | `/data` | 数据盘(磁盘门读它的 df) |
1932
+ | `BAKE_HEARTBEAT_MS` / `BAKE_IDLE_POLL_MS` / `IMAGE_BAKE_MIN_FREE_GB` / `BAKE_SHUTDOWN_GRACE_MS` | 30000 / 5000 / 20 / 5000 | 数值;非数 / 负数**拒启** |
1933
+ | `BAKE_ENV_ALLOW` | 空 | 子进程 env 的点名放行名单(见下) |
1934
+ | `LOG_LEVEL` | `info` | 日志档位 |
1935
+
1936
+ **子进程 env(7.103.0,S-684 残余)。** runner 起的每一只子进程组 —— `build.sh`,以及主机命令 `git fetch` / `docker push` /
1937
+ `docker image inspect` / `df` —— 拿到的 env = 本进程 env **剥密**:core 形状规则(`*_KEY` / `*_TOKEN` / `*_SECRET` / `*_PASSWORD` …)∘
1938
+ 服务配置目录里的 secret 名 ∘ 名单型旋钮原文。runner 自己的 `BAKE_RUNNER_TOKEN` 在内。剥了什么在第一次起组时记一行
1939
+ `secret_env_scrubbed`(warn,site = `server.bake-runner.spawn-env`,带 `allowed` 与出路句;只键名,不带值)。
1940
+ `PATH` / `HOME` / `NO_PROXY` / `DOCKER_CONFIG` / `BAKE_IMAGE_REGISTRY` / `RECIPE_GITSHA` 这类非密钥键照常传。
1941
+ docker 推送的鉴权走构建宿主上的 docker 凭据存储(`docker login`),不走 env,不受影响。
1942
+ git 的 env 配置组(`GIT_CONFIG_COUNT` + `GIT_CONFIG_KEY_<i>` / `GIT_CONFIG_VALUE_<i>`)要么整组到达、要么整组不到:core 剥掉其中任一成员
1943
+ (KEY 或 VALUE 的值里 `:/` 之后带 `@` / `%40`、VALUE 是鉴权头等)之后剩下的残组会让 git 起手退 128,所以残组整组扣下(随组扣下的成员在同一行
1944
+ `secret_env_scrubbed` 里,`kind` = `git-config-group`)。要这组配置到达 ⇒ 把全部 `GIT_CONFIG_KEY_<i>` 与 `GIT_CONFIG_VALUE_<i>` 写进
1945
+ `BAKE_ENV_ALLOW`(两边都可能被剥,只放回 KEY 仍是残组),或改用文件型 git 配置(`~/.gitconfig`,不放凭据)。
1946
+
1947
+ **`BAKE_ENV_ALLOW=NAME,NAME`。** 配方真要一枚密钥形 env(例如 build.sh 里自己做 registry 登录要 `REGISTRY_PASSWORD`,或
1948
+ `git fetch` 的 askpass 读 `GIT_TOKEN`)⇒ 点名它,runner 把该键从本进程 env **原样**放回给每一只子进程组(部署方显式承担)。
1949
+ 与服务侧 `LSP_ENV_ALLOW` 同一只解析器、同一只放行谓词:
1950
+ - 形错(空段 / 尾逗号 / 非变量名形,如 `$NPM_TOKEN`、`a b`、`NAME=value` 整段贴入 / 点名任一只名单型旋钮)⇒ runner **拒启**,
1951
+ `bake_runner_fatal` 那一行点名旋钮与第几段,**不回显值**;
1952
+ - 本进程环境里没有的名 ⇒ 起服一行 `config_env_entry_rejected`(只报序号)、不放行;
1953
+ - 旋钮原文本身按凭据类对待,不进子进程;
1954
+ - 生效名单(点到且环境里真有的名)见起服行 `bake_runner_start.envAllow`(只键名);
1955
+ - 没有多租户姿态(构建宿主守护进程没有主体 / 租户),`REQUIRE_PRINCIPAL` 与它无关。
1956
+
1957
+ ### 9.8 出网代理:`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`(7.103.0 起)
1958
+
1959
+ server 进程(HTTP 服务与 `run-local`)**所有走 undici 的出网跳**都认这三只平台标准变量:引擎 → 模型网关(含 https 网关经 `CONNECT` 隧道)、
1960
+ 配置中心拉取 / fleet 公告 / 凭证续签、WebSearch、对象存储、OTLP、hook 模型调用、core 的 MCP streamable-http / A2A / WebFetch 工具 …
1961
+ 实现是**一只**进程级 dispatcher,在启动时装一次;没有另外的开关 —— 这三只变量本身就是配置通道。
1962
+
1963
+ - **读法**(与 undici 读 env 逐字同序):小写 `http_proxy` / `https_proxy` / `no_proxy` 同认**且优先**;https 目标在 `HTTPS_PROXY` 缺席 / 空时
1964
+ 走 `HTTP_PROXY`;两者都没有 ⇒ 直连。**启动时读一次**(boot 冻结,运行中改 env 不生效)。
1965
+ - **代理 URL 形**:`http://host:port` 或 `https://host:port`(undici 7.30 构造也接受 `socks5://`,本版未实测转发);userinfo(`http://user:pass@host:port`)被译成
1966
+ `Proxy-Authorization: Basic …`(密码里的 `/` `#` `?` `@` 要百分号编码)。**Node 运行时**:无 scheme(curl 式 `proxy.corp:3128`)、`ftp:`、
1967
+ 带路径 / 查询的值 undici 用不了 ⇒ **拒启**,拒句带码 `[config.outbound_proxy_invalid]`(带 `scheme://` 却在 host:port 之后还有路径 / 点段 / 查询 /
1968
+ 片段的值,本仓先于 undici 拒 —— 这一道各运行时同,Bun 下也拒);回显只出 `scheme//host[:port]`,形不干净(有路径 /
1969
+ 查询 / 片段 —— 未编码的特殊字符会让密码落进这几段)整只占位 `«redacted:url»`。Bun 运行时不校验(见下)。
1970
+ - **`NO_PROXY` 语法 = undici 的**:逗号或空白分隔;`host` 精确匹配**且含其子域**;`.host` / `*.host` 后缀;`host:port` 限端口;整串 `*` = 全直连;
1971
+ IPv6 写方括号形 `[fd00::1]`。**不支持 CIDR / IP 段**(`10.0.0.0/8`、`10.*` 都不生效):按 IP 访问的目标逐个列(可带 `:port`)。
1972
+ - 🔴 **回环恒直连,不看 `NO_PROXY`**:目标是 `localhost` / `127.0.0.0/8` / `::1` / `::ffff:127.x` / `0.0.0.0` / `::` 时一律直连 —— 本机网关、本机中心
1973
+ 不会被送进公司代理(代理够不到本进程的回环)。这是无条件规则,没有关掉它的旋钮;它是对 undici 缺省的收窄(undici 自己只按 `NO_PROXY` 判)。
1974
+ 内网目标(对象存储 / 中心 / 自托管网关 / OTLP collector)**不是**回环,要直连就写进 `NO_PROXY`。
1975
+ - **企业代理的 CA**:给 server 进程设 `NODE_EXTRA_CA_CERTS=<pem 文件>`(Node 启动时读进缺省信任库,undici 的 TLS 用的就是它 —— 隧道里目标的
1976
+ TLS 已实测;`https://` 代理本身那一段同一个信任库,未单独实测)。不要关证书校验。
1977
+ - **Node 24 `NODE_USE_ENV_PROXY`**:它让 Node 在启动时自己装一只读这三只变量的 dispatcher;server 随后装的这一只**覆盖**它(fetch 那一半)——
1978
+ 行为一致,只多回环直连。不设它也一样生效。
1979
+ - **代理那一跳失败时怎么读**(Node 运行时):连代理的每一次拨号 —— 拒连、解析不了、超时、`https://` 代理自身的 TLS、收下连接就关、CONNECT 回
1980
+ 非 200 —— 失败时,引擎的终局句(与 `run_failed` 日志行)形如
1981
+ `fetch failed (via outbound proxy (from HTTPS_PROXY) ← connect ECONNREFUSED 10.0.0.9:3128)` / `… ← proxy answered CONNECT with HTTP 403)` —— 这句会进
1982
+ 租户可见的失败原因:**包装句**(`via outbound proxy (from <变量>)`)点明「经代理 + 哪只变量」、不带代理地址;`←` 之后是根因原文(undici 的底层网络
1983
+ 错误原样,拒连 / 解析不了时它本身会写出代理的 host:port —— 本仓不改写上游错误;代理地址对照启动日志 `outbound_proxy` 行)。代理拨号有一只
1984
+ **从拨号起计的绝对期限** 10 s(= undici 连接阶段的缺省时限;连代理的 TCP / TLS 与 CONNECT 交换都算在内,代理中途吐字节也不续命)—— 到点
1985
+ CONNECT 还没回完 ⇒ 根因 `proxy did not answer CONNECT within 10000 ms`、那条连接随之关掉;调用方先取消时请求当场结束,挂着的那次拨号同样
1986
+ 在期限内收口、不留连接。请求当场失败,不重连(代理收下连接就关也一样)。隧道建好之后的失败(目标的 TLS、
1987
+ 目标的响应)不带这段前缀 —— 那是目标本身的事。
1988
+ - **启动日志**:恰一行 `outbound_proxy`,`{httpProxy, httpsProxy, noProxy, loopbackDirect, runtime}`(代理值 display-safe;`httpsProxy` 是 https
1989
+ 目标**实际**走的那只,含回落)。配置目录 `GET /v1/config/catalog` 有 `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` 三行(`limitsHttp` 组,回显同一份
1990
+ display-safe 投影)。`/health` 与 fleet 公告**不**带代理信息。
1991
+ - **Bun 运行时(发布镜像 `CMD ["bun", …]` 与 `bun build --compile` 二进制)**:Bun 的 fetch 不读 undici 的全局 dispatcher,它**自己**读这三只变量
1992
+ (代理与 `NO_PROXY` 都认),但**回环不豁免**。启动行因此报 `loopbackDirect:false`,配了代理时升 warn —— 在 Bun 上请把 `localhost,127.0.0.1,::1`
1993
+ 写进 `NO_PROXY`。Bun 下代理值**不拒启**(本仓的 undici 在 Bun 里是垫片、不校验;Bun 自己把无 scheme 的值当 http 用,`ftp:` 到请求时才报错),
1994
+ 上面那条「代理那一跳失败时怎么读」的前缀也只在 Node 下有;e2b SDK 在 Bun 下走全局 fetch,**认**代理变量。
1995
+ - **不走这只 dispatcher 的出网**(本版如实列出,不在本版改):e2b SDK(Node 下自带 dispatcher,只认它自己的 `proxy` 选项,server 不传)、k8s 车道
1996
+ (`node:https` + `ws`)、LSP 的 `ws` 传输、ssh 车道、MySQL / PG 驱动(TCP)、`bake-runner`(独立进程)。宿主上起的 host shell 与 git 子进程 env 里
1997
+ 照常带着代理变量(不被剥名),由 git / curl 这类工具自己认它们;**MCP stdio 服务器不继承**(MCP SDK 只给子进程 `HOME` / `LOGNAME` / `PATH` /
1998
+ `SHELL` / `TERM` / `USER` 加上该服务器声明的 env)—— 要它走代理,在它的 env 声明里写上代理变量。
@@ -402,6 +402,33 @@ export declare function readParkRowTrueSeat(toolApproval: object, seat: ParkRowT
402
402
  type ToolApprovalPark = Extract<CorePendingAction, {
403
403
  kind: "tool_approval";
404
404
  }>;
405
+ /**
406
+ * 🔴 **闭集词座的唯一窄读**(S-721 起 `mandate`;S-755 起 `origin`)—— 一座一只 core 成员筛,**名字是参数**(与
407
+ * {@link readParkRowTrueSeat} 同一条:一处解析,不是两只各写一遍的函数)。
408
+ * · `mandate`(core 7.32.0 #1092)—— 一问**站在哪一个 mandate 词上**(`mandated` 说「站在强制上」,本座说是 core
409
+ * `PERSISTED_RULE_MANDATES` 六词里的哪一个;core 只在强制位的**整因**是那一个词时铸 —— 治理 / hook / 祖先,或不止一个理由
410
+ * ⇒ 缺席:缺席从不命名一个词)。读者:活卡帧与活挂起行读 `AskRequest.mandate`(`tool-approval.ts`),运维队列行读寄存行
411
+ * 孪生座 `PendingAction.tool_approval.mandate`({@link parkRowFacts})。**不绑 `mandated`**(与 core `summarizeCheckpoint` 同一条筛法)。
412
+ * · `origin`(core 7.5.0 [ref] 的 park 孪生座;S-755)—— **谁提的这一问**,core `ASK_ORIGINS` 闭集一词。读者:运维队列行读寄存行
413
+ * 孪生座 `PendingAction.tool_approval.origin`({@link parkRowFacts})。活卡帧**不经**本函数:它读引擎在门上刚盖好的
414
+ * `AskRequest.origin`(盖章点即 core 的筛点),照旧只过「非空串」形门(`tool-approval.ts` 帧键 `origin` 顶注)。
415
+ *
416
+ * 什么时候经本函数 —— 一句话:**筛点在 core;server 只在「读的是持久层原字节、中间没有 core 投影」的地方亲手调 core 的那一只筛**。
417
+ * inbox 行读 core `CheckpointSummary.{mandate,origin}`(core `summarizeCheckpoint` 已用同一只 `isPersistedRuleMandate` / `isAskOrigin`
418
+ * 筛过)⇒ 那一面直投、不经本函数(与 `ruleStoreUnreadable` 同一条法理);运维队列行读的是 park 行 `pendingAction` 子树的原字节
419
+ * (两店 `listPending`,中间没有 core 投影)⇒ 经本函数 —— 于是 inbox 行与队列行对同一张 park 行(含手改行 / 世代差的集外词)逐字同答。
420
+ *
421
+ * **不抄词表**([ref]):闭集判定 = core 的成员筛({@link CLOSED_WORD_SEAT_GUARDS});值型 = core 寄存行上那一座的型。本仓对这些词
422
+ * **零分支**(echo-only:core 原话「What a surface does with a word is the surface's own rule」)⇒ 没有需要穷举的 switch;将来本仓若按
423
+ * 词分支,必须 `switch` 在 core 的值型上穷举。在场却读不出(集外词 / 非串值 —— 世代差、手改行)⇒ 按缺席处置 + 计一次
424
+ * `server.approval-card.closed-word-out-of-set`(S-249 族:缺席从不计数、读不出计一次;detail 前缀 = 座名,诊断经 {@link outOfSetDetail},
425
+ * 只读值不调用值)。
426
+ */
427
+ export declare function readClosedWordSeat<S extends ClosedWordSeat>(carrier: object, seat: S): ClosedWordOf<S> | undefined;
428
+ /** 闭集词座的名:三个 core 载体上**同名**(`AskRequest` / 寄存行 `tool_approval` 臂 / `CheckpointSummary`)—— 任一处改名 / 退役 ⇒ 下面的围栏编译红。 */
429
+ type ClosedWordSeat = "mandate" | "origin";
430
+ /** 一座的值型 = core 寄存行上那一座的型(去掉可选位的 `undefined`)。 */
431
+ type ClosedWordOf<S extends ClosedWordSeat> = NonNullable<ToolApprovalPark[S]>;
405
432
  /**
406
433
  * 🔴 S-700 P1(client-core CC-180 / cli L-652 的根子;板 [ref] 订正 → [ref] 立案)—— **park 行事实**:
407
434
  * 运维队列行(`GET /v1/approvals` + `/stream`)顶层展开的那几位。型**从 core 寄存行派生,不手抄**(S-249 同一条
@@ -415,8 +442,12 @@ type ToolApprovalPark = Extract<CorePendingAction, {
415
442
  * 的 `FACT_KEYS` 对本型双向围栏,少列一名同样编译红。
416
443
  * core d.ts 说三位彼此独立:`requiresRealApproval`(只有人能清 —— 蕴含 `mandated`)、`mandated`(没有规则能清)、
417
444
  * `ruleOffersAbsence`(报价车道有什么可给;「never a substitute for this one」)—— 投影逐位照投,不互推。
445
+ * S-721(core 7.32.0 #1092)第四位 `mandate`:强制位站在哪一个词上(经 {@link readClosedWordSeat};词缺席 ≠ 不强制,不从 `mandated` 推)。
446
+ * S-755(cli L-695 / [ref],client-core [ref])第五位 `origin`:谁提的这一问(core `ASK_ORIGINS` 闭集一词,经 {@link readClosedWordSeat};
447
+ * 与活卡帧 `origin` / inbox 行 `origin` 同名同义同值)。此前只有活卡帧与 inbox 行带它 ⇒ 耐久重开 / 重连时壳从队列行读不到出身词,
448
+ * bypass 姿态下按 `ask_rule` / `hook` 照问的那条壳规则无从落地。缺席 = 老行(core 7.5.0 之前 park 的)/ 集外词(计 S-249 tag)。
418
449
  */
419
- export type ParkRowFacts = Pick<ToolApprovalPark, "requiresRealApproval" | "ruleOffersAbsence" | "mandated">;
450
+ export type ParkRowFacts = Pick<ToolApprovalPark, "requiresRealApproval" | "ruleOffersAbsence" | "mandated" | "mandate" | "origin">;
420
451
  /**
421
452
  * S-700 —— park 行事实的**唯一投影**(`pendingAction` 是寄存行上 core 铸的那一只,读自持久层 ⇒ `unknown`)。
422
453
  *
@@ -434,7 +465,9 @@ export type ParkRowFacts = Pick<ToolApprovalPark, "requiresRealApproval" | "rule
434
465
  * 同答(`test/park-row-facts.test.ts` 钉)。不计数:inbox 那一面对同一形同样静默缺席,两面同律;
435
466
  * · `ruleOffersAbsence` —— 经 {@link readRuleOffersAbsence}(活卡帧 / `card_json` 同一只 schema、同一个集外 tag):
436
467
  * park 孪生座与同步座是 core 同一个 factory 铸的同一闭集,不另起第二份词表;
437
- * · 缺席 ≠ false:两位都只在「真」时在场,消费端读**在场**。
468
+ * · `mandate` / `origin` —— 经 {@link readClosedWordSeat}(一座一只 core 成员筛 `isPersistedRuleMandate` / `isAskOrigin`,与 core
469
+ * `summarizeCheckpoint` 投 inbox 行时是同一只筛 ⇒ 两条耐久读面对同一张行同答;集外词读作缺席 + 计同一个集外 tag);
470
+ * · 缺席 ≠ false:各位都只在「真 / 有词」时在场,消费端读**在场**。
438
471
  *
439
472
  * 🔴 **这是展示/分诊投影,不是权限判据**(core 对两位的定性都是 display metadata:「the resume belts keep reading
440
473
  * the gate's own bit」)。本投影对「在场却读不出」的方向是**缺席**(卡上少一行 ≪ 卡上出现生词);一条要从
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { ASK_EVIDENCE_ABSENCE_VALUES, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX } from "@sema-agent/core";
2
+ import { ASK_EVIDENCE_ABSENCE_VALUES, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, isAskOrigin, isPersistedRuleMandate } from "@sema-agent/core";
3
3
  import { redactSecrets } from "./trace/redact.js";
4
4
  import { recordFailOpen } from "./observability/fail-open.js";
5
5
  import { PERSISTED_RULE_MATCHES, UNCOVERED_SEGMENT_REASONS } from "./permission-rule-vocab.js";
@@ -270,13 +270,31 @@ export function readParkRowTrueSeat(toolApproval, seat) {
270
270
  const v = seat in toolApproval ? toolApproval[seat] : undefined;
271
271
  return v === true;
272
272
  }
273
+ export function readClosedWordSeat(carrier, seat) {
274
+ const raw = seat in carrier ? carrier[seat] : undefined;
275
+ if (raw === undefined)
276
+ return undefined;
277
+ const isMember = CLOSED_WORD_SEAT_GUARDS[seat];
278
+ if (isMember(raw))
279
+ return raw;
280
+ recordFailOpen("server.approval-card.closed-word-out-of-set", `${seat}=${outOfSetDetail(raw)}`);
281
+ return undefined;
282
+ }
283
+ const CLOSED_WORD_SEAT_GUARDS = {
284
+ mandate: isPersistedRuleMandate,
285
+ origin: isAskOrigin,
286
+ };
273
287
  export function parkRowFacts(pendingAction) {
274
288
  if (typeof pendingAction !== "object" || pendingAction === null || !("kind" in pendingAction) || pendingAction.kind !== "tool_approval")
275
289
  return {};
276
290
  const ruleOffersAbsence = readRuleOffersAbsence(pendingAction);
291
+ const mandate = readClosedWordSeat(pendingAction, "mandate");
292
+ const origin = readClosedWordSeat(pendingAction, "origin");
277
293
  return {
278
294
  ...(readParkRowTrueSeat(pendingAction, "requiresRealApproval") ? { requiresRealApproval: true } : {}),
279
295
  ...(readParkRowTrueSeat(pendingAction, "mandated") ? { mandated: true } : {}),
296
+ ...(mandate !== undefined ? { mandate } : {}),
297
+ ...(origin !== undefined ? { origin } : {}),
280
298
  ...(ruleOffersAbsence !== undefined ? { ruleOffersAbsence } : {}),
281
299
  };
282
300
  }