@sema-agent/server 7.99.0 → 7.100.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 (115) hide show
  1. package/MIGRATION.md +13 -0
  2. package/USAGE.md +75 -52
  3. package/dist/approval-ask-machine.d.ts +10 -0
  4. package/dist/approval-ask-machine.js +3 -0
  5. package/dist/approval-card.d.ts +74 -1
  6. package/dist/approval-card.js +41 -8
  7. package/dist/approval.d.ts +44 -20
  8. package/dist/approval.js +14 -6
  9. package/dist/boot/config-center.d.ts +16 -8
  10. package/dist/boot/config-center.js +39 -81
  11. package/dist/boot/execution-env.d.ts +1 -2
  12. package/dist/boot/execution-env.js +2 -2
  13. package/dist/boot/leader.js +12 -9
  14. package/dist/boot/memory-consolidation.d.ts +38 -0
  15. package/dist/boot/memory-consolidation.js +35 -0
  16. package/dist/boot/resolve-spec.d.ts +0 -1
  17. package/dist/boot/resolve-spec.js +4 -3
  18. package/dist/boot/runner-deps.d.ts +2 -2
  19. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  20. package/dist/boot/stage-06-runners.d.ts +2 -2
  21. package/dist/boot/stage-06-runners.js +3 -3
  22. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  23. package/dist/boot/stage-07-capability-layer.js +6 -6
  24. package/dist/boot/stage-08-reapers.d.ts +3 -4
  25. package/dist/boot/stage-08-reapers.js +2 -2
  26. package/dist/boot/stage-09-leader.d.ts +2 -3
  27. package/dist/boot/stage-10-http-server.d.ts +2 -3
  28. package/dist/boot/stage-10-http-server.js +2 -2
  29. package/dist/capabilities/center-plugins.d.ts +14 -2
  30. package/dist/capabilities/center-plugins.js +13 -5
  31. package/dist/capabilities/hands-lane.js +2 -1
  32. package/dist/capabilities/scenarios.js +18 -5
  33. package/dist/capabilities/tool-defer.d.ts +2 -2
  34. package/dist/config-catalog.d.ts +2 -2
  35. package/dist/config-catalog.js +12 -13
  36. package/dist/config-center/apply-effective.d.ts +34 -39
  37. package/dist/config-center/apply-effective.js +100 -64
  38. package/dist/config-center/apply-ledger.d.ts +35 -16
  39. package/dist/config-center/apply-ledger.js +25 -18
  40. package/dist/config-center/facade.d.ts +11 -5
  41. package/dist/config-center/facade.js +1 -1
  42. package/dist/config-center/hot-keys-registry.d.ts +11 -4
  43. package/dist/config-center/hot-keys-registry.js +11 -7
  44. package/dist/config-center/restart-signal.d.ts +21 -14
  45. package/dist/config-center/restart-signal.js +16 -22
  46. package/dist/config-center/types.d.ts +3 -3
  47. package/dist/config-types.d.ts +22 -7
  48. package/dist/cross-session-settings.js +1 -1
  49. package/dist/host-lsp-manager.d.ts +29 -0
  50. package/dist/host-lsp-manager.js +14 -0
  51. package/dist/http/active-run-conflict.d.ts +2 -12
  52. package/dist/http/active-run-conflict.js +1 -6
  53. package/dist/http/admission.js +8 -0
  54. package/dist/http/resume-legs.d.ts +1 -1
  55. package/dist/http/resume-legs.js +14 -10
  56. package/dist/http/route-ctx.d.ts +2 -2
  57. package/dist/http/routes/approvals-assistant.d.ts +1 -1
  58. package/dist/http/routes/approvals-assistant.js +2 -2
  59. package/dist/http/routes/memory-compliance.js +5 -0
  60. package/dist/http/routes/memory-origin.js +5 -0
  61. package/dist/http/routes/sessions.js +6 -0
  62. package/dist/http/routes/tasks.js +7 -6
  63. package/dist/http/server.d.ts +6 -5
  64. package/dist/http/wire-types.d.ts +6 -3
  65. package/dist/leader/wire.d.ts +20 -12
  66. package/dist/leader/wire.js +7 -6
  67. package/dist/memory-layer-preflight.d.ts +46 -0
  68. package/dist/memory-layer-preflight.js +121 -0
  69. package/dist/memory-operator-faces.js +17 -1
  70. package/dist/observability/corrupt-read-seat.d.ts +21 -4
  71. package/dist/observability/corrupt-read-seat.js +31 -0
  72. package/dist/observability/fail-open.d.ts +8 -4
  73. package/dist/observability/fail-open.js +8 -4
  74. package/dist/observability/run-terminal.js +1 -0
  75. package/dist/observability/secret-env-scrub.d.ts +1 -0
  76. package/dist/observability/secret-env-scrub.js +1 -0
  77. package/dist/orchestration/workflow-completion-inbox.d.ts +31 -23
  78. package/dist/orchestration/workflow-completion-inbox.js +8 -3
  79. package/dist/plugins/approval-ask-store-file.d.ts +20 -2
  80. package/dist/plugins/approval-ask-store-file.js +21 -3
  81. package/dist/plugins/approval-ask-store-memory.d.ts +2 -0
  82. package/dist/plugins/approval-ask-store-memory.js +39 -13
  83. package/dist/plugins/approval-ask-store-sql.d.ts +24 -5
  84. package/dist/plugins/approval-ask-store-sql.js +27 -3
  85. package/dist/plugins/checkpoint-store-sql.d.ts +9 -3
  86. package/dist/plugins/checkpoint-store-sql.js +19 -2
  87. package/dist/plugins/local-checkpoint-store.js +2 -0
  88. package/dist/plugins/mailbox-store-sql.d.ts +7 -5
  89. package/dist/plugins/mailbox-store-sql.js +6 -4
  90. package/dist/plugins/permission-rule-store-file.js +3 -2
  91. package/dist/plugins/sql-json-column.d.ts +16 -3
  92. package/dist/plugins/sql-json-column.js +15 -4
  93. package/dist/plugins/web-search.d.ts +42 -22
  94. package/dist/plugins/web-search.js +141 -49
  95. package/dist/run-local.js +6 -5
  96. package/dist/runs.js +6 -5
  97. package/dist/runtime-governance.d.ts +50 -3
  98. package/dist/runtime-governance.js +5 -0
  99. package/dist/server-secret-env.d.ts +4 -2
  100. package/dist/task-settings.d.ts +1 -1
  101. package/dist/tool-approval.d.ts +107 -19
  102. package/dist/tool-approval.js +64 -15
  103. package/dist/trace/core-keyset-guard.d.ts +5 -5
  104. package/dist/trace/ledger-sink.js +2 -2
  105. package/dist/trace/project.d.ts +13 -0
  106. package/dist/trace/project.js +22 -5
  107. package/dist/trace/projection-drop.d.ts +2 -0
  108. package/dist/trace/projection-drop.js +6 -0
  109. package/dist/trace/sema-provenance.d.ts +4 -1
  110. package/dist/trace/sema-provenance.js +1 -1
  111. package/dist/trace/task-notification-facets.d.ts +65 -0
  112. package/dist/trace/task-notification-facets.js +31 -0
  113. package/dist/trace/wire-projection-faces.d.ts +3 -3
  114. package/dist/trace/wire-projection-faces.js +3 -3
  115. package/package.json +3 -3
package/MIGRATION.md CHANGED
@@ -7,6 +7,19 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.100.0 —— 十条 BREAKING(部署面 `CROSS_SESSION_INBOUND=accept` 拒启 / 类型面 `MailboxStore.ack` / WebSearch 配置形错响亮 / 请求面 `settings.webSearch` / WebSearch 报错句换形 / deny 部署的「无人可答」记部署政策 / 记忆抹除面新 409 / 审批名单热应用 / 子进程剥名 / local 车道回滚前收敛 ask 账本)
11
+
12
+ - **`CROSS_SESSION_INBOUND=accept` ⇒ 拒启**(core 7.30.0 把 `accept` 从 `crossSessionInbound` 闭集删掉,拆成 `wake`(立刻唤醒收件方)/ `next-turn`(排到收件方下一轮 = 旧 `accept` 的那一档);本仓词表直接取 core 常量,零别名 —— 拒启句列出新词表)。**迁移**:env 写 `next-turn`(原义)或 `wake`;`settings.json` 里的 `accept` 由引擎自己读成 `next-turn` 并通告一次,但 `@sema-agent/settings-schema` 6.0.0 的文件层对 `accept` 响亮拒 ⇒ 一并改写。本版精确钉 core 7.30.1 + settings-schema 6.0.0(锁步序:core → settings-schema → server → 宿主才可写 `wake`)。
13
+ - **`MailboxStore.ack` 返回 `Promise<{ acked: boolean }>`**(core 7.30.0):树外手铸的 `MailboxStore` 实现升级即 tsc 红 —— 围栏拒(盒不在 / 无人持租 / 非持租者)答 `{ acked: false }`,放行答 `{ acked: true }`(删 0 行也是 true)。本仓 SQL 两只孪生已改。
14
+ - **部署 env:WebSearch 旋钮形错 ⇒ 拒启**。`WEB_SEARCH_PROVIDER` 点名了后端时,`WEB_SEARCH_ENDPOINT`(不是 `scheme://host[:port][/path]` 形的绝对 URL)、`WEB_SEARCH_SEARXNG_PARAMS`(任一段不是 `name=value`、名不是参数名、同名两次、把数组 / 对象的 JSON 文本填进来)、`WEB_SEARCH_MAX_RESULTS` / `WEB_SEARCH_TIMEOUT_MS`(非数或 < 1,含 `30s` 这类带单位的)任一形错,进程起不来,stderr 首屏带键名与机读码 `config.web_search_invalid`。改前这些被静默吸收或落默认。**空串 / 未设不受影响;没配 `WEB_SEARCH_PROVIDER` 的部署不读这四键。** **迁移**:按拒启句把那一键改对或删掉。
15
+ - **每请求 `settings.webSearch` 形错 ⇒ `400 request.field_invalid`**(不分车道,请求面 BREAKING),错误句点名字段。**`provider` 缺席或不在词表里同样 400**(改前:整段丢弃、静默改用部署后端)—— 从 `settings.json` 带着拼错 provider(或只写了 `apiKey` / `endpoint`、没写 provider,或一个空的 `webSearch: {}`)的壳,修后提交即 400。**迁移**:按错误句改请求体(`provider` 写 `brave` / `tavily` / `searxng` 之一,不需要 per-request 搜索就整段删掉);`searxngParams` 用对象 `{ name: value }` 或串 `name=value;name=value`。
16
+ - **brave / tavily 非 2xx 报错句换形**:`<provider> search failed: HTTP <status> — <响应体摘录>`(改前 `<provider> search failed (<status>): …`)。按旧句形匹配的消费方要改。
17
+ - **`UNATTENDED_APPROVAL_POLICY=deny` 下「无人可答」记在部署政策名下(S-689,行为面;点名 cli)**:没人能答的受门调用,`tool_end.gate.settlement` 从 `{kind:"human_refused", who:{party:"person"}}` 变 `{kind:"policy_refused", who:{party:"none"}}`;模型读到引擎的策略句,run **不再**以 `haltedOnUserRejection:true` 收束、接着跑(auto 模式否决限回落那一问被拒时改为 `failed` / `classifier.denial_limit`);持久 ask 行当场落 DECIDED(deny) + `settled_by="policy"`,重入回放同一个政策拒。**谁受伤**:按 `haltedOnUserRejection` 或 `settlement.kind === "human_refused"` 判「用户拒了」的壳。**迁移**:改读 `policy_refused`;模型多跑的拍数由部署的 `maxTurns` / 墙钟兜顶。
18
+ - **记忆抹除面:三个既有端点新答 409 `memory.layer_unwritable`(S-693,行为面)**:`POST /v1/memory/erase`、`POST /v1/memory/origin/entries/:entryId/clear`、`POST /v1/sessions/:id/memory/erase`。锁住且有条目的记忆层在场时(默认路径 core 7.30.1 每次项目会话都会锁 `memory/local`),operator / 属主抹除与清标先答 409 `memory.layer_unwritable`(7.99.0:项目层条目 200、`local` 条目 500 + journal 卡盘、下次起服 fatal);同一门序让这种层在场时的非法选择子(400 `config.memory_erasure_request`)、清标空理由(422 `memory.origin_clear_invalid`)与已提交抹除同 `requestId` 重放的 200 收执也先答 409。挂载面里有指向树外目录的软链 ⇒ 未分类 500(判不了,日志点名软链)。**迁移**:按 409 体点名的目录 `chmod u+w <dir>` 后**用同一个 `requestId`** 重试(得到原答复);软链形把软链移出记忆层。core 7.31.0(#1076)提货版撤销本预检与这个码。
19
+ - **审批名单热应用(S-668 波 2,行为面)**:`governance.approvalRequire` 从「改了要重启」变热,`/health.restart.reasons` 与 `GET /v1/config/catalog` 的 `restartSlices` 不再有 `runtime-gates`(同版波 0:`models-tiers` 出、`memory-consolidation` 进 ⇒ 闭集 7 词)。三处行为变化:① 中心下发的名单形错(非数组 / `null` / 非字符串项)或拼法匹配不到 ⇒ 审批组**整组拒**:活配置零变化、候选不成为 LKG、每拍重判直到修好(改前只 warn、候选照样成为 LKG);没有 checkpoint 店的部署收到非空名单同样整组拒(改前重启后拒启 = 崩溃环);② 中心撤掉这一键 ⇒ 立刻回 env 底 `APPROVAL_REQUIRE`(改前保留到下次重启);③ 开了 leader 端点、有 checkpoint 店、只配 `APPROVAL_DENY` / `APPROVAL_NEVER_AUTO` 而 require 为空的部署,leader worker **开始执行**这两张表(改前一张都不执行;收紧向)。**迁移**:按 `sema_registry_approval_require_invalid` 日志修名单;依赖「leader 不执行 deny / never-auto」的部署核对名单。
20
+ - **回滚到 7.99.0(local 车道 + `UNATTENDED_APPROVAL_POLICY=deny`)要先收敛 / 压实 ask 账本(NP-9,持久数据面)**:7.100.0 起部署政策拒当场落 ask 行,File 形账本(`<数据根>/approval-asks/asks.jsonl`)把它记成新动词 `claimTerminalRefuse`。账本里只要还留着这种记录,7.99.0 **启动即拒启**并点名(「verb outside this build's closed set (a DOWNGRADE …)」)—— 不会静默把政策拒读成 VOID,但也起不来。**回滚步骤**:①停止接新任务,等所有 run 收敛(`GET /v1/approvals` 的 `pending` / `livePending` 都空,没有停驻或在飞的审批);②再二选一:让 7.100.0 继续跑到账本到阈压实(压实后账本只剩行快照,7.99.0 读得动,政策结算原样保留),或停机把 `asks.jsonl` 移开(已收敛的部署上只丢已决历史 —— 与 `docs/DEPLOY-PREREQS.md`「要清就停机删文件」同一动作);③再启 7.99.0。SQL 车道(`mysql` / `pg`)不受影响:政策结算是行上的列,7.99.0 原样读得动。
21
+ - **子进程按名剥凭据形变量(core 7.30.1 #1072,运行面)**:工具子 shell、git 子进程与本地语言服务器收不到名字以 `KEYS` / `TOKENS` / `PASSWORDS` 结尾的宿主变量;计数豁免只看紧挨结尾词的那一个词 ⇒ `MAX_NEW_TOKENS` / `MAX_COMPLETION_TOKENS` / `MAX_BATCH_PREFILL_TOKENS` / `DB_FOREIGN_KEYS` / `JSON_SORT_KEYS` 这五个 7.30.0 不剥的非密钥名也被剥(core 收紧过头,归 core 修)。**迁移**:host shell 用 `inheritEnv:[names]` 点名放行,或给变量改个不以这三形结尾的名字;规则全文见 `docs/DEPLOY-PREREQS.md`。
22
+
10
23
  ## 7.99.0 —— 三条 BREAKING(行为面 / 类型面 / 运行时下限)
11
24
 
12
25
  - **显式签发方不再算托管证据(7.99.0,S-671 / B-10;按 clay 裁定「显式签发方不算托管证据」)**:`PRINCIPAL_JWT_*` / `AUTH_BRIDGE_ISSUER` 在场而 `REQUIRE_PRINCIPAL` 未设的部署,7.95–7.98 拒启,7.99.0 起服;托管 ⇔ 运维显式声明 `REQUIRE_PRINCIPAL=true`。**谁受伤**:靠这条推断顶着没设 `REQUIRE_PRINCIPAL` 的多租机器,升级后不再被拦(召回缺口如实登记)。**迁移**:多租机器自己设 `REQUIRE_PRINCIPAL=true`(+ `OPERATOR_PRINCIPALS`),见 `docs/DEPLOY-PREREQS.md` 托管形前置。
package/USAGE.md CHANGED
@@ -89,30 +89,39 @@ ANTHROPIC_MAX_RETRIES=10 # 云 Anthropic 腿
89
89
  ```bash
90
90
  WEB_SEARCH_PROVIDER=brave|tavily|searxng # 唯一的"装配开关":合法词才挂 WebSearch 工具;缺席/非法词 = 不装配(不是挂了报错)
91
91
  WEB_SEARCH_API_KEY=… # brave/tavily 必需(searxng 不读这一键);只进 backend 闭包,永不进模型 prompt 或工具参数
92
- WEB_SEARCH_ENDPOINT=https://searx.example # searxng 必需(实例地址);brave/tavily 下是可选的 base-URL 覆盖(代理/测试)
93
- WEB_SEARCH_MAX_RESULTS=10 # 1..20(夹逼);缺席/非正数/非数字 → 用 backend 默认 10;小数向下取整
94
- WEB_SEARCH_TIMEOUT_MS=10000 # 单次搜索墙钟(ms),下限 1000;缺席/非正数/非数字 → 用 backend 默认 10000
95
- WEB_SEARCH_SEARXNG_PARAMS="engines=bing,duckduckgo;language=zh-CN" # 仅 searxng 腿消费;`;` 分隔 k=v 对;一个都解析不出 → 键整个不铸(不产出空对象)
92
+ WEB_SEARCH_ENDPOINT=https://searx.example # searxng 必需(实例地址);brave/tavily 下是可选的 base-URL 覆盖(代理/测试/**Brave 兼容网关**)
93
+ # Brave 兼容网关(S-669,2026-09-24 实跑):自托管的 Nólë 网关暴露与 Brave 同形的 `GET …/res/v1/web/search`,
94
+ # 认 `X-Subscription-Token`,返回 `web.results[{title,url,description,…}]` —— 用 brave 词 + endpoint 覆写即可,零新词:
95
+ # WEB_SEARCH_PROVIDER=brave
96
+ # WEB_SEARCH_ENDPOINT=https://<gateway-host>:<port>/res/v1/web/search # 完整搜索 URL(不是 base)
97
+ # WEB_SEARCH_API_KEY=<网关给本部署的独立钥匙> # 从部署密钥存放处注入,不进仓、不进日志
98
+ # `GET /v1/capabilities` 的 `webSearch.backend` 仍报 `"brave"`(闭集不变);网关侧按钥匙限速 / 吊销 / 记 client。
99
+ WEB_SEARCH_MAX_RESULTS=10 # ≥1 的数,>20 夹到 20,小数向下取整;缺席/空 → backend 默认 10;非数字 / <1 → 拒启
100
+ WEB_SEARCH_TIMEOUT_MS=10000 # 单次搜索墙钟(ms),<1000 抬到 1000;缺席/空 → backend 默认 10000;非数字(含 "30s" 这类带单位的)/ <1 → 拒启
101
+ WEB_SEARCH_SEARXNG_PARAMS="engines=bing,duckduckgo;language=zh-CN" # 仅 searxng 腿消费;`;` 分隔 name=value;空 → 不铸;任一段不成形 → 拒启
96
102
  WEB_SEARCH_PROBE_ON_BOOT=true # boot 期一次性真出网探活,默认 OFF;只认 "true"/"1"(trim+小写后比较),含糊值当没开
97
103
  ```
98
104
 
99
- 七键逐一(`src/plugins/web-search.ts` `webSearchConfigFromEnv` §294-309;类型/默认/坏值登记见
100
- `src/config-catalog.ts:654-662`):
105
+ 七键逐一(属主 `src/plugins/web-search.ts` `webSearchConfigFromEnv`,每个字段一只判官、与每请求 `settings.webSearch`
106
+ 共用;类型/默认/坏值登记见 `src/config-catalog.ts` 的 `WEB_SEARCH_*` 行)。**只有 `WEB_SEARCH_PROVIDER` 点名了后端时其余
107
+ 旋钮才被读**——没配后端时它们不装配、也不判:
101
108
 
102
109
  | env 键 | 类型 | 默认 | 缺席行为 | 坏值行为 |
103
110
  |---|---|---|---|---|
104
111
  | `WEB_SEARCH_PROVIDER` | 闭集(`brave`\|`tavily`\|`searxng`) | 无 | WebSearch 工具整体不装配(功能缺席,无 warn) | 三词之外的任何值 = 同缺席处理,不装配、不 warn(`webSearchConfigFromEnv` 的词表守卫 `isWebSearchProvider`)。**读面(S-382 起)**:`GET /v1/capabilities` 的 `webSearch.backend` 把这一格的生效值广告成闭集词(缺席/坏值都报 `"none"` —— 两者行为本就相同);端点/密钥/配额**不上 wire**,那些仍只在 operator 面 `GET /v1/config/catalog` |
105
- | `WEB_SEARCH_API_KEY` | secret string | 无 | brave/tavily:后端仍会装配(装配只看 `PROVIDER`),**首次真实工具调用**时抛 `WEB_SEARCH_API_KEY is required for the <provider> provider`(`braveSearch` / `tavilySearch` 的首行守卫);searxng:本键无消费点 | 空字符串同缺席(`env.WEB_SEARCH_API_KEY ?` 只认真值,`webSearchConfigFromEnv` 的条件展开) |
106
- | `WEB_SEARCH_ENDPOINT` | url string | brave/tavily → 各自官方 API;searxng → 无默认 | brave/tavily:落官方 endpoint;searxng:**首次真实工具调用**时抛 `WEB_SEARCH_ENDPOINT (the SearXNG instance URL) is required for the searxng provider`(`searxngSearch` 的首行守卫) | 不做 URL 形校验——写不成 URL 由 `new URL()` 抛出,同样落到"首次调用才现形"那条路径,不拒启 |
107
- | `WEB_SEARCH_MAX_RESULTS` | number | `10` | 用默认 10 | 非数字/≤0 → 回落默认 10;是数字则向下取整;最终值再夹在 `[1,20]`(设 999 也被 clamp 到 20 —— `createWebSearchBackend` 里的 `maxResults` clamp) |
108
- | `WEB_SEARCH_TIMEOUT_MS` | number(ms) | `10000` | 用默认 10000 | 非数字/≤0 → 回落默认;最终值下限夹到 1000ms(`createWebSearchBackend` 里的 `Math.max(1000, …)`) |
109
- | `WEB_SEARCH_SEARXNG_PARAMS` | string(`k=v;k=v`) | 无(不铸键 = 不传 `extraParams`,交给 core adapter 自己的缺省) | 键整个不铸 | 一个 `k=v` 对都解析不出(没有 `=`,或 `k`/`v` 任一为空)→ 同缺席,整串忽略,不拒启不 warn(`parseSearxngParams`);env 侧值恒为字符串,不会触发下面 per-request 那个"数组被误当对象吸收"的边角(见 §9.5) |
112
+ | `WEB_SEARCH_API_KEY` | secret string | 无 | brave/tavily:后端仍会装配(装配只看 `PROVIDER`),**首次真实工具调用**时抛 `WEB_SEARCH_API_KEY is required for the <provider> provider`(`braveSearch` / `tavilySearch` 的首行守卫);searxng:本键无消费点 | 空串 / 纯空白同缺席;内容不设形(它是凭据) |
113
+ | `WEB_SEARCH_ENDPOINT` | url string(brave 下 = 完整搜索 URL,可指向 Brave 兼容网关如 Nólë) | brave/tavily → 各自官方 API;searxng → 无默认 | brave/tavily:落官方 endpoint;searxng:**首次真实工具调用**时抛 `WEB_SEARCH_ENDPOINT (the SearXNG instance URL) is required for the searxng provider`(`searxngSearch` 的首行守卫) | 空串同缺席(compose 模板 `${WEB_SEARCH_ENDPOINT:-}` 铸的就是空串)。**不是 `scheme://host[:port][/path]` 形的绝对 URL ⇒ 拒启**(`searx.internal`、`localhost:8888` 这类少了 `scheme://` 的形都算;判据与模型路由 base URL 拒启同一只)。拒启句带键名与机读码 `config.web_search_invalid`,**不回显值**(这一格可能带凭据) |
114
+ | `WEB_SEARCH_MAX_RESULTS` | number | `10` | 用默认 10 | 空串同缺席;**非数字或 < 1 ⇒ 拒启**(`config.web_search_invalid`;改前静默落默认 10);是数则向下取整,再夹到 `[1,20]`(设 999 被夹到 20 —— 夹紧不是形错) |
115
+ | `WEB_SEARCH_TIMEOUT_MS` | number(ms) | `10000` | 用默认 10000 | 空串同缺席;**非数字(`"30s"` 这类带单位的也算)或 < 1 ⇒ 拒启**(`config.web_search_invalid`;改前静默落默认 10000 —— 写了 30s 的运维实际拿到 10s);是数则向下取整,再抬到 ≥ 1000ms |
116
+ | `WEB_SEARCH_SEARXNG_PARAMS` | string(`k=v;k=v`) | 无(不铸键 = 不传 `extraParams`,交给 core adapter 自己的缺省) | 键整个不铸 | 空串 / 纯空白 / 只有分号 = 缺席。文法与每请求 `settings.webSearch.searxngParams` 同一套:`name=value` 用 `;` 分隔(尾分号可有可无),名是参数名(字母 / 数字 / `_` / `-` / `.`),值非空,同名只许一次。**任一段不成形 ⇒ 拒启**(`config.web_search_invalid`,句里点名是哪一段 / 哪个名):把数组或对象的 JSON 文本(`["engines=bing"]`、`{"engines":"bing"}`)填进来、一段没有 `=`、名为空、同名两次。改前这些被静默跳过或吸成垃圾参数发给实例 |
110
117
  | `WEB_SEARCH_PROBE_ON_BOOT` | boolean(仅认 `"true"`/`"1"`) | `false`(OFF) | 不探活 | trim+小写后不等于 `"true"`/`"1"` 的任何拼法(含 `"yes"`/`"on"`)一律当 `false`;探活失败(网络不通/后端拒绝)只 `warn`(`web_search_probe_failed`),**不拒启**——功能型能力缺席走降级,不是保护型旋钮(`main.ts:1028-1034`) |
111
118
 
112
- **结论:七键无一在 boot 期拒启。** 与本仓其它安全轴旋钮(`MODEL_DEGRADE_ON`、思考档三键等,拼错即拒启)
113
- 姿势不同——WebSearch 挂不挂是**功能面**取舍,不是安全边界,七键统一走"回落/降级",不是"fail-closed 拒启"。
114
- 真正的配置错误(缺 key / endpoint 错)只在**首次真实工具调用**时才现形,以 tool_result 错误文本形式回给
115
- 模型,不是 HTTP 层错误、不影响 boot、也不使任务整体 `failed`(细节见 §9.5)。
119
+ **结论:缺席与形错分两件事。** 缺席(未设 / 空串,以及 `WEB_SEARCH_PROVIDER` 不是三词之一)一律走「不装配 / 用默认」,
120
+ 不 warn —— WebSearch 挂不挂是**功能面**取舍,不是安全边界。**形错**(写了但写不成该有的形:`WEB_SEARCH_ENDPOINT`、
121
+ `WEB_SEARCH_SEARXNG_PARAMS`、`WEB_SEARCH_MAX_RESULTS`、`WEB_SEARCH_TIMEOUT_MS` 四键)**拒启**,stderr 首屏一句带键名 +
122
+ 机读码 `config.web_search_invalid`:形错此前被静默吸收(垃圾参数发给实例)或静默落到一个运维没选过的默认值,
123
+ 与「配对了」在外面同形。仍只在**首次真实工具调用**时现形的只剩「形对但不通」一类(缺 key、地址对但连不上),
124
+ 以 tool_result 错误文本回给模型,不是 HTTP 层错误、不使任务整体 `failed`(细节见 §9.5)。
116
125
  - 结果是**不可信输入**:core 会 `delimitUntrusted` 围栏并**重新施加** `allowed_domains`/`blocked_domains`
117
126
  地板,所以 backend 遵不遵守 `opts` 是优化不是正确性要求 —— 换 provider(包括换成自建 SearXNG)
118
127
  不会削弱域名地板。
@@ -493,7 +502,10 @@ SEND_USER_FILE_SANDBOX_PUT_ENDPOINT=… # 可选:沙箱直传 PUT 的端
493
502
  SEMA_REGISTRY_URL=http://<config-center-host>:3100 # 启动拉 GET /api/config/effective(Bearer+ETag),覆盖 env 兜底
494
503
  SEMA_REGISTRY_TOKEN=<SERVICE_PULL_TOKEN 的值> # 取自配置控制面主机 .env;只读拉取令牌
495
504
  # SEMA_REGISTRY_TOKEN_FILE=/path/to/token # 或:凭证文件(与上一行二选一,都设=拒启)。每条中心请求都重读 ⇒ 轮换 / 换账号改写文件即可,
496
- # 不重启;文件不可读 = error 级 center_credential_unreadable,不当作无凭证。
505
+ # 中心请求面不重启;文件不可读 = error 级 center_credential_unreadable,不当作无凭证。
506
+ # ⚠️ 换账号(凭证身份变化)后,只在启动期物化的面 —— 技能 / MCP / A2A / 场景 —— 仍是上一个
507
+ # 身份的,直到重启(`/health` 报 restartRequired,reasons 含 skills);热面(models / roles /
508
+ # limits / 审批名单 …)在新身份第一份候选到达那一拍换代。「换账号即降档 + 当场拉取」候 S-667(不在 7.100.0)。
497
509
  # 用户 JWT 形(CLI 登录)⇒ skill 正文 / prompt 产物走 /api/v1/me/…;盘上 LKG 与 prompt 状态按「worker + 凭证身份」分箱。
498
510
  SEMA_REGISTRY_DRY_RUN=true # 安全灰度:只 LOG 中心配置 vs env 推导的差异,不 apply
499
511
  SEMA_REGISTRY_WORKER=<worker名> # 可选:拉取 /effective?worker=<名> 取该 worker 的 roster(reconciler 按 worker 注);不设=全局 roster(向后兼容)
@@ -510,7 +522,11 @@ FLEET_ADVERTISE_ADDRESS=http://<本机可达IP>:8090 # 可选:设了才启 flee
510
522
  |---|---|---|
511
523
  | models / roles / default 模型 | **热**(下一 refresh 拍) | 全 boot Runner **原子换代**(swap 失败=候选整拒,活配置零触碰);此前「改 models 需重启」的时代已随 Runner swap 腿落地终结 |
512
524
  | pricing / keys(env-名引用)/ prompts / teams | **热** | 值换代即生效;cost 族限额座(rate limit / cost quota)同热(限额=纯比较参数,窗内累计不动;窗长换代=记账周期重开) |
513
- | **tier 变更**(models-tiers plane 与 tier-frozen 基线不一致) | **defer 到重启**(唯一 defer 臂) | 候选 durable 落地(LKG,`CONFIG_LKG_DURABLE`)后 `/health` 报 `restartRequired`;无 durable handoff ⇒ `/health` 报 `modelPlaneDeferred{version,since,blockedReasons}` 候运维(不强制重启,plane 保持未应用) |
525
+ | tier 变更(档位组 / 档位绑定) | **热**(下一 refresh 拍) | 与 models 同一条换代腿:tier 表随模型面在 commit 前原子换进全部 boot Runner,下一任务按新档位路由,`/health` 不报 `restartRequired`(7.100.0 起 `models-tiers` 重启理由退役——它在生产上从未发出过) |
526
+ | plugins(插件引用) | 重启生效 | 插件只在启动期物化;改了之后 `/health` 报 `restartRequired`,`restart.reasons` 含 `skills`(7.100.0 前这一改动**不报**,要等一次无关的重启) |
527
+ | skills / mcp / a2a / scenarios(技能 / MCP / A2A / 场景) | 重启生效 | 只在启动期物化;中心改了 ⇒ `/health` 报 `restartRequired`,`restart.reasons` 各含其名。**换账号**(凭证身份变化)同理:这些面仍是上一身份的直到重启(候 S-667) |
528
+ | 审批名单(`governance.approvalRequire`) | **热**(下一 refresh 拍) | 7.100.0 起:下一个任务调名单里的工具即停车等审批;撤键回 env 底(`APPROVAL_REQUIRE`)。**没有 checkpoint 店**的部署(`DURABLE_APPROVAL` 关 / 后端无 checkpoint 面)发非空名单 ⇒ 审批组**整组拒**(旧名单继续服务、`configAppliedVersion` 停旧代、拒因见日志 `sema_registry_approval_require_invalid` 与诊断端点 `configApply.groups.approvalGate`)—— 与启动期「名单已配却无 durable 门 ⇒ 拒启」同一条判据。此前 `/health` 报 `restartRequired`(`runtime-gates`,该理由退役) |
529
+ | 记忆整理驱动席(仅 `MEMORY_CONSOLIDATION_DRIVER=on`) | 重启生效 | 整理跑用的模型在启动期解析一次;中心改了 roles / 档位 / 该条目录后 `/health` 报 `restartRequired`,`restart.reasons` 含 `memory-consolidation`(7.100.0 新理由;此前不报) |
514
530
 
515
531
  生效与否的观测口=`/health` 世代账键(`configTargetVersion`/`configAppliedVersion`+两 ordinal+`configApplyStaleMs`,见 API 表 `/health` 行)——target≠applied 持续=「改了没生效」的机读信号。
516
532
  - **仅 `/v1/tasks`(同步)+ `/v1/runs`(异步)**——`/v1/tasks/stream` 不支持(多次尝试非单流,400)。与 `verify` **互斥**(同时给 → 400)。
@@ -657,10 +673,18 @@ UNATTENDED_APPROVAL_POLICY=park
657
673
  没有 park 设施的部署由引擎 fail-closed 拒绝并告诉模型「没有可停靠的 durable 审批门」。
658
674
  - `deny` ⇒ **真无人值守/headless 部署的显式声明**:这类 ask 当场拒绝,让模型自己改道,不积压一堆
659
675
  等不到人的挂起 run。它是**收紧**方向(不放行任何东西)。
676
+ 这一拒记在**部署政策**名下,不记在人名下(7.100.0 起):那次调用的 `tool_end.gate.settlement` 是
677
+ `{kind:"policy_refused", who:{party:"none"}}`;模型读到的是「部署政策拒了、此处没有交互审批,别重试,
678
+ 先做不需要审批的部分」,run 接着跑(auto 模式下分类器连拒后回落的那一问被拒时,引擎按「无人可判」停下
679
+ run:终局 `failed` / `classifier.denial_limit`)。此前记成 `human_refused` / `who.party:"person"`,模型被告知
680
+ 「用户不想继续,停下来等他」,run 以 `haltedOnUserRejection` 收束 —— 而这台部署自己声明了没人会来。
681
+ 少数情形记的是 `approval_window_expired`,同样不在人名下:活卡已送达、本服务自己的窗走完没人点,**且**没有持久 ask 店(或 ask 店
682
+ 当时报错、结束等待的只能说是本服务的窗)。有 ask 店时窗走完、终局 claim 赢下的那一支记的是 `policy_refused`(行上同落 `settled_by`)。
660
683
  - **7.34.0 的行为变化(park 侧)**:此前「窗走完没人答」是**当场拒绝 + 一条 error 回模型**(模型往往就
661
684
  绕开了那次治理);现在它与其余「无人可答」的情形一样走 park。要恢复旧结局请显式配 `deny`。
662
685
  - 纯部署级:**不看**客户端的任何表态/权限模式。协调器整体关掉(`TOOL_APPROVAL_ENABLED` 关)时这个旋钮
663
- 没有施加对象;`APPROVAL_REQUIRE` 名单里的工具走的是 durable 审批门,不受它影响。
686
+ 没有施加对象。`APPROVAL_REQUIRE` 名单只决定「这只工具要不要审批」,与本旋钮无关;协调器在场时,名单工具的
687
+ 审批与其余 ask 走同一条路、同一个政策(`deny` 部署上同样当场拒)。
664
688
  - 🔴 **与 `STREAM_APPROVAL_ENABLED` 正交**:把流内协议开关关掉**不会**回滚 R-13 —— 它只是不发
665
689
  `approval_request`、不落 ask 行、不起收敛器腿,活卡窗到期照样走 park(park 的承载是引擎的 checkpoint,
666
690
  不是 server 的 ask 行,「有 durable 设施但没开协议」正是 R-13 要救的那类部署)。要回到旧的「窗满即拒」
@@ -1605,7 +1629,7 @@ armed ⟺ 本次请求 permissionMode 是 "auto" ∧ 本部署装配了分类
1605
1629
  | 旋钮 | 缺省 | 语义 |
1606
1630
  |---|---|---|
1607
1631
  | `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 轴) |
1608
- | `CROSS_SESSION_INBOUND` | 未设 | 跨终端会话设计线:CC settings 键名 `crossSessionInbound` 的**部署/组织层**(引擎层形的 `managed` 层,CC `policySettings` 对位)—— 本部署对**入站跨会话消息**的治理三态:`accept` 投递 / `hold` 停在收件会话的待审队列(模型看不见、不能据此行动)/ `refuse` 本部署整体退出这条车道。🔴 **未设 ≠ `accept`**:未设 = 这一层不表态,由其余层与引擎的 mode-parity 判定决定。词表外的值**拒启**。⚠️ **生效前提**:引擎的 peer 目录席(`RunnerDeps.peerDirectory`)在场——本版**未接**该席,故本旋钮今天「就位待命」不生效(目录席到货那天零改动即生效);目录行见 `GET /v1/config/catalog`(approval 域,security 轴) |
1632
+ | `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 轴) |
1609
1633
  | `CROSS_SESSION_DIALOG_EXPIRY` | 未设(⇒ 引擎缺省 `5m`) | 同族:CC settings 键名 `dialogExpiry` 的部署层 —— 被 hold 的跨会话消息等待人审多久后按**安全缺省**结算(过期丢弃,**带回执**告知发送方,不静默吞)。词表 `60s`/`5m`/`10m`/`never`(`never` = 不设期限);未设 ⇒ 本仓**不铸键**,由引擎落它自己的缺省(不复制上游缺省值)。词表外拒启。⚠️ 生效前提同上。另注:CC 的同名键还有第二个消费面(转发到远端客户端的审批对话框停靠时长),本旋钮**今天不驱动**那一面 |
1610
1634
 
1611
1635
  **自查读面**(`GET /v1/capabilities.permissionModeAuto`,壳的 `sema doctor permissions` 消费;下例第一行是
@@ -1660,20 +1684,22 @@ GET /v1/capabilities?permissionMode=default
1660
1684
  是唯二两条配置来源。当 per-request 那条腿"生效"(见下面的受理门槛)时,它产出一个**全新、完全独立**的
1661
1685
  `WebSearchBackendConfig`,**整只替换**掉 env 配出来的 backend —— 不是把 per-request 给的字段一个个覆盖到
1662
1686
  env 的 config 上,env 的其余字段(尤其是 `WEB_SEARCH_TIMEOUT_MS`)**不会**被继承到 per-request 那次调用里
1663
- (`src/capabilities/scenarios.ts:264-266`):
1687
+ (`src/capabilities/scenarios.ts` 的 `requestWebSearchBackend`,判决来自 `src/plugins/web-search.ts` 的
1688
+ `judgeRequestWebSearch`):
1664
1689
 
1665
1690
  ```
1666
- const reqWebSearch = deps.requirePrincipal !== true
1667
- ? webSearchConfigFromSettings(req.settings?.webSearch)
1668
- : undefined;
1669
- const webSearch = reqWebSearch ? createWebSearchBackend(reqWebSearch) : deps.webSearch;
1691
+ judgeRequestWebSearch(req.settings?.webSearch) // 单用户车道才问;多租车道恒答 absent
1692
+ absent ⇒ 部署后端(env 那只;env 也没配 ⇒ 工具不装配)
1693
+ honored ⇒ 这次调用用 per-request 那只(整段整取)
1694
+ malformed ⇒ 新鲜提交已在受理面 400(含 provider 缺席 / 词表外);只有续跑重放会走到这里 ⇒ 这一次不装配
1695
+ WebSearch(不拿部署后端顶替调用方点名的目的地)+ 一行 warn `web_search_settings_unusable` 点名字段
1670
1696
  ```
1671
1697
 
1672
- `reqWebSearch` 非 `undefined`(即 `settings.webSearch.provider` 是合法词**且**本部署是单用户车道)时,
1698
+ 判决为 `honored`(即 `settings.webSearch` 形对、`provider` 是合法词,**且**本部署是单用户车道)时,
1673
1699
  `deps.webSearch`(env 配的 backend)整个不参与这次请求 —— 谁赢是**二选一**,不是字段级合并。
1674
1700
 
1675
1701
  **受理门槛(单用户车道)**:🔒 per-request `settings.webSearch` 只在 `REQUIRE_PRINCIPAL !== true`(单用户 /
1676
- TOC 本地形)时被读取;`REQUIRE_PRINCIPAL=true`(多租户)上这个键**结构性够不着**——不是被拒、也不留任何
1702
+ TOC 本地形)时被**采纳**;`REQUIRE_PRINCIPAL=true`(多租户)上一段**形对**的配置**结构性够不着**——不是被拒、也不留任何
1677
1703
  `warn`/`capabilities` 位说"你发的 webSearch 被忽略了"(对比 `mcpServers` 有 `capabilities.mcpInjection` +
1678
1704
  `mcp_injection_dropped` 日志可读;`settings.webSearch` 没有对应的能力探测位,`src/http/wire-types.ts:320-323`)。
1679
1705
  🔴 **别把 S-382 的新位读成这个探测位**:`GET /v1/capabilities` 的 `webSearch.backend`(S-382)说的是
@@ -1681,17 +1707,20 @@ TOC 本地形)时被读取;`REQUIRE_PRINCIPAL=true`(多租户)上这个键**结
1681
1707
  依旧是结构性够不着、无 warn、无位,这一段的结论一字未变。
1682
1708
  原因是安全边界(与多租户能力配置隔离规则同源):per-request `endpoint`/`apiKey` 是能力配置,多租户下一个租户把 `searxng`
1683
1709
  指向内网地址就是 SSRF,所以多租户上只认部署 env 配的 backend。
1710
+ **形判不分车道**:一段**形错**的 `settings.webSearch`(见下表「形错」列)在任何车道上都在提交当场答
1711
+ `400 request.field_invalid`,错误句逐字点名字段(`settings.webSearch.<字段> must be …`)——与 `settings.hooks`
1712
+ 同姿势:形在受理面判,采不采纳由单用户闸决定。
1684
1713
 
1685
- **`settings.webSearch` 键表**(`src/plugins/web-search.ts` `webSearchConfigFromSettings` §318-335;类型契约见
1686
- `@sema-agent/sdk` 9.0.0 `settings.d.ts` `SettingsWebSearch`):
1714
+ **`settings.webSearch` 键表**(`src/plugins/web-search.ts` `judgeRequestWebSearch`,每个字段一只判官、与部署 env 的
1715
+ 同名旋钮共用;类型契约见 `@sema-agent/sdk` 9.0.0 `settings.d.ts` `SettingsWebSearch`)。整段本身必须是对象(`null` = 缺席):
1687
1716
 
1688
- | 键 | 类型 | 必填 | 默认 | 消费点 |
1717
+ | 键 | 类型 | 必填 | 默认 | 形错(⇒ 400) |
1689
1718
  |---|---|---|---|---|
1690
- | `provider` | `"brave"|"tavily"|"searxng"` | 是(缺席/非三词之一 ⇒ 整个 `settings.webSearch` 被丢,回落到 env 的 backend,**不报错**) | 无 | `webSearchConfigFromSettings` 的词表守卫(与 env 腿**同一只** `isWebSearchProvider`,词表属主 = `WEB_SEARCH_PROVIDERS`) |
1691
- | `apiKey` | string(明文) | brave/tavily 建议带(缺了首次调用才报错,见下表);searxng 不读 | 无(不继承 env 的 `WEB_SEARCH_API_KEY`) | `webSearchConfigFromSettings` 的 `apiKey` 条件展开 |
1692
- | `endpoint` | string | searxng 必需(缺了首次调用才报错);brave/tavily 可选覆盖 | 无(不继承 env 的 `WEB_SEARCH_ENDPOINT`) | `webSearchConfigFromSettings` 的 `endpoint` 条件展开 |
1693
- | `searxngParams` | `Record<string,string>`(对象)或 `"k=v;k=v"`(字符串) | 否 | 无 | `webSearchConfigFromSettings` 的 `searxngParams` 臂,解析逻辑与 env 腿共用 `parseSearxngParams`。⚠️ **未列入已发布的 `@sema-agent/sdk` 8.8.0 `SettingsWebSearch` 类型**(该接口只有 `provider`/`apiKey`/`endpoint`/`maxResults` 四键、无开放下标)——server 侧代码认这个键,但当前发布的 TS 类型接不到它;手写 JSON 请求体仍可以发,server 会照常解析(源码头注自述:字段和消费点先落地,配置录入面尚未跟上,是一笔尚未还清的既有债务) |
1694
- | `maxResults` | number | 否 | 10(与 env 同一 clamp,`[1,20]`,小数向下取整) | `webSearchConfigFromSettings` 的 `maxResults` 归一 |
1719
+ | `provider` | `"brave"|"tavily"|"searxng"` | **是** | 无 | 缺席、不是字符串、或不是三词之一(大小写 / 首尾空白照旧归一)——一段 `settings.webSearch` 就是在点名目的地,认不出就 400 点名 `settings.webSearch.provider`,**不**静默换成部署 env 的 backend(改前:整段丢弃、回落 env backend、不报错)。词表守卫与 env 腿**同一只** `isWebSearchProvider`;env 腿词表外是「不装配」(部署自己的缺席) |
1720
+ | `apiKey` | string(明文) | brave/tavily 建议带(缺了首次调用才报错,见下表);searxng 不读 | 无(不继承 env 的 `WEB_SEARCH_API_KEY`);空串同缺席 | 不是字符串 |
1721
+ | `endpoint` | string | searxng 必需(缺了首次调用才报错);brave/tavily 可选覆盖 | 无(不继承 env 的 `WEB_SEARCH_ENDPOINT`);空串同缺席 | 不是字符串,或不是 `scheme://host[:port][/path]` 形的绝对 URL(`localhost:8888` 这类少了 `scheme://` 的也算) |
1722
+ | `searxngParams` | `Record<string,string>`(对象)或 `"name=value;name=value"`(字符串) | 否 | 无;空串 / 空对象同缺席 | 与 env `WEB_SEARCH_SEARXNG_PARAMS` 同一套文法:数组、数字、一段不是 `name=value`、名不是参数名(字母 / 数字 / `_` / `-` / `.`)、值为空或不是字符串、同名两次。⚠️ **未列入已发布的 `@sema-agent/sdk` 8.8.0 `SettingsWebSearch` 类型**(该接口只有 `provider`/`apiKey`/`endpoint`/`maxResults` 四键、无开放下标)——server 侧代码认这个键,但当前发布的 TS 类型接不到它;手写 JSON 请求体仍可以发,server 会照常解析(源码头注自述:字段和消费点先落地,配置录入面尚未跟上,是一笔尚未还清的既有债务) |
1723
+ | `maxResults` | number | 否 | 10(与 env 同一 clamp,`[1,20]`,小数向下取整;999 夹到 20 不是形错) | 不是数,或 < 1(`"10"` 这种字符串也算) |
1695
1724
  | *(无)* `timeoutMs` | — | — | — | **per-request 车道没有这个字段**——即便部署用 `WEB_SEARCH_TIMEOUT_MS` 配了非默认超时,per-request 生效那次调用永远退回 backend 自己的默认(10000ms,下限 1000ms),因为"整段整取"意味着 env 的 `timeoutMs` 根本不在 `reqWebSearch` 那个新对象里 |
1696
1725
  | *(无)* `fetchImpl` | — | — | — | 同上,仅测试注入用,per-request 车道不可达 |
1697
1726
 
@@ -1699,33 +1728,27 @@ TOC 本地形)时被读取;`REQUIRE_PRINCIPAL=true`(多租户)上这个键**结
1699
1728
  (`src/task-settings.ts:309`,集外顶层键 400 `request.body_shape`),但 `webSearch` **自己的子键没有闭集门**
1700
1729
  ——不像 `settings.permissions.*` 有 `TASK_SETTINGS_PERMISSION_KEYS` 逐键拒(`src/task-settings.ts:367-403`
1701
1730
  的 `taskSettingsKeyIssue` 只扫 `settings.*` 顶层和 `settings.permissions.*` 两层)。`settings.webSearch` 下
1702
- 塞一个上表之外的键(拼错的 `mxResults` 之类)**不会** 400,会被 `webSearchConfigFromSettings` 静默无视
1703
- ——门槛之外没有"未知键"这一说。
1731
+ 塞一个上表之外的键(拼错的 `mxResults` 之类)**不会** 400,会被判官静默无视
1732
+ ——门槛之外没有"未知键"这一说(子键闭集是另一件事,未随本版做)。
1704
1733
 
1705
- **错误形**(逐条对应任务书里的四问;全部经**首次真实工具调用**才现形,均为 WebSearch 工具的
1706
- `tool_result` 错误文本,回到模型的对话里,**不是** HTTP 层 `errorCode`,**不会**使 `TaskResult` 整体
1707
- `failed`,也不拒启/不拒收请求本身;core `dist/tools/web.js:840-919` `createWebSearchTool` 统一兜底):
1734
+ **错误形**(形错已在上表:提交当场 400。下表是**形对**之后的失败,全部经**首次真实工具调用**才现形,均为
1735
+ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是** HTTP 层 `errorCode`,**不会**使 `TaskResult` 整体
1736
+ `failed`;core `dist/tools/web.js` `createWebSearchTool` 统一兜底,失败卡上的 `retryable` 由 core 从错误文本分档):
1708
1737
 
1709
1738
  | 触发条件 | 现象 | 是否 retryable(core `classifySearchFailure`) |
1710
1739
  |---|---|---|
1711
- | 坏 `provider`(`settings.webSearch.provider` 非三词之一,或缺席) | **不是错误** —— `webSearchConfigFromSettings` 返回 `undefined`,整段回落到 env 配的 backend(env 也没配 ⇒ WebSearch 工具不装配,模型看不到这个工具);无 warn、无日志 | 不适用 |
1712
1740
  | 缺 `apiKey`(brave/tavily,env 或 per-request 均未给) | 首次调用抛 `WEB_SEARCH_API_KEY is required for the <provider> provider`,core 接住转成 `Error (WebSearch): the search backend failed. …` 文本回模型 | `"unknown"`(消息里没有 HTTP 状态码模式,`classifySearchFailure` 落最后一条默认分支) |
1713
- | `searxngParams` 非对象/非字符串(如数字、布尔、`null`) | 静默丢弃——`parseSearxngParams` 落到"非 object 且非 string"分支,返回 `undefined`,键整个不铸,**不报错、不 warn** | 不适用 |
1714
- | ⚠️ `searxngParams` 是**数组**(如 `["engines=bing"]`) | `typeof [] === "object"` 让它落进对象分支:`Object.entries` 按数组下标产出 `{"0":"engines=bing"}`——**不被拒绝**,但产出的键是数字字符串、值是未拆分的原始 `"k=v"` 串,传给 core adapter 的 `extraParams` 后是一组没有意义的查询参数(不是解析出 `engines=bing`)。这条边角只在 per-request 车道可达(env 值恒为字符串,不会触发);已用与源码逐字一致的独立复现脚本核验(见收车档),未改代码 | 不适用(不是异常路径) |
1715
- | 搜索后端返回**非 2xx HTTP 响应**(鉴权失败/限流/服务端 5xx 等) | 三个 provider 各自的 `!res.ok` 分支抛错,消息形固定为 `"<provider> search failed (<status>): <body首 200 字符>"`(brave/tavily,`braveSearch` / `tavilySearch` 的 `!res.ok` 分支)或 `"SearXNG <status> <statusText> from <url>"`(searxng,core adapter) | ⚠️ **实测几乎恒为 `"unknown"`,不是按状态码分档**:`classifySearchFailure` 的状态码分支要求 `http`/`status`/`code`/`error` 四词之一紧邻数字前(`\D{0,12}`内),但本仓三个 adapter 的消息把状态码写在 `failed (…)`/`SearXNG …` 之后,不触发该分支;独立复现脚本核验(见收车档)brave/tavily/searxng 的 429/500/403/403 全部落到最后一条默认分支 `retryable:"unknown"`。**真正被分类对的只有两条兜底正则**:上游错误体文本里若真含 `"rate limit"` 才判 429 类 `true`,含 `"timed out"`/`"timeout"` 才判 408 类 `true`——都取决于上游返回的具体措辞,不是本仓能保证的 |
1741
+ | 搜索后端返回**非 2xx HTTP 响应**(鉴权失败/限流/服务端 5xx 等) | brave / tavily:一句形 `"<provider> search failed: HTTP <status> — <响应体首 200 字符>"`(`src/plugins/web-search.ts` 的 `httpFailure`,两腿唯一铸点);searxng:`"SearXNG <status> <statusText> from <url>"`(core adapter 自己抛,本仓不改写) | brave / tavily:**按状态码分档** —— 429 与 5xx ⇒ `true`,其余 4xx ⇒ `false`(句里带 `HTTP <status>` 令牌,core 的分档认得出)。⚠️ searxng 腿**仍是 `"unknown"`**:core adapter 那句话的状态码前没有状态词,分档认不出 —— 归 core 修(源头),修前只有上游错误体里碰巧写着 `rate limit` / `timed out` 时才分得对 |
1716
1742
  | `endpoint` **网络层**不可达(连接被拒/DNS 解析失败/fetch 自身抛错,尚未拿到任何 HTTP 响应) | `fetch` 抛出的传输层错误(`ECONNREFUSED`/`ENOTFOUND`/`fetch failed` 等 Node/undici 标准措辞)被同一 `catch` 接住 | `true`——这条路径的错误文本天然含 `econnrefused`/`enotfound`/`fetch failed`/`dns` 等词,`classifySearchFailure` 的网络故障正则能命中(与上一行"已拿到 HTTP 响应但非 2xx"是两条不同的失败路径,别混淆) |
1717
1743
 
1718
- ⚠️ **"坏 provider 静默回落"这条对调用方是否可观察,取决于部署 env 有没有配 backend**:上表第一行说
1719
- "env 也没配 ⇒ 工具不装配、模型看不到这个工具"——那只是**部署 env 同样缺席**这一种情形。若部署 env
1720
- **已经**配了合法 backend(例如 `WEB_SEARCH_PROVIDER=brave`),调用方 per-request 传一个拼错的 `provider`
1721
- (如 `"searx"`)、或带着一个本想打到自建 SearXNG 的 `endpoint`,`settings.webSearch` 整段被丢弃、静默回落
1722
- 到 env 的 brave backend——**WebSearch 工具照常挂载、照常可用**,查询实际发给了 env 配的 provider,不是
1723
- 调用方以为自己指定的那个;工具存在这一事实本身**不能**证明 per-request 的 `provider`/`endpoint` 真的
1724
- 生效了,两种情形(per-request 生效 / per-request 被静默丢弃回落 env)在壳侧不可判别(此条经独立复现验证,
1725
- 见收车档)。
1744
+ ✅ **"坏 provider 静默回落"这一形自 7.100.0 起没有了**:改前部署 env 配了合法 backend(例如 `WEB_SEARCH_PROVIDER=brave`)
1745
+ 时,调用方 per-request 传一个拼错的 `provider`(如 `"searx"`)、或带着一个本想打到自建 SearXNG 的 `endpoint`,
1746
+ `settings.webSearch` 整段被丢弃、静默回落到 env 的 brave backend —— 工具照常挂载,查询发给了 env 配的 provider,
1747
+ 壳侧判别不了。现在同一请求在提交当场答 `400 request.field_invalid` 点名 `settings.webSearch.provider`;WebSearch
1748
+ 工具在场 ⇔ 调用方的 per-request 配置被采纳(单用户车道)或调用方根本没带(用部署默认)。
1726
1749
 
1727
1750
  **`apiKey` 明文与 env 槽边界**:server 收到的 `settings.webSearch.apiKey` 是**明文字符串**,没有任何服务端
1728
- 密钥槽位/引用间接——收到什么字符串就直接进 backend 闭包(`webSearchConfigFromSettings` 的 `apiKey` 条件展开,与 `WEB_SEARCH_API_KEY` 同一条消费路径,
1751
+ 密钥槽位/引用间接——收到什么字符串就直接进 backend 闭包(判官 `judgeRequestWebSearch` 只判「是不是字符串」,与 `WEB_SEARCH_API_KEY` 同一只判官、同一条消费路径,
1729
1752
  见 `src/plugins/web-search.ts` 的 `WebSearchBackendConfig.apiKey` 字段注)。**server 本批不改受理面**:明文字段的形状维持原样。调用方(壳)如何在
1730
1753
  自己机器上管理这份明文是调用方的事——例如 cli 壳侧的约定是在**调用方自己的环境**里按
1731
1754
  `SEMA_WEBSEARCH_KEY_<PROVIDER>` 这样的命名空间存放每个 provider 的 key,由壳在本地读出后把明文塞进请求体;
@@ -46,4 +46,14 @@ export declare function deriveAskId(sourceTaskId: string, runId: string, toolCal
46
46
  /** batch_id = sha256('batch' ⊕ sourceTaskId ⊕ runId ⊕ legKey).slice(0,64)。同一 (sourceTaskId, runId,
47
47
  * legKey) 恒派生同一批——批 = 同一决策点的兄弟 ask 集合,故不掺 toolCallId。 */
48
48
  export declare function deriveBatchId(sourceTaskId: string, runId: string, legKey: string): string;
49
+ /**
50
+ * 🔴 S-689 codex R1-F1:**单 ask 批** —— batch_id = sha256('ask-batch' ⊕ askId).slice(0,64)。
51
+ *
52
+ * 批是 **park 路由的单位**:窗到期赢家把整批转投递面(OPEN → ROUTING_UNBOUND)、兄弟连坐 VOID,core 随后 park,
53
+ * 这条腿就此结束 —— 所以「整腿一批」成立。不走 park 路由的部署(`UNATTENDED_APPROVAL_POLICY=deny`)上这个前提
54
+ * 不在:core 拿到的是部署政策拒,腿**继续跑**。整腿一批时,首只到期就把批关了,同腿后续每一只卡 `decideAsk`
55
+ * 恒 `batch_closed`(卡看得见、答不了),兄弟还被连坐成 VOID。⇒ 没有 park 路由时,批退化成 ask 自己。
56
+ * 选用处 = 协调器的 `batchIdFor`(唯一消费者);前缀与腿批不同(`ask-batch` 一元组 vs `batch` 三元组),两族 id 不相撞。
57
+ */
58
+ export declare function deriveAskScopedBatchId(askId: string): string;
49
59
  //# sourceMappingURL=approval-ask-machine.d.ts.map
@@ -33,4 +33,7 @@ export function deriveAskId(sourceTaskId, runId, toolCallId, legKey, parentToolC
33
33
  export function deriveBatchId(sourceTaskId, runId, legKey) {
34
34
  return deterministicId("batch", [sourceTaskId, runId, legKey]);
35
35
  }
36
+ export function deriveAskScopedBatchId(askId) {
37
+ return deterministicId("ask-batch", [askId]);
38
+ }
36
39
  //# sourceMappingURL=approval-ask-machine.js.map
@@ -21,7 +21,7 @@
21
21
  * 没有方法、没有捕获的行为。
22
22
  */
23
23
  import { z } from "zod";
24
- import { READ_ROOT_CANDIDATE_DIR_MAX, type AskEvidenceAbsence, type AskRequest as CoreAskRequest, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
24
+ import { READ_ROOT_CANDIDATE_DIR_MAX, type AskEvidenceAbsence, type AskRequest as CoreAskRequest, type PendingAction as CorePendingAction, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
25
25
  import type { AskRow } from "./plugins/approval-ask-store-sql.js";
26
26
  /** 模型自由文本(`sourceAgentName` / `delegation.agentName`)的限长(设计稿 §6.2)。设计 §3.1 把
27
27
  * 「限长 + 脱敏」写成 **server 新增责任**(引擎无此层):spawning model 挑的名字是自由文本,
@@ -351,12 +351,85 @@ export declare const RuleOffersAbsenceSchema: z.ZodEnum<{
351
351
  lane_cannot_speak: "lane_cannot_speak";
352
352
  shadowed: "shadowed";
353
353
  }>;
354
+ /**
355
+ * `ruleOffersAbsence` 座的**三态读数**(S-663 codex r1 起;此前是 {@link readRuleOffersAbsence} 函数体内的两步)。
356
+ * 一只 schema、一处解析,两个读者各自处置「**在场却读不出**」(集外词 / 非串值 = 世代差或第三方 producer):
357
+ * · 展示面 {@link readRuleOffersAbsence}(活卡帧 / `card_json`)—— 按缺席丢并计 F 类(卡上少一行解释 ≪ 卡上出现生词);
358
+ * · 耐久 `/decide` 腿的 remember 判据(`tool-approval.ts` 的 `parkRowStandingAnswerFacts` 的 `mandated` 一格,读 park 行上的
359
+ * 孪生座 `PendingAction.tool_approval.ruleOffersAbsence`,core 同一 factory 铸)—— 判不出是不是强制 ⇒ **按强制**(fail-closed)。
360
+ * 同一个词在两处方向相反是刻意的:展示面的最坏后果是少一行字,权限面的最坏后果是一张会话级免询问。
361
+ * 本函数**不计数**(计不计、计哪个 tag 是读者的处置,不是读数)。
362
+ */
363
+ export type RuleOffersAbsenceSeat = {
364
+ readonly state: "absent";
365
+ } | {
366
+ readonly state: "word";
367
+ readonly word: RuleOffersAbsence;
368
+ } | {
369
+ readonly state: "unreadable";
370
+ readonly raw: unknown;
371
+ };
372
+ export declare function readRuleOffersAbsenceSeat(x: unknown): RuleOffersAbsenceSeat;
354
373
  /**
355
374
  * `AskRequest.ruleOffersAbsence`(core [ref] 修②)的**边界窄读** —— 活卡帧与 `card_json` 的唯一铸造点
356
375
  * (与 {@link readProbeCause} 同款分工),两面结构性同值。闭集词 verbatim(非内容族,零 redact);
357
376
  * 形不合/缺席 ⇒ 不铸键(缺席不是断言 —— 它同时覆盖「有 offers」与结构性无车道的门,core 契约逐字)。
377
+ * 解析在 {@link readRuleOffersAbsenceSeat}(与耐久腿判据共用一只 schema)。S-700 起第三个读者:运维队列行经
378
+ * {@link parkRowFacts} 读寄存行上的 park 孪生座(`PendingAction.tool_approval.ruleOffersAbsence`,core 同一个 factory 铸的同一
379
+ * 闭集)—— 同一只 schema、同一个集外 tag,三面结构性同值;集外 detail 经 {@link outOfSetDetail}(只读值,从不调用值)。
358
380
  */
359
381
  export declare function readRuleOffersAbsence(req: unknown): RuleOffersAbsence | undefined;
382
+ /**
383
+ * park 行 `requiresRealApproval` 座的读数(NP-1 起;此前是 {@link parkRowFacts} 函数体内的一行)—— **`=== true` 严判**,
384
+ * `false` 与其它值读作缺席(core d.ts「Absent otherwise (never `false`)」;与 core `summarizeCheckpoint` 同一条判法)。
385
+ * `kind` 由调用方先判(本函数只读这一位)。两个读者共用这一只,解析单源、处置各归读者(与 {@link readRuleOffersAbsenceSeat}
386
+ * 同一条分工):展示投影 {@link parkRowFacts}(队列行顶层展开)与耐久 `/decide` 腿的 remember 判据(`tool-approval.ts`
387
+ * 的 `parkRowStandingAnswerFacts`)。这一位在两处方向相同(在场才算、读不出 = 缺席)⇒ 不需要三态;也因此权限面读它
388
+ * **不经** `parkRowFacts` —— 那只展示投影顺手读 `ruleOffersAbsence` 并对集外词计展示面的 F 类 tag,而权限面对同一格是
389
+ * fail-closed 的另一种处置(S-663 codex r1 钉「权限面不计展示面 tag」)。
390
+ */
391
+ export declare function readRequiresRealApprovalSeat(toolApproval: object): boolean;
392
+ /** core 寄存行的 `tool_approval` 臂(park 行事实全部住在这一臂上;其余臂没有工具字段)。 */
393
+ type ToolApprovalPark = Extract<CorePendingAction, {
394
+ kind: "tool_approval";
395
+ }>;
396
+ /**
397
+ * 🔴 S-700 P1(client-core CC-180 / cli L-652 的根子;板 [ref] 订正 → [ref] 立案)—— **park 行事实**:
398
+ * 运维队列行(`GET /v1/approvals` + `/stream`)顶层展开的那几位。型**从 core 寄存行派生,不手抄**(S-249 同一条
399
+ * 纪律):core 改名 / 退役任一位 ⇒ 这里 tsc 红;这一位与 {@link RuleOffersAbsence}(从 `AskRequest` 派生)两个
400
+ * 联合漂开 ⇒ {@link parkRowFacts} 的返回处 tsc 红(core 说两座是同一个 factory 铸的同一闭集,本仓照此钉)。
401
+ *
402
+ * 两只店的 `PendingCheckpoint` **直接继承本型**(`extends`),于是 core 再给这一臂加一位同类事实时,改的只有
403
+ * 下面这张 Pick 表 + {@link parkRowFacts} 函数体一行,两店与路由零改。
404
+ * `mandated`(core #1079,7.31.0):7.30.1 的 `PendingAction` 上**没有**这一位 ⇒ 本版不铸;到货后表里加
405
+ * `"mandated"`、函数体按 `requiresRealApproval` 同式加一行。
406
+ */
407
+ export type ParkRowFacts = Pick<ToolApprovalPark, "requiresRealApproval" | "ruleOffersAbsence">;
408
+ /**
409
+ * S-700 —— park 行事实的**唯一投影**(`pendingAction` 是寄存行上 core 铸的那一只,读自持久层 ⇒ `unknown`)。
410
+ *
411
+ * 病:core 在寄存行 `PendingAction.tool_approval` 上铸的「卡读者该看的事实」(core [ref] 原话:「the SAME fact,
412
+ * restated where a card reader looks for it」)中 `requiresRealApproval` 经 core `summarizeCheckpoint` 早就上了 inbox(`ruleOffersAbsence`
413
+ * 不在 core 的 `CheckpointSummary` 上),而运维队列的两只店
414
+ * `listPending` 是逐键手工组行、一位都没取 ⇒ 同一张卡「inbox 上看得到只有真人能清、队列上看不到」,壳的 park
415
+ * 卡因此恒不知道这件事(client-core 0.82.4 `26fca98b` 的透传在真部署上恒不触发)。
416
+ *
417
+ * 判据(每一条都是 core d.ts 的消费纪律,不是本仓自定):
418
+ * · **kind 先判**(「Every consumer that reads the tool fields MUST branch on `kind` first」)—— 非对象 / 缺席 /
419
+ * 非 `tool_approval` 臂 ⇒ `{}`,那些臂上出现的同名键不是这两位;
420
+ * · `requiresRealApproval` —— 经 {@link readRequiresRealApprovalSeat}(`=== true` 严判,真才带;`false` 与其它值读作
421
+ * 缺席,「Absent otherwise (never `false`)」)。与 core `summarizeCheckpoint` 逐字同一条判法 ⇒ inbox 行与队列行对同一张卡
422
+ * 同答(`test/park-row-facts.test.ts` 钉)。不计数:inbox 那一面对同一形同样静默缺席,两面同律;
423
+ * · `ruleOffersAbsence` —— 经 {@link readRuleOffersAbsence}(活卡帧 / `card_json` 同一只 schema、同一个集外 tag):
424
+ * park 孪生座与同步座是 core 同一个 factory 铸的同一闭集,不另起第二份词表;
425
+ * · 缺席 ≠ false:两位都只在「真」时在场,消费端读**在场**。
426
+ *
427
+ * 🔴 **这是展示/分诊投影,不是权限判据**(core 对两位的定性都是 display metadata:「the resume belts keep reading
428
+ * the gate's own bit」)。本投影对「在场却读不出」的方向是**缺席**(卡上少一行 ≪ 卡上出现生词);一条要从
429
+ * `ruleOffersAbsence` 推「强制」的**权限**判据需要的是三态(读不出 ⇒ 按强制,fail-closed),方向相反,所以权限面
430
+ * 不许拿本函数的产物去推强制 —— 两者共用的是 schema,不是折叠。
431
+ */
432
+ export declare function parkRowFacts(pendingAction: unknown): ParkRowFacts;
360
433
  /** S-114(core 7.4.0 [ref])—— 回落卡的计数与窗。`limit` 是**闭二词**(core d.ts 逐字),两个计数是
361
434
  * 非负整数,`autoDenyAfterMs` 是 core 契约里的 `0..2147483647` 非负整数(`0` = 本卡不武装窗:部署把
362
435
  * 旋钮关了,或 TOTAL 档的卡按 [ref]① 恒 0 等人)。四成员**全必填**——core 的 `DenialLimitFallback` 上
@@ -236,15 +236,48 @@ export function readRuleEvidence(req) {
236
236
  const RULE_OFFERS_ABSENCE_VALUES = ["mandated", "lane_cannot_speak", "shadowed"];
237
237
  export const RuleOffersAbsenceSchema = z.enum(RULE_OFFERS_ABSENCE_VALUES);
238
238
  const RuleOffersAbsenceEnvelopeSchema = z.object({ ruleOffersAbsence: RuleOffersAbsenceSchema.optional() });
239
- export function readRuleOffersAbsence(req) {
240
- const parsed = RuleOffersAbsenceEnvelopeSchema.safeParse(req);
239
+ export function readRuleOffersAbsenceSeat(x) {
240
+ const parsed = RuleOffersAbsenceEnvelopeSchema.safeParse(x);
241
241
  if (parsed.success)
242
- return parsed.data.ruleOffersAbsence;
243
- const raw = req !== null && typeof req === "object" ? req.ruleOffersAbsence : undefined;
244
- if (raw === undefined)
245
- return undefined;
246
- recordFailOpen("server.approval-card.closed-word-out-of-set", `ruleOffersAbsence=${redactSecrets(String(raw)).slice(0, 40)}`);
247
- return undefined;
242
+ return parsed.data.ruleOffersAbsence === undefined ? { state: "absent" } : { state: "word", word: parsed.data.ruleOffersAbsence };
243
+ const raw = x !== null && typeof x === "object" ? x.ruleOffersAbsence : undefined;
244
+ return raw === undefined ? { state: "absent" } : { state: "unreadable", raw };
245
+ }
246
+ export function readRuleOffersAbsence(req) {
247
+ const seat = readRuleOffersAbsenceSeat(req);
248
+ switch (seat.state) {
249
+ case "word":
250
+ return seat.word;
251
+ case "absent":
252
+ return undefined;
253
+ case "unreadable":
254
+ recordFailOpen("server.approval-card.closed-word-out-of-set", `ruleOffersAbsence=${outOfSetDetail(seat.raw)}`);
255
+ return undefined;
256
+ }
257
+ }
258
+ function outOfSetDetail(raw) {
259
+ if (typeof raw === "string")
260
+ return redactSecrets(raw).slice(0, 40);
261
+ if (raw === null)
262
+ return "<null>";
263
+ if (Array.isArray(raw))
264
+ return "<array>";
265
+ if (typeof raw === "object" || typeof raw === "function")
266
+ return `<${typeof raw}>`;
267
+ return String(raw).slice(0, 40);
268
+ }
269
+ export function readRequiresRealApprovalSeat(toolApproval) {
270
+ const v = "requiresRealApproval" in toolApproval ? toolApproval.requiresRealApproval : undefined;
271
+ return v === true;
272
+ }
273
+ export function parkRowFacts(pendingAction) {
274
+ if (typeof pendingAction !== "object" || pendingAction === null || !("kind" in pendingAction) || pendingAction.kind !== "tool_approval")
275
+ return {};
276
+ const ruleOffersAbsence = readRuleOffersAbsence(pendingAction);
277
+ return {
278
+ ...(readRequiresRealApprovalSeat(pendingAction) ? { requiresRealApproval: true } : {}),
279
+ ...(ruleOffersAbsence !== undefined ? { ruleOffersAbsence } : {}),
280
+ };
248
281
  }
249
282
  const DenialLimitFallbackSchema = z
250
283
  .object({
@@ -7,7 +7,26 @@ import { type ToolPolicy } from "@sema-agent/core";
7
7
  import { type PostureSource } from "./posture-source.js";
8
8
  import { type ExecutionLane } from "./execution-lane-caps.js";
9
9
  /**
10
- * 运维**是否表达了门意图** —— boot 的单用户 allow-all 基线只在"零门意图"时才允许铺开,
10
+ * 门意图判据读的五个量(`ServiceConfig` 结构满足)。单独成型是为了让**热路径**能拿「候选视图」来判
11
+ * (S-668 波 2:`{ ...config, approvalRequire: 候选名单 }`)—— 判据只有一份,输入可以是活配置也可以是候选。
12
+ */
13
+ export interface GateIntentView {
14
+ readonly approvalRequire: readonly string[];
15
+ readonly approvalDeny: readonly string[];
16
+ readonly approvalNeverAuto: readonly string[];
17
+ readonly durableApproval: boolean;
18
+ readonly durableApprovalSource: PostureSource;
19
+ }
20
+ /**
21
+ * 三张审批名单(require / deny / never-auto)里**有没有一张在场** —— 「这台部署有一份要执行的审批策略」。
22
+ *
23
+ * 🔴 S-668 波 2(codex r1 F1):leader 车道的 durable 审批席曾只按 `approvalRequire` 非空给出。require 名单转热之后,
24
+ * 中心热撤空它就把 leader worker 上**独立**的 deny / never-auto 两张表一起撤掉(主车道照旧 deny / ask)—— 三张表是
25
+ * 三条独立的限制,任何一张在场都要装策略。{@link hasOperatorGateIntent} 的名单三格就是本函数(同一判据,不写两份)。
26
+ */
27
+ export declare function hasApprovalListIntent(config: Pick<GateIntentView, "approvalRequire" | "approvalDeny" | "approvalNeverAuto">): boolean;
28
+ /**
29
+ * 运维**是否表达了门意图** —— 单用户 allow-all 基线只在"零门意图"时才允许铺开,
11
30
  * 所以这个谓词漏一格 = 那一格的意图被 allow-all 静默吞掉。
12
31
  *
13
32
  * 🔴 由来(2026-07-31 缝合审):这判据本来是 `main.ts` 里的一行内联表达式,只枚举了
@@ -20,26 +39,31 @@ import { type ExecutionLane } from "./execution-lane-caps.js";
20
39
  * 判定挪到**与 inv#2 同一个文件**就是为了这个:两者再想漂开,得有人同时改这两段。
21
40
  * 新增任何"门意图"配置项时,这里必须同步加一格(下面的表驱动测试会点名漏的那格)。
22
41
  */
23
- export declare function hasOperatorGateIntent(config: {
24
- approvalRequire: readonly string[];
25
- approvalDeny: readonly string[];
26
- approvalNeverAuto: readonly string[];
27
- durableApproval: boolean;
28
- durableApprovalSource: PostureSource;
42
+ export declare function hasOperatorGateIntent(config: GateIntentView): boolean;
43
+ /**
44
+ * 单用户 turnkey 的 **auto-accept 基线**适用吗 —— 单用户(未开 `REQUIRE_PRINCIPAL`)∧ 运维零门意图。
45
+ *
46
+ * 🔴 S-668 波 2 ③:**每请求现算**,不是 boot 常量。判据的输入 `approvalRequire` 自本批起在进程内会被中心
47
+ * 热换;boot 期算好的一个布尔会让「名单从空变非空」之后 allow-all 基线仍挂在每个新任务上 —— 运维刚表达的
48
+ * 门意图被基线静默吞掉([ref] 方向)。所以消费点(`boot/resolve-spec.ts` 每个任务、stage-07 的 boot 姿态行、
49
+ * run-local 的补偿 warn)一律调本函数,谁也不持有它的快照。
50
+ */
51
+ export declare function singleUserAutoAcceptBaselineApplies(config: GateIntentView & {
52
+ readonly requirePrincipal: boolean;
29
53
  }): boolean;
30
54
  /**
31
- * 门意图不可服务=boot 拒启(全窗复审 D3-F1,HIGH;与 env 墓碑族同谱系)。
32
- * v4.3.0 的轮询门形(APPROVAL_REQUIRE + DB backend + DURABLE_APPROVAL 未设)在 5.0.0 只剩 core 每任务
33
- * warn——**原先拦、现在放**(方向反转)。「留 undefined 让 core warn」对新配置是曝露误配,对升级存量是
34
- * 门静默消失,后者必须响亮。放这文件与 hasOperatorGateIntent/inv#2 同源(意图判据再漂要同时改两段)。
55
+ * 门意图**不可服务**的判据 —— 返回拒因(可服务 ⇒ `undefined`)。**全仓唯一一份**,两处调用、处置不同:
56
+ * · **boot**(`boot/stage-07-capability-layer.ts`):对装配完的整份 config 判,拒因 ⇒ **拒启**(全窗复审 D3-F1,
57
+ * HIGH;与 env 墓碑族同谱系)。v4.3.0 的轮询门形(APPROVAL_REQUIRE + DB backend + DURABLE_APPROVAL 未设)在
58
+ * 5.0.0 只剩 core 每任务 warn——**原先拦、现在放**(方向反转)。「留 undefined 让 core warn」对新配置是
59
+ * 曝露误配,对升级存量是门静默消失,后者必须响亮。
60
+ * · **热路径**(S-668 波 2 ④,`config-center/apply-effective.ts` 的审批组 stage):对**候选视图**
61
+ * (活配置 + 候选名单)判,拒因 ⇒ 审批组**整组拒**、活配置零变化、世代账停旧代。进程在跑,拒启不是选项;
62
+ * 而放行一份「没有 checkpoint 店可停车」的名单 = 单用户 auto-accept 基线仍在、名单静默不生效。
63
+ * `checkpointStorePresent` 是**部署事实**(本进程建没建出 checkpoint 店),不在 `ServiceConfig` 里 ⇒ 调用方显式递。
64
+ * 放这文件与 hasOperatorGateIntent/inv#2 同源(意图判据再漂要同时改两段)。
35
65
  */
36
- export declare function assertGateIntentServiceable(config: {
37
- approvalRequire: readonly string[];
38
- approvalDeny: readonly string[];
39
- approvalNeverAuto: readonly string[];
40
- durableApproval: boolean;
41
- durableApprovalSource: PostureSource;
42
- }, checkpointStorePresent: boolean): void;
66
+ export declare function gateIntentUnserviceableReason(config: GateIntentView, checkpointStorePresent: boolean): string | undefined;
43
67
  /**
44
68
  * S-602 C3 —— 「**无应答面**」两条 boot warn 的判据与文案(纯函数;stage-07 只负责打日志)。
45
69
  *
@@ -85,8 +109,8 @@ export declare function buildNoResponderBootWarns(input: {
85
109
  * 为什么在 **boot** 判而不是 resolve 时判:两个入参都是**部署级**的(entitlement 源在不在、durable 门开
86
110
  * 不开),per-principal 的那一半改变不了结论;放 resolve 里就得自己造去重,而 boot 天然只跑一次。
87
111
  *
88
- * 纯函数(返回一行或 undefined,不自己写日志)= 红先测得动;调用点在 main.ts 的
89
- * {@link assertGateIntentServiceable} 之后(同一段门装配)。
112
+ * 纯函数(返回一行或 undefined,不自己写日志)= 红先测得动;调用点在 stage-07 的
113
+ * {@link gateIntentUnserviceableReason} 拒启判之后(同一段门装配)。
90
114
  */
91
115
  export declare function buildForceDurableGateInertNotice(input: {
92
116
  /** center per-principal entitlement 源在不在(`centerEntitlementSourceWired` 的结果 —— 判据单源在那里)。 */
package/dist/approval.js CHANGED
@@ -3,19 +3,27 @@ import { operatorAsk } from "./operator-ask.js";
3
3
  import { ASK_USER_QUESTION_TOOL_NAME } from "./approval-content-kind.js";
4
4
  import { isExplicitPosture } from "./posture-source.js";
5
5
  import { handsOnWorkerFilePlane } from "./execution-lane-caps.js";
6
- export function hasOperatorGateIntent(config) {
6
+ export function hasApprovalListIntent(config) {
7
7
  return (config.approvalRequire.length > 0 ||
8
8
  config.approvalDeny.length > 0 ||
9
- config.approvalNeverAuto.length > 0 ||
9
+ config.approvalNeverAuto.length > 0);
10
+ }
11
+ export function hasOperatorGateIntent(config) {
12
+ return (hasApprovalListIntent(config) ||
10
13
  (config.durableApproval && isExplicitPosture(config.durableApprovalSource)));
11
14
  }
12
- export function assertGateIntentServiceable(config, checkpointStorePresent) {
15
+ export function singleUserAutoAcceptBaselineApplies(config) {
16
+ return config.requirePrincipal !== true && !hasOperatorGateIntent(config);
17
+ }
18
+ export function gateIntentUnserviceableReason(config, checkpointStorePresent) {
13
19
  if (!hasOperatorGateIntent(config) || checkpointStorePresent)
14
- return;
20
+ return undefined;
15
21
  if (!config.durableApproval) {
16
- throw new Error("APPROVAL_REQUIRE/APPROVAL_DENY/APPROVAL_NEVER_AUTO are set but the durable gate is off — since 5.0.0 the poll lane is retired and durable is the ONLY gate. Set DURABLE_APPROVAL=true (with a DB/file backend that has a checkpoint face), or remove the APPROVAL_* lists to run ungated on purpose.");
22
+ return ("an approval list is in force (APPROVAL_REQUIRE / APPROVAL_DENY / APPROVAL_NEVER_AUTO, or the center's governance.approvalRequire) but the durable gate is off — " +
23
+ "since 5.0.0 the poll lane is retired and durable is the ONLY gate, so the list would never be enforced. Set DURABLE_APPROVAL=true (with a DB/file backend that has a checkpoint face), " +
24
+ "or remove the approval lists to run ungated on purpose.");
17
25
  }
18
- throw new Error("DURABLE_APPROVAL=true but no checkpoint store is available — the configured DB backend has no checkpoint face (or no backend is configured). Wire a MySQL-protocol/PostgreSQL/file backend, or unset DURABLE_APPROVAL.");
26
+ return "DURABLE_APPROVAL=true but no checkpoint store is available — the configured DB backend has no checkpoint face (or no backend is configured). Wire a MySQL-protocol/PostgreSQL/file backend, or unset DURABLE_APPROVAL.";
19
27
  }
20
28
  export function buildNoResponderBootWarns(input) {
21
29
  if (input.approvalFaceWired)