@sema-agent/server 7.69.0 → 7.70.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 (51) hide show
  1. package/README.md +3 -0
  2. package/README.zh-CN.md +3 -0
  3. package/USAGE.md +103 -7
  4. package/dist/approval-ask-machine.d.ts +1 -1
  5. package/dist/approval-card.d.ts +3 -2
  6. package/dist/approval-reconciler.d.ts +0 -54
  7. package/dist/approval-reconciler.js +2 -2
  8. package/dist/boot/parked-revive-gate.js +10 -9
  9. package/dist/boot/runner-deps.d.ts +35 -4
  10. package/dist/boot/runner-deps.js +2 -6
  11. package/dist/budget.js +3 -2
  12. package/dist/config-catalog.d.ts +31 -2
  13. package/dist/config-catalog.js +41 -23
  14. package/dist/http/routes/approvals-assistant.js +3 -12
  15. package/dist/http/routes/diagnostics.js +4 -0
  16. package/dist/http/routes/runs.js +2 -3
  17. package/dist/http/routes/tasks.js +22 -18
  18. package/dist/http/server.js +4 -2
  19. package/dist/main.js +2 -2
  20. package/dist/observability/metrics.d.ts +0 -6
  21. package/dist/observability/metrics.js +3 -1
  22. package/dist/observability/permission-rule-events.d.ts +20 -0
  23. package/dist/observability/permission-rule-events.js +28 -0
  24. package/dist/observability/run-terminal-log.d.ts +9 -19
  25. package/dist/observability/run-terminal-log.js +183 -32
  26. package/dist/observability/tool-trace.d.ts +12 -0
  27. package/dist/observability/tool-trace.js +5 -1
  28. package/dist/plugins/store-backend.d.ts +2 -1
  29. package/dist/plugins/workflow-journal-store-sql.d.ts +1 -1
  30. package/dist/plugins/workflow-journal-store-sql.js +3 -10
  31. package/dist/posture-source.d.ts +23 -3
  32. package/dist/posture-source.js +1 -1
  33. package/dist/read-face-posture.d.ts +8 -1
  34. package/dist/read-face-posture.js +1 -0
  35. package/dist/run-cancel-context.d.ts +50 -1
  36. package/dist/run-cancel-context.js +22 -0
  37. package/dist/run-local.d.ts +14 -6
  38. package/dist/run-local.js +1 -1
  39. package/dist/runs.d.ts +0 -51
  40. package/dist/runs.js +40 -25
  41. package/dist/tool-approval.d.ts +14 -8
  42. package/dist/tool-approval.js +3 -3
  43. package/dist/trace/core-keyset-guard.d.ts +7 -3
  44. package/dist/trace/ledger-events.d.ts +17 -1
  45. package/dist/trace/ledger-events.js +6 -1
  46. package/dist/trace/ledger-sink.d.ts +10 -8
  47. package/dist/trace/ledger-sink.js +2 -1
  48. package/dist/trace/project.d.ts +3 -2
  49. package/dist/trace/redact.d.ts +28 -0
  50. package/dist/trace/redact.js +50 -27
  51. package/package.json +3 -3
package/README.md CHANGED
@@ -191,6 +191,9 @@ stop different things at different moments (per-task ceiling / per-principal ADM
191
191
  not interrupt a run already executing / deployment usage window that does stop one at a turn
192
192
  boundary) — the side-by-side table is in `USAGE.md`, worth reading before picking one.
193
193
 
194
+ Web search (deployment `WEB_SEARCH_*` env, per-request `settings.webSearch`, the two-lane precedence
195
+ and the tool-level error forms) is documented in `USAGE.md` §9.5.
196
+
194
197
  ## HTTP API overview
195
198
 
196
199
  One row per endpoint family (not exhaustive):
package/README.zh-CN.md CHANGED
@@ -170,6 +170,9 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
170
170
  (单任务硬闸 / per-principal **进场门**——不打断已在跑的 run / 部署级治理窗——会在 turn 边界停住它);
171
171
  选之前请先读 `USAGE.md` 里的「三道 $ 闸的分工」对照表。
172
172
 
173
+ Web search(部署 env `WEB_SEARCH_*` 七键、per-request `settings.webSearch`、两车道优先级、工具层错误形)
174
+ 见 `USAGE.md` §9.5。
175
+
173
176
  ## HTTP API 概览
174
177
 
175
178
  一行一个端点族(非全量):
package/USAGE.md CHANGED
@@ -75,14 +75,34 @@ ANTHROPIC_MAX_RETRIES=10 # 云 Anthropic 腿
75
75
  - **重试上限**(2026-07-31):两个键**不设就不传给引擎** —— 引擎默认当家。以前这里是 server 侧硬编码 `2`(openai 腿甚至零配置出口),而**显式传参压过引擎默认**,于是引擎抬默认对 server 部署毫无效果;第三方限流 provider 下"重试两次就放弃"正是由此而来。现在:不设=继承引擎默认(抬默认那天自动跟上),设了=按设的走。钉在 `test/brain.test.ts` 的「网关腿重试次数」两条上。
76
76
  - 断路器状态:配了 `SESSION_BACKEND=mysql` 时自动用**跨副本共享态**(SQL `circuit_breaker` 表,写穿+刷新最终一致),否则进程内 Map。启动日志 `breakerState` 字段回显 `shared(mysql)`/`in-process`/`off`。
77
77
 
78
- **可选 — WebSearch 后端(core 只留注入口,不自带任何 provider)**
78
+ **可选 — WebSearch 后端(core 只留注入口,不自带任何 provider;部署 env 七键)**
79
79
  ```bash
80
- WEB_SEARCH_PROVIDER=brave|tavily|searxng # 缺席 = 不装配(WebSearch 工具根本不挂,不是挂了报错)
81
- WEB_SEARCH_API_KEY=… # brave/tavily 必填;只进 backend 闭包,永不进模型 prompt 或工具参数
82
- WEB_SEARCH_ENDPOINT=https://searx.example # searxng 必填;brave/tavily 下是可选的 base-URL 覆盖(代理/测试)
83
- WEB_SEARCH_MAX_RESULTS=10 # 1..20,返给模型的条数上限
84
- WEB_SEARCH_TIMEOUT_MS=10000 # 单次搜索墙钟
80
+ WEB_SEARCH_PROVIDER=brave|tavily|searxng # 唯一的"装配开关":合法词才挂 WebSearch 工具;缺席/非法词 = 不装配(不是挂了报错)
81
+ WEB_SEARCH_API_KEY=… # brave/tavily 必需(searxng 不读这一键);只进 backend 闭包,永不进模型 prompt 或工具参数
82
+ WEB_SEARCH_ENDPOINT=https://searx.example # searxng 必需(实例地址);brave/tavily 下是可选的 base-URL 覆盖(代理/测试)
83
+ WEB_SEARCH_MAX_RESULTS=10 # 1..20(夹逼);缺席/非正数/非数字 → 用 backend 默认 10;小数向下取整
84
+ WEB_SEARCH_TIMEOUT_MS=10000 # 单次搜索墙钟(ms),下限 1000;缺席/非正数/非数字 → 用 backend 默认 10000
85
+ WEB_SEARCH_SEARXNG_PARAMS="engines=bing,duckduckgo;language=zh-CN" # 仅 searxng 腿消费;`;` 分隔 k=v 对;一个都解析不出 → 键整个不铸(不产出空对象)
86
+ WEB_SEARCH_PROBE_ON_BOOT=true # boot 期一次性真出网探活,默认 OFF;只认 "true"/"1"(trim+小写后比较),含糊值当没开
85
87
  ```
88
+
89
+ 七键逐一(`src/plugins/web-search.ts` `webSearchConfigFromEnv` §294-309;类型/默认/坏值登记见
90
+ `src/config-catalog.ts:654-662`):
91
+
92
+ | env 键 | 类型 | 默认 | 缺席行为 | 坏值行为 |
93
+ |---|---|---|---|---|
94
+ | `WEB_SEARCH_PROVIDER` | 闭集(`brave`\|`tavily`\|`searxng`) | 无 | WebSearch 工具整体不装配(功能缺席,无 warn) | 三词之外的任何值 = 同缺席处理,不装配、不 warn(`:296-297`) |
95
+ | `WEB_SEARCH_API_KEY` | secret string | 无 | brave/tavily:后端仍会装配(装配只看 `PROVIDER`),**首次真实工具调用**时抛 `WEB_SEARCH_API_KEY is required for the <provider> provider`(`:152`/`:164`);searxng:本键无消费点 | 空字符串同缺席(`env.WEB_SEARCH_API_KEY ?` 只认真值,`:303`) |
96
+ | `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`(`:193`) | 不做 URL 形校验——写不成 URL 由 `new URL()` 抛出,同样落到"首次调用才现形"那条路径,不拒启 |
97
+ | `WEB_SEARCH_MAX_RESULTS` | number | `10` | 用默认 10 | 非数字/≤0 → 回落默认 10;是数字则向下取整;最终值再夹在 `[1,20]`(设 999 也被 clamp 到 20,`:227`) |
98
+ | `WEB_SEARCH_TIMEOUT_MS` | number(ms) | `10000` | 用默认 10000 | 非数字/≤0 → 回落默认;最终值下限夹到 1000ms(`Math.max(1000,…)`,`:228`) |
99
+ | `WEB_SEARCH_SEARXNG_PARAMS` | string(`k=v;k=v`) | 无(不铸键 = 不传 `extraParams`,交给 core adapter 自己的缺省) | 键整个不铸 | 一个 `k=v` 对都解析不出(没有 `=`,或 `k`/`v` 任一为空)→ 同缺席,整串忽略,不拒启不 warn(`:260-277`);env 侧值恒为字符串,不会触发下面 per-request 那个"数组被误当对象吸收"的边角(见 §9.5) |
100
+ | `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`) |
101
+
102
+ **结论:七键无一在 boot 期拒启。** 与本仓其它安全轴旋钮(`MODEL_DEGRADE_ON`、思考档三键等,拼错即拒启)
103
+ 姿势不同——WebSearch 挂不挂是**功能面**取舍,不是安全边界,七键统一走"回落/降级",不是"fail-closed 拒启"。
104
+ 真正的配置错误(缺 key / endpoint 错)只在**首次真实工具调用**时才现形,以 tool_result 错误文本形式回给
105
+ 模型,不是 HTTP 层错误、不影响 boot、也不使任务整体 `failed`(细节见 §9.5)。
86
106
  - 结果是**不可信输入**:core 会 `delimitUntrusted` 围栏并**重新施加** `allowed_domains`/`blocked_domains`
87
107
  地板,所以 backend 遵不遵守 `opts` 是优化不是正确性要求 —— 换 provider(包括换成自建 SearXNG)
88
108
  不会削弱域名地板。
@@ -91,6 +111,7 @@ WEB_SEARCH_TIMEOUT_MS=10000 # 单次搜索墙钟
91
111
  笔记本上起的 localhost SearXNG,云端 worker 够不着 —— 所以「本地 SearXNG 兜底」这一档**只适用
92
112
  壳自 spawn 本地引擎的同机形**;壳连接远程引擎时该档整级跳过,由运维在**引擎侧**配 `WEB_SEARCH_*`。
93
113
  用户手填一个集中式 SearXNG 地址是合法形,照常放行。
114
+ - **per-request 覆盖 + 两车道优先级 + 错误形全表**:见 §9.5。
94
115
 
95
116
  - **大工具结果落盘**(core 1.47/1.49):单条工具结果 > ~20000 字符时 core 把全文移出上下文、只留预览+ref,模型用 `read_tool_result` 按需分页回取。配了 TiDB 时自动用**durable `tool_result` 表**(跨副本 wake 仍能取回全文;否则 core 进程内默认 = 跨副本 wake 取不到→降级到预览,不崩)。`TOOL_RESULT_TTL_SEC`(默认 86400)按 TTL 回收(要 ≥ run 可恢复期)。启动日志 `toolResultStore` 回显 `shared(tidb)`/`in-process`。
96
117
 
@@ -1392,5 +1413,80 @@ GET /v1/capabilities?permissionMode=default
1392
1413
  **settings 层 kill-switch(件⑥)**:请求体 `settings.permissions.disableAutoMode` 受理 **`"disable"` / `true` / `false`** 三形(两套已发布契约的拼写:CC 250 的严极词 `"disable"`,与 `@sema-agent/sdk` `SettingsPermissions.disableAutoMode?: boolean`);受理集之外的值 400 `request.field_invalid`。
1393
1414
  `"disable"` 与 `true` **同义**:把该 run 的生效模式由 auto 折成 default(座不写 ⇒ 不武装;tighten-only,只能关不能开);`false` = 显式「不禁」= **合法且无效果**(本键没有放宽臂——它不是 auto 的开关)。`disableBypassPermissionsMode` 本批不落点。
1394
1415
 
1395
- **per-run 面**:本批不在 run 记录上加 per-run `autoMode` 座——引擎侧的武装结果 / 未武装原因(含只有引擎知道的熔断类)
1416
+ **per-run 面**:本批不在 run 记录上加 per-run `autoMode` 座——引擎侧的武装结果 / 未武装原因(含只有引擎知道的那些;core 7.12.0 起熔断族已退役)
1396
1417
  将随 core 的状态座到货,届时 server 只投影不自算;此前 `armed` 是 server 侧三项可知判据的裁决。
1418
+
1419
+ ### 9.5 Web search:两车道优先级、per-request `settings.webSearch`、错误形
1420
+
1421
+ **两车道,整段整取(不是逐键 merge)**:部署 env(`WEB_SEARCH_*`,§0)与 per-request `body.settings.webSearch`
1422
+ 是唯二两条配置来源。当 per-request 那条腿"生效"(见下面的受理门槛)时,它产出一个**全新、完全独立**的
1423
+ `WebSearchBackendConfig`,**整只替换**掉 env 配出来的 backend —— 不是把 per-request 给的字段一个个覆盖到
1424
+ env 的 config 上,env 的其余字段(尤其是 `WEB_SEARCH_TIMEOUT_MS`)**不会**被继承到 per-request 那次调用里
1425
+ (`src/capabilities/scenarios.ts:264-266`):
1426
+
1427
+ ```
1428
+ const reqWebSearch = deps.requirePrincipal !== true
1429
+ ? webSearchConfigFromSettings(req.settings?.webSearch)
1430
+ : undefined;
1431
+ const webSearch = reqWebSearch ? createWebSearchBackend(reqWebSearch) : deps.webSearch;
1432
+ ```
1433
+
1434
+ `reqWebSearch` 非 `undefined`(即 `settings.webSearch.provider` 是合法词**且**本部署是单用户车道)时,
1435
+ `deps.webSearch`(env 配的 backend)整个不参与这次请求 —— 谁赢是**二选一**,不是字段级合并。
1436
+
1437
+ **受理门槛(单用户车道)**:🔒 per-request `settings.webSearch` 只在 `REQUIRE_PRINCIPAL !== true`(单用户 /
1438
+ TOC 本地形)时被读取;`REQUIRE_PRINCIPAL=true`(多租户)上这个键**结构性够不着**——不是被拒、也不留任何
1439
+ `warn`/`capabilities` 位说"你发的 webSearch 被忽略了"(对比 `mcpServers` 有 `capabilities.mcpInjection` +
1440
+ `mcp_injection_dropped` 日志可读;`settings.webSearch` 没有对应的能力探测位,`src/http/wire-types.ts:320-323`)。
1441
+ 原因是安全边界(与多租户能力配置隔离规则同源):per-request `endpoint`/`apiKey` 是能力配置,多租户下一个租户把 `searxng`
1442
+ 指向内网地址就是 SSRF,所以多租户上只认部署 env 配的 backend。
1443
+
1444
+ **`settings.webSearch` 键表**(`src/plugins/web-search.ts` `webSearchConfigFromSettings` §318-335;类型契约见
1445
+ `@sema-agent/sdk` 9.0.0 `settings.d.ts` `SettingsWebSearch`):
1446
+
1447
+ | 键 | 类型 | 必填 | 默认 | 消费点 |
1448
+ |---|---|---|---|---|
1449
+ | `provider` | `"brave"|"tavily"|"searxng"` | 是(缺席/非三词之一 ⇒ 整个 `settings.webSearch` 被丢,回落到 env 的 backend,**不报错**) | 无 | `:321-322` |
1450
+ | `apiKey` | string(明文) | brave/tavily 建议带(缺了首次调用才报错,见下表);searxng 不读 | 无(不继承 env 的 `WEB_SEARCH_API_KEY`) | `:326` |
1451
+ | `endpoint` | string | searxng 必需(缺了首次调用才报错);brave/tavily 可选覆盖 | 无(不继承 env 的 `WEB_SEARCH_ENDPOINT`) | `:327` |
1452
+ | `searxngParams` | `Record<string,string>`(对象)或 `"k=v;k=v"`(字符串) | 否 | 无 | `:330-333`,解析逻辑与 env 腿共用 `parseSearxngParams`(`:260-277`)。⚠️ **未列入已发布的 `@sema-agent/sdk` 8.8.0 `SettingsWebSearch` 类型**(该接口只有 `provider`/`apiKey`/`endpoint`/`maxResults` 四键、无开放下标)——server 侧代码认这个键,但当前发布的 TS 类型接不到它;手写 JSON 请求体仍可以发,server 会照常解析(源码头注自述:字段和消费点先落地,配置录入面尚未跟上,是一笔尚未还清的既有债务) |
1453
+ | `maxResults` | number | 否 | 10(与 env 同一 clamp,`[1,20]`,小数向下取整) | `:323` |
1454
+ | *(无)* `timeoutMs` | — | — | — | **per-request 车道没有这个字段**——即便部署用 `WEB_SEARCH_TIMEOUT_MS` 配了非默认超时,per-request 生效那次调用永远退回 backend 自己的默认(10000ms,下限 1000ms),因为"整段整取"意味着 env 的 `timeoutMs` 根本不在 `reqWebSearch` 那个新对象里 |
1455
+ | *(无)* `fetchImpl` | — | — | — | 同上,仅测试注入用,per-request 车道不可达 |
1456
+
1457
+ **门槛以外的键**:`settings.webSearch` 本身是 `TASK_SETTINGS_KEYS` 闭集里的受理顶层键
1458
+ (`src/task-settings.ts:309`,集外顶层键 400 `request.body_shape`),但 `webSearch` **自己的子键没有闭集门**
1459
+ ——不像 `settings.permissions.*` 有 `TASK_SETTINGS_PERMISSION_KEYS` 逐键拒(`src/task-settings.ts:367-403`
1460
+ 的 `taskSettingsKeyIssue` 只扫 `settings.*` 顶层和 `settings.permissions.*` 两层)。`settings.webSearch` 下
1461
+ 塞一个上表之外的键(拼错的 `mxResults` 之类)**不会** 400,会被 `webSearchConfigFromSettings` 静默无视
1462
+ ——门槛之外没有"未知键"这一说。
1463
+
1464
+ **错误形**(逐条对应任务书里的四问;全部经**首次真实工具调用**才现形,均为 WebSearch 工具的
1465
+ `tool_result` 错误文本,回到模型的对话里,**不是** HTTP 层 `errorCode`,**不会**使 `TaskResult` 整体
1466
+ `failed`,也不拒启/不拒收请求本身;core `dist/tools/web.js:840-919` `createWebSearchTool` 统一兜底):
1467
+
1468
+ | 触发条件 | 现象 | 是否 retryable(core `classifySearchFailure`) |
1469
+ |---|---|---|
1470
+ | 坏 `provider`(`settings.webSearch.provider` 非三词之一,或缺席) | **不是错误** —— `webSearchConfigFromSettings` 返回 `undefined`,整段回落到 env 配的 backend(env 也没配 ⇒ WebSearch 工具不装配,模型看不到这个工具);无 warn、无日志 | 不适用 |
1471
+ | 缺 `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` 落最后一条默认分支) |
1472
+ | `searxngParams` 非对象/非字符串(如数字、布尔、`null`) | 静默丢弃——`parseSearxngParams` 落到"非 object 且非 string"分支,返回 `undefined`,键整个不铸,**不报错、不 warn** | 不适用 |
1473
+ | ⚠️ `searxngParams` 是**数组**(如 `["engines=bing"]`) | `typeof [] === "object"` 让它落进对象分支:`Object.entries` 按数组下标产出 `{"0":"engines=bing"}`——**不被拒绝**,但产出的键是数字字符串、值是未拆分的原始 `"k=v"` 串,传给 core adapter 的 `extraParams` 后是一组没有意义的查询参数(不是解析出 `engines=bing`)。这条边角只在 per-request 车道可达(env 值恒为字符串,不会触发);已用与源码逐字一致的独立复现脚本核验(见收车档),未改代码 | 不适用(不是异常路径) |
1474
+ | 搜索后端返回**非 2xx HTTP 响应**(鉴权失败/限流/服务端 5xx 等) | 三个 provider 各自的 `!res.ok` 分支抛错,消息形固定为 `"<provider> search failed (<status>): <body首 200 字符>"`(brave/tavily,`:158`/`:174`)或 `"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`——都取决于上游返回的具体措辞,不是本仓能保证的 |
1475
+ | `endpoint` **网络层**不可达(连接被拒/DNS 解析失败/fetch 自身抛错,尚未拿到任何 HTTP 响应) | `fetch` 抛出的传输层错误(`ECONNREFUSED`/`ENOTFOUND`/`fetch failed` 等 Node/undici 标准措辞)被同一 `catch` 接住 | `true`——这条路径的错误文本天然含 `econnrefused`/`enotfound`/`fetch failed`/`dns` 等词,`classifySearchFailure` 的网络故障正则能命中(与上一行"已拿到 HTTP 响应但非 2xx"是两条不同的失败路径,别混淆) |
1476
+
1477
+ ⚠️ **"坏 provider 静默回落"这条对调用方是否可观察,取决于部署 env 有没有配 backend**:上表第一行说
1478
+ "env 也没配 ⇒ 工具不装配、模型看不到这个工具"——那只是**部署 env 同样缺席**这一种情形。若部署 env
1479
+ **已经**配了合法 backend(例如 `WEB_SEARCH_PROVIDER=brave`),调用方 per-request 传一个拼错的 `provider`
1480
+ (如 `"searx"`)、或带着一个本想打到自建 SearXNG 的 `endpoint`,`settings.webSearch` 整段被丢弃、静默回落
1481
+ 到 env 的 brave backend——**WebSearch 工具照常挂载、照常可用**,查询实际发给了 env 配的 provider,不是
1482
+ 调用方以为自己指定的那个;工具存在这一事实本身**不能**证明 per-request 的 `provider`/`endpoint` 真的
1483
+ 生效了,两种情形(per-request 生效 / per-request 被静默丢弃回落 env)在壳侧不可判别(此条经独立复现验证,
1484
+ 见收车档)。
1485
+
1486
+ **`apiKey` 明文与 env 槽边界**:server 收到的 `settings.webSearch.apiKey` 是**明文字符串**,没有任何服务端
1487
+ 密钥槽位/引用间接——收到什么字符串就直接进 backend 闭包(`:326`,与 `WEB_SEARCH_API_KEY` 同一条消费路径,
1488
+ `src/plugins/web-search.ts:63-64`)。**server 本批不改受理面**:明文字段的形状维持原样。调用方(壳)如何在
1489
+ 自己机器上管理这份明文是调用方的事——例如 cli 壳侧的约定是在**调用方自己的环境**里按
1490
+ `SEMA_WEBSEARCH_KEY_<PROVIDER>` 这样的命名空间存放每个 provider 的 key,由壳在本地读出后把明文塞进请求体;
1491
+ 这纯粹是**客户端约定**,本仓不读取、不校验、也不感知任何这类客户端侧 env 命名(这条命名事实来自任务书,
1492
+ 本车未读 cli 源码核实,列入收车档「未闭环」)。
@@ -34,7 +34,7 @@ export declare function canBatchTransition(from: BatchState, to: BatchState): bo
34
34
  * ── `legKey`(取代数值 `leg`,一腿一凭据)─────────────────────────────────────────────────────────────
35
35
  * = `sha256(resume checkpoint token)` 的 hex,**首腿 = 空串**。一次 park→resume 的 token 就是这条腿的
36
36
  * 天然身份:同 token 重投 = 同一腿(幂等,正确);新 park ⇒ 新 token ⇒ 新腿 ⇒ 新 askId。
37
- * 取摘要而非原始 token 是因为 token 是能力凭据(本仓有 `stripCheckpointToken` 专门把它从可重放账本里
37
+ * 取摘要而非原始 token 是因为 token 是能力凭据(本仓有 `stripResumeToken` 专门把它从可重放账本里
38
38
  * 剥掉),而 `askId` 是 wire 可见值 ⇒ **只存/只喂摘要**。用它当轴顺带消掉了原裁需要的 `task_run` 加列
39
39
  * 与 `markResuming` 返回加宽(后者有第二个调用点 `http/routes/runs.ts:463`,加宽会误杀已成功 resume 的
40
40
  * running run)。残留边界:同一 run 内**不经 checkpoint token** 的重入形若未来出现会得到同一 legKey ——
@@ -328,8 +328,9 @@ declare const DenialLimitFallbackSchema: z.ZodObject<{
328
328
  autoDenyAfterMs: z.ZodNumber;
329
329
  }, z.core.$strict>;
330
330
  export type DenialLimitFallback = z.infer<typeof DenialLimitFallbackSchema>;
331
- /** S-185(core 7.10.0 [ref])—— **分类器为什么答不了**。`cause` 是 core 的闭三词
332
- * (`AUTO_MODE_UNAVAILABLE_CAUSES` = error | timeout | breaker_open),本仓**刻意不枚举**它:词表的单一
331
+ /** S-185(core 7.10.0 [ref])—— **分类器为什么答不了**。`cause` 是 core 的闭集词
332
+ * (`AUTO_MODE_UNAVAILABLE_CAUSES`;core 7.12.0 起 = error | timeout,`breaker_open` 随熔断族退役),
333
+ * 本仓**刻意不枚举**它:词表的单一
333
334
  * 属主在 core,抄一份的形会在 core 加词那天把新词静默吞成缺席 —— 而丢的正是「这次不可用是新出现的那
334
335
  * 一类」这条信息(与 `AskRequest.origin` 顶注同一条纪律)。判据只到「非空串」这一层形门。
335
336
  * `.strict()` + 单成员必填:core 将来给这只对象加成员时本读面当场判假、**整键不铸**,新成员必须由人
@@ -1,57 +1,3 @@
1
- /**
2
- * 流内审批协议([ref] §3.0)的**对账收敛器** —— [ref] 车5。
3
- *
4
- * 职责一句话:把 `PARKING`(窗到期中选、正在转投递面)这个**唯一的非终态中间态**收敛成
5
- * `PARKED | DENIED | VOID`,并在崩溃后补位那些没人打 expire 的孤儿 `STREAM_PENDING` 行。
6
- *
7
- * ── 判据表 v2(设计稿 §9 尾的五臂汇总,逐字落地;每臂注读口)────────────────────────────────────────
8
- * ① **身份三元组 ∧ hash 双等** ⇒ `bindBatch`(判别式返回;`ok:false` ⇒ 降级续判)
9
- * 身份 = `sourceTaskId` 相等 ∧ `toolCallId` 相等 ∧ **因果下界**(不是等式)`cp.createdAtMs ≥
10
- * ask.createdAtMs`;再 ∧ `cp.boundInputHash === ask.boundInputHash`。三维里只有前两维是等式,把时间
11
- * 那一维读成等式会让合法 park 几乎命不中。逐字实现在 {@link classifyGateMatch};判据的**唯一
12
- * 属主**是那个函数的头注,这里只列纲要,细则(祖先层 fold 否决、多候选取舍)不在此复述。
13
- * 🔴 两处易错,写在这里免得下一个人照旧口径改码:
14
- * · 承重的第一维是 **`sourceTaskId`**([ref] 件1,黑板 [ref]③①)——`sessionId` **不进身份等式**
15
- * (委派子代的 ask 落行记的是投递上下文的根会话,park 却发生在子代自己的 sessionId 上,只按
16
- * session 等值会张冠李戴),它在这一层只是读口 `findCheckpointCandidatesForAsk` 的入参。
17
- * ⚠️ 但别据此把它当无用键:{@link isAncestorFoldMint} 的祖先层否决判的正是
18
- * `sourceTaskId !== sessionId` —— 那是内存里的承重用法,只是不属于身份等式;
19
- * · hash **任一侧缺席一律不 bind**(硬相等不放宽,理由同下)。缺席的**归因**分两级,别写成一句:
20
- * 身份先判 —— 同身份候选一条都没有且候选集非空 ⇒ `identity_miss`(有对家但不是这一只,或读不出);
21
- * 只有在身份这一层没被判掉时,hash 缺席才落 `single_mint` 三形
22
- * (`ask_only` / `checkpoint_only` / `neither`)。`single_mint` 是可观测分类、不是放宽的命中;
23
- * 把「只有一侧铸过」这格结构事实混进 `identity_miss` 的噪声底,运维就读不出两者的区别。
24
- * 两道等式都不许放宽的原因不变(§9 C2:同 session 内 `toolCallId` 会被网关重用,身份不严会把旧 ask
25
- * PARK 到别人的 resume 坐标上,而 `PARKED` 是不可回滚的终态)。读口 = `findCheckpointCandidatesForAsk`
26
- * (§9 C4 窄谓词精确查,无分页假阴性);`unparseable` 候选**视同不匹配**(单行读不出不许打断整段
27
- * 扫描,§8 C-6)。
28
- * ② run 终局分臂(读口 `runStore.getRun`):`status ∈ {completed, failed, blocked}`(§8 A-1 词表修正 ——
29
- * `cancelled` 不是 run 状态,取消 = `failed` + `errorCode`)——
30
- * - `failed ∧ errorCode === "cancelled"`,或批行已 `ABORTED` ⇒ `VOID`(取消不是路由失败,§9 C3);
31
- * - 其余终局 ⇒ `DENIED(routing_failure_fail_closed)`。**全仓唯一的 DENIED 写点**(grep 钉)。
32
- * ②′ **bind-once 落选者**(codex round2 R2-3 增补,排在 ② 之前判):批已 `ROUTING_BOUND` 且中选者是
33
- * 兄弟 ⇒ `VOID(batch_bound_elsewhere)` —— 迟到进 PARKING 的行(`bindBatch` 的原子事务连坐不到它)
34
- * 结构上再也赢不了 bind,当下即可判;不判会让它滞留到 run 终局再被 ② 误报成路由失败。
35
- * ③ else(run 还在跑 / suspended / needs_review / 无行且不满足 ④)⇒ **保持 PARKING** + `deferReconcile`
36
- * touch(推 `updated_at_ms` 排到队尾,配合 `listByState` 的 `ORDER BY updated_at_ms ASC` 解队头堵塞,
37
- * §8 D-4 / §9 C5)。**超时永不产生终态 denial**(约束②)。
38
- * ④ adhoc 双谓词(`sessionId === taskId` ∧ `getRun` 无行,§8 C-2)∧ 窗过 + `ADHOC_GRACE` ⇒ `VOID`
39
- * (归因 `adhoc_leg_no_durable_domain`)。
40
- * ⑤ `createdAtMs` 量的 `ORPHAN_TTL` ⇒ `VOID(orphan_ttl_exceeded)` + warn ——「任何未在 TTL 内收敛的
41
- * PARKING」的最后兜底(§8 A-1 放宽形,不限「getRun 无行」),约束①(遗孤最终可判)。
42
- * 🔴 TTL 一律量 immutable 的 `createdAtMs`/`expiresAtMs`,**绝不量 `updatedAtMs`**(它被 ③ 的队列
43
- * 轮转每轮刷新,量它的 TTL 永不到期,§9 C5)。
44
- *
45
- * ── 三条铁则 ────────────────────────────────────────────────────────────────────────────────────
46
- * 1. **一经发布的终态不改义**:本模块只从 `PARKING`/`STREAM_PENDING` 出发,`PARKED/DECIDED/DENIED/VOID`
47
- * 的行永不再被碰(CAS 的 `WHERE state=?` 谓词是机器保证,不靠调用序自觉)。
48
- * 2. **幂等可重放**:两副本同扫无害——每条转移都是带 `from` 态的 CAS,单赢者;输者本轮什么都不做。
49
- * 3. **宁可不命中,绝不错配**:判别不出(hash 缺席 / 候选 `unparseable` / 列与 blob 矛盾)一律落 ②③⑤,
50
- * 绝不发一张别人的 resume 凭据。
51
- *
52
- * 命名(CLAUDE.md 工厂命名律):`decideReconcileAction`/`selectGateCandidate` 是纯判定函数;
53
- * `createApprovalReconciler` 返回带方法的活对象 ⇒ `create*`。
54
- */
55
1
  import type { AskRow, ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
56
2
  import type { BatchState } from "./approval-ask-machine.js";
57
3
  import type { CheckpointAskCandidate } from "./plugins/checkpoint-store-sql.js";
@@ -1,8 +1,8 @@
1
+ import { CANCELLED_CODE } from "./run-cancel-context.js";
1
2
  import { isTerminalRunStatus } from "./plugins/store-contracts.js";
2
3
  import { DENY_REASONS, VOID_REASONS } from "./approval-deny-reasons.js";
3
4
  import { buildRevokeFrame } from "./approval-card.js";
4
5
  import { encodeCheckpointScope } from "./security.js";
5
- const CANCELLED_ERROR_CODE = "cancelled";
6
6
  const RECONCILE_STORE_TIMEOUT_MS = 10_000;
7
7
  const RECONCILE_SEGMENT_BUDGET_MS = 30_000;
8
8
  function withDeadline(op, label, timeoutMs = RECONCILE_STORE_TIMEOUT_MS) {
@@ -64,7 +64,7 @@ export function decideReconcileAction(input) {
64
64
  return { kind: "void", reason: VOID_REASONS.BATCH_BOUND_ELSEWHERE };
65
65
  }
66
66
  if (run !== null && isTerminalRunStatus(run.status)) {
67
- if (run.status === "failed" && run.errorCode === CANCELLED_ERROR_CODE) {
67
+ if (run.status === "failed" && run.errorCode === CANCELLED_CODE) {
68
68
  return { kind: "void", reason: VOID_REASONS.RUN_CANCELLED };
69
69
  }
70
70
  return { kind: "deny", reason: DENY_REASONS.ROUTING_FAILURE };
@@ -83,22 +83,23 @@ function autoModePostureOf(deps, row, caps) {
83
83
  }
84
84
  function createUnreachableAncestorClassifier(deps, row) {
85
85
  const inner = createAutoModeDecider({
86
- failureThreshold: 1,
87
86
  classify: async () => {
88
87
  throw new Error("the ancestor task's auto-mode classifier is a live closure over that task's own transcript/brain — it cannot be " +
89
- "reconstructed on a fresh replica, so this inherited layer has no classifier verdict to give (the call falls " +
90
- "through to the ancestor's frozen approver chain)");
88
+ "reconstructed on a fresh replica, so this inherited layer has no classifier verdict to give");
91
89
  },
92
- onBreakerOpen: () => deps.logger.warn("parked_revive_ancestor_classifier_unreachable", {
93
- handle: row.handle,
94
- sessionId: row.sessionId,
95
- note: "an inherited ancestor layer was auto-mode armed at park time; its classifier does not survive the process boundary — inherited asks resolve on the approver chain (durable park) instead of a classifier verdict",
96
- }),
97
90
  });
91
+ let warned = false;
98
92
  return {
99
- breakerOpen: () => inner.breakerOpen(),
100
93
  decide: async (input, signal) => {
101
94
  recordFailOpen("server.parked-revive.ancestor-classifier-unreachable", row.handle);
95
+ if (!warned) {
96
+ warned = true;
97
+ deps.logger.warn("parked_revive_ancestor_classifier_unreachable", {
98
+ handle: row.handle,
99
+ sessionId: row.sessionId,
100
+ note: "an inherited ancestor layer was auto-mode armed at park time; its classifier does not survive the process boundary — since core 7.12.0 (#661) the inherited station DENIES such calls outright (it no longer falls through to the frozen approver chain), so classifier-eligible inherited asks on this redeemed leg are auto-denied, not re-parked for a person",
101
+ });
102
+ }
102
103
  return inner.decide(input, signal);
103
104
  },
104
105
  };
@@ -79,6 +79,39 @@ export interface RunnerDepsCtx {
79
79
  question: QuestionCoordinator | undefined;
80
80
  toolApproval: ToolApprovalCoordinator | undefined;
81
81
  sessionStore: ReturnType<StoreBackend["session"]>;
82
+ /**
83
+ * B-060(test [ref] 核心发现①)—— **一条部署腿上的每一只 Runner 看同一只检查点店**。
84
+ *
85
+ * 本键从「差异键」升进{@link createSharedRunnerDeps 共享基座}。修前它**只**挂在 subRunner 上,主
86
+ * runner 靠 per-task `spec.checkpointStore` 拿店 —— 而 core 的 `run_workflow` 是挂在**主** Runner
87
+ * (`prepare-caps-and-workflow.js:244` 的 `runner: runnerSelf`;core 7.12.0 亲核行号,B-060 票据引的
88
+ * :249 是 7.11.2 的坐标)上的,workflow 子代的 childSpec 不带
89
+ * `checkpointStore`(`orchestration/workflow-primitives.js:80-81` 只透传 `"disabled"` 这一种状态),
90
+ * 于是子代的 `resolveCheckpointStore(spec, deps)` 落到主 runner 这一格 = `undefined`
91
+ * ⇒ `parkLane {capable:false, reasons:["no_checkpoint_store"]}` ⇒ 受门的调用不是 park 而是
92
+ * `delegation.ask_unresolvable{parkLaneExisted:false}` 直接拒(过度拒绝,S-185 的 `/decide`
93
+ * 第三车道在这条路径上结构性不可达)。
94
+ *
95
+ * 🔴 host 腿的读数判据是**core 侧硬的**,不是本仓的但愿:`resolveCheckpointStore`
96
+ * (`checkpoint-store.js:367`)**spec 优先**——`spec.checkpointStore` 在场就用它、`"disabled"` 就是
97
+ * 关、只有缺席才落 deps。而本仓 host 腿恒经 `resolveSpec` 的 durableEnabled 分支盖上**同一个实例**,
98
+ * 且 `durableEnabled ⇔ checkpointStore !== undefined`(`boot/coordinators.ts:70` 与 main.ts 的构造
99
+ * 条件同源)⇒ 每一条经 `resolveSpec` 的腿看到的店逐字同一只。
100
+ *
101
+ * ⚠️ **如实登记一处不经解析器的直读**(codex 交叉复审提名,亲读 core dist 确认):
102
+ * `core/runner/run-terminal-adoption.js:74` 读的是 `runner.deps.checkpointStore` **本身**,不走
103
+ * `resolveCheckpointStore`。它是「宿主任务 park 时,把没排空的 steer / follow-up 迁移到 park token 上」
104
+ * 那一步 —— 修前主 runner 这一格是空的,于是**整段迁移静默不跑**(那些输入随本腿一起丢,只留
105
+ * `task.user_followup_undrained` 一类通告)。接上之后它开始真的跑。方向是修好一条本就该在的腿,
106
+ * 但它**不是**「host 腿字节不变」的一部分,所以写在这里而不是混进上面那句。
107
+ *
108
+ * 🔴 这一席是**部署腿级**的答案,不是「哪只 Runner」的属性:一条腿要么有赎回口(HTTP 面的
109
+ * `/v1/approvals/:id/decide` + resume 家族)、于是它的每只 Runner 都该有店;要么没有(`run-local`
110
+ * 的一次性 CLI)、于是**一只都不该有** —— core 的平台/资源停驻(`prepare-boundary-parks.js` 的
111
+ * `suspendForPlatformLimit`)只看「店在不在」,不看 approver 席,给一条没有赎回口的腿接店 = 用一张
112
+ * 没人能兑付的 checkpoint 换掉一次干净的失败。
113
+ */
114
+ checkpointStore: RunnerDeps["checkpointStore"];
82
115
  memoryEngine: {
83
116
  backend: MemoryBackend;
84
117
  root: string;
@@ -154,7 +187,7 @@ export interface RunnerDepsCtx {
154
187
  }
155
188
  /** [ref] A10 留档发现②:main runner `RunnerDeps` 与 main.ts subRunner 字面量之间此前手工重复
156
189
  * 的 ~15 个键,类型标注见 {@link createSharedRunnerDeps} 头注。 */
157
- export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryCapturePolicy" | "delegationEntryCaps" | "crossSessionInbound" | "crossSessionDialogExpiry" | "writeProtectedPaths" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "hands" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore" | "memoryScopeAdmission" | "deploymentMemoryScopes" | "sharedMemoryStores" | "compliancePostureResolver" | "lockedConfig" | "retentionPolicy" | "onNotice" | "mcpRevocations">;
190
+ export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryCapturePolicy" | "delegationEntryCaps" | "crossSessionInbound" | "crossSessionDialogExpiry" | "writeProtectedPaths" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "checkpointStore" | "hands" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore" | "memoryScopeAdmission" | "deploymentMemoryScopes" | "sharedMemoryStores" | "compliancePostureResolver" | "lockedConfig" | "retentionPolicy" | "onNotice" | "mcpRevocations">;
158
191
  /**
159
192
  * [ref] A10 留档发现②(review 2026-07-29,[ref]§三族A 同源修补的延续):main runner 的
160
193
  * `RunnerDeps` 字面量(下方 `createRunnerDeps`)与 `main.ts` 里 subRunner 的 `new Runner({...})`
@@ -176,14 +209,12 @@ export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "readFace" | "readDeny
176
209
  * 差异键(为何不在本共享基座、逐一注明):
177
210
  * - `sessionStore`:main runner 用宿主 durable 会话店;subRunner 用私有短 TTL 的
178
211
  * `ForkRoutingSessionStore`(main.ts 现场构造,子代转录生命周期与主会话不同)。
179
- * - `checkpointStore`:main runner 走 `spec.checkpointStore` 分支(不进 `RunnerDeps`,本 ctx 根本
180
- * 没有这个字段);subRunner 单独携带(子代执行面的 park 设施,[ref])。
181
212
  * - `onBackgroundChildEvent` / `loadProjectMemory` / `probeInstructionSources` / `onError`:
182
213
  * [ref]§三族A 已用「直引 `runnerDeps.X`」修过(main.ts 两处引用同一个 `runnerDeps` 实例的同一
183
214
  * 属性,比再抽一层共享基座更强的同源保证),不重复收纳进这里。
184
215
  */
185
216
  /** 基座真实消费的窄面(Pick)——subRunner 调用点(main.ts)只需凑这 13 个字段,不必造全量 ctx。 */
186
- export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "sessionPolicyStore" | "hands" | "usageWindowStore" | "orgMemoryAdmission" | "sharedMemoryStores" | "logger" | "governanceSeams" | "mcpRevocations">;
217
+ export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "checkpointStore" | "sessionPolicyStore" | "hands" | "usageWindowStore" | "orgMemoryAdmission" | "sharedMemoryStores" | "logger" | "governanceSeams" | "mcpRevocations">;
187
218
  /**
188
219
  * core `EngineNotice` → 结构化 `engine_notice` 行的**唯一**转发体([ref]②)。
189
220
  *
@@ -69,6 +69,7 @@ export function createSharedRunnerDeps(ctx) {
69
69
  hooks: ctx.deploymentHooks,
70
70
  hands: ctx.hands,
71
71
  toolResultStore: ctx.toolResultStore,
72
+ checkpointStore: ctx.checkpointStore ? ctx.checkpointStore : undefined,
72
73
  sessionPolicyStore: ctx.sessionPolicyStore ? ctx.sessionPolicyStore : undefined,
73
74
  memoryScopeAdmission: ctx.orgMemoryAdmission.memoryScopeAdmission ? ctx.orgMemoryAdmission.memoryScopeAdmission : undefined,
74
75
  deploymentMemoryScopes: ctx.orgMemoryAdmission.deploymentMemoryScopes && ctx.orgMemoryAdmission.deploymentMemoryScopes.length > 0 ? ctx.orgMemoryAdmission.deploymentMemoryScopes : undefined,
@@ -147,12 +148,7 @@ export function createRunnerDeps(ctx) {
147
148
  onElicit: elicitation ? elicitation.elicit : undefined,
148
149
  onQuestion: question ? question.question : undefined,
149
150
  onAsk: createRunnerDepsOnAsk(toolApproval),
150
- autoMode: {
151
- onBreakerOpen: (info) => {
152
- metrics.inc("auto_mode_breaker_open_total");
153
- logger.warn("auto_mode_breaker_open", { consecutiveFailures: info.consecutiveFailures, lastCause: info.lastCause });
154
- },
155
- },
151
+ autoMode: {},
156
152
  sessionStore,
157
153
  memoryBackend: memoryEngine ? memoryEngine.backend : undefined,
158
154
  memoryEngineDir: memoryEngine ? memoryEngine.root : undefined,
package/dist/budget.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { modelCostToPricing } from "@sema-agent/core";
2
2
  import { currentPrincipal } from "./observability/principal-context.js";
3
+ import { isPermissionRuleEventKind, permissionRuleEventWordOf } from "./observability/permission-rule-events.js";
3
4
  import { promptManifestRecordOf, configAssembledRecordOf } from "./observability/prompt-manifest.js";
4
5
  export function buildPricing(models) {
5
6
  const pricing = {};
@@ -119,8 +120,8 @@ export function createTracer(metrics, costQuota, modelUsage, fleetUsage, fleetLe
119
120
  else if (e.kind.startsWith("compaction.")) {
120
121
  metrics.inc("compaction_events_total", { outcome: e.kind.slice("compaction.".length), trigger: e.trigger ?? "none" });
121
122
  }
122
- else if (e.kind === "permission.persisted_rule_allowed" || e.kind === "permission.rule_store_unreadable" || e.kind === "permission.read_only_allowed") {
123
- metrics.inc("permission_rule_events_total", { event: e.kind.slice("permission.".length) });
123
+ else if (isPermissionRuleEventKind(e.kind)) {
124
+ metrics.inc("permission_rule_events_total", { event: permissionRuleEventWordOf(e.kind) });
124
125
  }
125
126
  };
126
127
  }
@@ -34,10 +34,30 @@ export interface ConfigCatalogSpec {
34
34
  readonly derivedDefaultNote?: string;
35
35
  readonly danger?: readonly CatalogDangerAxis[];
36
36
  readonly enumValues?: readonly string[];
37
+ /**
38
+ * S-100 —— **底层布尔 ↔ 闭集词**的行内映射,只给「实算腿回布尔、而行对外说词」的 enum 行。
39
+ *
40
+ * 为什么写在行上而不是建第二张全局极性表:`config.ts` 的极性表(`ConfigKnobRecord`)记的是
41
+ * **布尔旋钮**的来源与默认方向,它压根不带词 —— 拿它当 `true→enumValues[0]` 的位置约定用,等于
42
+ * 在两个文件之间约定一个不写下来的下标。行自己声明,是同一处声明它 `enumValues` 的地方。
43
+ *
44
+ * 某一极**没有词**(该极的真实投影是「键不在场」)⇒ 显式写 `null`,不是省略 —— 省略与「这一行忘了
45
+ * 声明映射」在形上同形,而后者必须落到响亮的 `effectiveValueInvalid` 臂。
46
+ */
47
+ readonly booleanEnum?: {
48
+ readonly true: string | null;
49
+ readonly false: string | null;
50
+ };
37
51
  /** `ServiceConfig` 的平铺键(实算兜底 + center managedNow 判据对表用;嵌套块不填,走 resolve)。 */
38
52
  readonly configKey?: string;
39
- /** 本进程实算(非 secret/opaque 行才可用;返回 undefined = 本行没有实算腿,落 env 原文/staticDefault)。 */
40
- readonly resolve?: (config: ServiceConfig) => CatalogValue | undefined;
53
+ /**
54
+ * 本进程实算(非 secret/opaque 行才可用;返回 undefined = 本行没有实算腿,落 env 原文/staticDefault)。
55
+ *
56
+ * 🔴 第二参是**部署 env**,不是第五条供值腿:有几根旋钮的属主不在 `ServiceConfig` 上(它在装配点按
57
+ * env 现算,如 WebSearch 后端),而目录的纪律是「回显属主算出来的值」—— 只能把同一条实算腿的入参放宽,
58
+ * 不能在目录里再抄一遍属主的读法(S-100 / codex r1 F3 的成案:抄一遍就是第二个写者,当场分歧)。
59
+ */
60
+ readonly resolve?: (config: ServiceConfig, env: NodeJS.ProcessEnv) => CatalogValue | undefined;
41
61
  readonly center?: CatalogCenterSeatSpec;
42
62
  /** 缺省 "deploy";retired/零消费键标 "inert"(设计稿 ⚪ 的 env 腿投影)。 */
43
63
  readonly envTiming?: "inert";
@@ -69,6 +89,15 @@ export interface ConfigCatalogRow {
69
89
  readonly staticDefault?: string;
70
90
  readonly derivedDefaultNote?: string;
71
91
  readonly effectiveValue: CatalogValue;
92
+ /**
93
+ * S-100(additive;**只在真非法时铸,恒不铸 `false`**)—— 本行的实算值**落在它自己宣告的
94
+ * `enumValues` 之外**,或它的实算腿回了一个本行没有声明映射的布尔。
95
+ *
96
+ * 在场时 `effectiveValue` 恒 `null`:回显一个集外值等于目录在教消费方写一个写回去就非法的词,
97
+ * 而**静默丢**会让「配错了」与「真的没设」在 wire 上同形([ref] 响亮臂)。
98
+ * 缺席 = 这一格没问题(不是「不知道」)。
99
+ */
100
+ readonly effectiveValueInvalid?: true;
72
101
  readonly valueClass: "plain" | "secret" | "opaque";
73
102
  readonly danger: readonly CatalogDangerAxis[];
74
103
  readonly center?: ConfigCatalogRowCenter;