@sema-agent/server 7.114.0 → 7.115.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 (68) hide show
  1. package/MIGRATION.md +73 -1
  2. package/USAGE.md +35 -2
  3. package/deploy/sema-up/cluster-up.sh +13 -1
  4. package/deploy/sema-up/kube-up.sh +13 -1
  5. package/deploy/sema-up/preflight.sh +13 -1
  6. package/deploy/sema-up/sema-up.sh +13 -1
  7. package/deploy/sema-up/smoke.sh +13 -1
  8. package/dist/adoption/wire.d.ts +0 -3
  9. package/dist/approval-card.d.ts +21 -24
  10. package/dist/approval-card.js +12 -0
  11. package/dist/approval.js +5 -2
  12. package/dist/boot/exit-boundary.d.ts +3 -2
  13. package/dist/boot/exit-boundary.js +3 -0
  14. package/dist/boot/resolve-spec.js +4 -1
  15. package/dist/boot/shutdown.d.ts +19 -0
  16. package/dist/boot/shutdown.js +12 -1
  17. package/dist/capabilities/composite-members.d.ts +1 -0
  18. package/dist/capabilities/composite-members.js +1 -0
  19. package/dist/config-catalog.js +2 -1
  20. package/dist/config-center/apply-effective.js +11 -11
  21. package/dist/config-provider.d.ts +16 -0
  22. package/dist/config-provider.js +11 -2
  23. package/dist/config-types.d.ts +20 -1
  24. package/dist/config.d.ts +41 -18
  25. package/dist/config.js +36 -13
  26. package/dist/deployment-governance.d.ts +15 -4
  27. package/dist/deployment-governance.js +20 -2
  28. package/dist/fleet/fleet-bus.d.ts +1 -22
  29. package/dist/hooks/hook-runner.d.ts +29 -3
  30. package/dist/hooks/hook-runner.js +30 -17
  31. package/dist/http/admission.js +7 -2
  32. package/dist/http/approval-sweeps.js +20 -0
  33. package/dist/http/checkpoint-ctx.d.ts +68 -0
  34. package/dist/http/checkpoint-ctx.js +36 -0
  35. package/dist/http/durable-run-leg.js +2 -1
  36. package/dist/http/resume-legs.js +9 -0
  37. package/dist/http/route-ctx.d.ts +7 -0
  38. package/dist/http/routes/a2a-serve.d.ts +1 -3
  39. package/dist/http/routes/a2a-serve.js +0 -2
  40. package/dist/http/routes/admin-memory-optout.d.ts +0 -2
  41. package/dist/http/routes/admin-memory-optout.js +0 -1
  42. package/dist/http/routes/capabilities.js +1 -0
  43. package/dist/http/routes/diagnostics.d.ts +5 -0
  44. package/dist/http/routes/diagnostics.js +2 -0
  45. package/dist/http/routes/sessions.js +1 -1
  46. package/dist/http/routes/tasks.js +3 -2
  47. package/dist/index.d.ts +1 -1
  48. package/dist/leader/wire.js +2 -1
  49. package/dist/local-data-root.d.ts +1 -1
  50. package/dist/mcp-content-origin.d.ts +0 -8
  51. package/dist/model-entry-refusal.d.ts +10 -2
  52. package/dist/model-entry-refusal.js +19 -3
  53. package/dist/observability/fail-open.d.ts +5 -8
  54. package/dist/permission-rule-list.d.ts +81 -0
  55. package/dist/permission-rule-list.js +17 -0
  56. package/dist/run-local.js +4 -1
  57. package/dist/runtime-governance.d.ts +21 -1
  58. package/dist/runtime-governance.js +6 -1
  59. package/dist/security.js +2 -2
  60. package/dist/session-policy-wire.d.ts +5 -4
  61. package/dist/task-settings.d.ts +6 -5
  62. package/dist/task-settings.js +16 -30
  63. package/dist/tool-approval.d.ts +16 -4
  64. package/dist/tool-approval.js +2 -1
  65. package/dist/trace/core-keyset-guard.d.ts +9 -4
  66. package/dist/trace/project.js +9 -0
  67. package/package.json +4 -4
  68. package/scripts/{retired-mode-seed-drain-local.mjs → drain-check-local.mjs} +147 -26
package/MIGRATION.md CHANGED
@@ -7,6 +7,76 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.115.0 —— `@sema-agent/core` 7.39.2 → 7.40.1:没人表态的服务器工具调用要问 · `settings.permissions.allow` 从名单翻成 allow 规则(表态,不是免问)· 升级时停驻、带 `allow` 的任务续跑被拒(升级前排空) · 运维的 allow 规则 `APPROVAL_GRANT`(新旋钮,四条腿同装) · 能力位 `settings.permissions.allowGrant` · hook 只给上下文不再替它放行 · 续跑入口取消一个码 · 随包排空脚本改名
11
+
12
+ - **🔴 询问类模式下,MCP 服务器工具 / A2A 技能的调用若没人表态 ⇒ 出卡(行为面硬破坏,引擎变更经本服务过境)**。① *谁受影响*:挂了 MCP / A2A 服务器(运维的 `config.d/mcp.json` / 配置中心,或请求体 `mcpServers` / `a2aPeers`),
13
+ 且任务跑 `default`(不写 `permissionMode` 即此)/ `acceptEdits` / `auto` / `plan`(`plan` 下只有**只读**的服务器工具出这张卡,非只读的仍是 plan 自己的写问)的部署 —— 7.114.x 这些调用零卡直跑(引擎的 `dontAsk` 模式对这一问不出卡、直接拒;本服务的请求面不收 `dontAsk`)。现在没有规则 / hook / 审批人 / 模式释放说「这次可以」,
14
+ 引擎出一张卡(`askClass: "unstated"`,卡句 `The agent requested permission to use <tool>, but it has not been granted yet.`);没有审批面(同步 `POST /v1/tasks` 无活审批、leader / run-local 无席)⇒ 拒。
15
+ ② *怎么改*:**allow 规则写在哪,按腿分** —— 请求腿(`POST /v1/tasks` / `/v1/runs`,有请求体)在请求体写 `settings.permissions.allow: ["mcp__<server>__*"]`(一条覆盖整台服务器;下条);
16
+ 没有请求体的腿 —— `run-local`(spec 由本服务自铸),以及 durable 续跑(含 leader worker 停驻后经 `/decide` 续跑的那一段)—— 由运维设 env `APPROVAL_GRANT=mcp__<server>__*`(逗号分隔;本版新旋钮,见下方「运维的 allow 规则」一条 —— 它在请求腿上同样生效)。
17
+ leader worker 的首跑与修复腿本键**到不了**(它们的工具策略在 leader 编排里自己折出来,不经部署治理的构造口);这两条腿今天不挂 MCP / A2A,所以没有要表态的调用。
18
+ 两处写的都是**服务器工具 / A2A 技能**的规则(`mcp__<server>__*` / `a2a__<peer>__*` / 它们的精确名):allow 规则给出「有人表态」—— 它回答的是没人表态的服务器工具 / A2A 技能调用;内置工具上引擎自己的写门、shell 检查照问,deny / ask 照赢 —— 写 `Bash` / `Write` **不会**让它们不再被问。
19
+ 另可:PreToolUse hook 显式回 `permissionDecision: "allow"` · 接审批面(流内审批 / `DURABLE_APPROVAL=true`)· `permissionMode: "bypassPermissions"`(只请求腿)。
20
+ ③ *不改会怎样*:每次服务器工具调用一张卡;无审批面 ⇒ 每次被拒。拒绝句**只在**引擎判「这条腿根本没接审批人」那一形里带出路句;接了审批协调器的 HTTP 腿
21
+ (含同步腿审批窗为 0、当场答「没人在」的形)与无人值守腿(`run-local` / leader)上被拒时,拒绝句今天**不带**出路句(引擎侧已认领补齐)—— 出路就是上面 ② 的 allow 规则 / 审批面。
22
+ - **运维的 allow 规则 `APPROVAL_GRANT`(新 env 旋钮,缺省关;零迁移,在无人值守腿上用 MCP 服务器的部署升级前设它)**。值 = 逗号分隔的工具名规则:精确名、`mcp__<server>__*`(整台服务器)、`a2a__<peer>__*`(整个 peer);命中 = 运维替这些调用表了态(回答的是没人表态的服务器工具 / A2A 技能调用),
23
+ 未命中 = 不表态。与 `settings.permissions.allow` 同一只编译口、同一只校验,HTTP 提交(`POST /v1/tasks` / `/v1/runs`)、durable 续跑(每一次从持久化的提交体重建,含 leader worker 停驻后经 `/decide` 续跑的那一段)、停驻后台 agent 的跨副本赎回、`run-local` 四条腿同装(leader worker 的首跑与修复腿到不了),**不越过**任何一层的 deny / ask(`APPROVAL_DENY` 拒、`APPROVAL_REQUIRE` 问、请求 settings 的 deny / ask、会话规则照旧压过它)。
24
+ **不是白名单**(不收窄任何东西;「只许这些」是会话规则的 `toolAllow`)。坏值(退役名、无前缀短名、`mcp__figma` 两段形、反斜杠、超长、`Bash(npm:*)` 这类内容形)⇒ 启动拒,错误句点名哪一条、怎么改。
25
+ 规则对**每个租户、每条腿**生效:`mcp__<server>__*` 也覆盖那台服务器将来新增的工具 —— 按需写窄。它给的只是「没人表态」那一句答,**不放掉任何一层自己的问**:
26
+ 内置工具的精确名(例 `Write` / `Bash`)写进来,`default` 模式下引擎自己的写门 / 形状层照问(问压过放行)。
27
+ A2A 如实:运维声明的对端(配置中心 / `config.d`,刻意不收 `toolAxes`)上,每次技能调用都是引擎自己的外发标记问(`tool_marks_a2a`),不是「没人表态」那一问 —— `a2a__<peer>__*` 不放它,无人值守腿上 A2A 技能调用照旧被拒 / 停驻 / 问人;只有请求体自带的对端(单用户闸)能在 `toolAxes` 里把某个技能的 `egress` 降为 `false`,那之后才是「没人表态」那一问,这时 allow 规则才答得上。在场与否:起服日志 `approval_grant_in_force`、`GET /v1/diagnostics/wiring` 的 `serverGates.approvalGrant`。v1 只有 env 一个写入通道。
28
+ - **🔴 `settings.permissions.allow` = allow 规则(表态),不再是名单(行为面硬破坏)**。① *谁受影响*:把 `settings.permissions.allow` 当白名单、用来**只许**某几只工具的调用方。7.114.x:名单在场 ⇒ 名单外的工具(含全部 `mcp__*` / `a2a__*`)一律拒。
29
+ 现在:一条 allow 规则命中 = 带理由的放行,即allow 规则给出「有人表态」—— 它回答的是没人表态的服务器工具 / A2A 技能调用;内置工具上引擎自己的写门、shell 检查照问,deny / ask 照赢。**未命中 = 不表态、照常判**(引擎自带工具按模式判,服务器工具出上一条的卡)。
30
+ ⚠️ 与参照实现(CC)的 `permissions.allow` **不同**:CC 里 `allow: ["Edit"]` 会跳过编辑确认;这里写 `Write` / `Bash` 只是表态,`default` 模式下引擎的写门 / shell 检查照问(要免掉这些问,用 `permissionMode`)。
31
+ 同时 allow 桶的规则形与 deny / ask 同一只判据:退役名(`Task` / `bash` …)、无协议前缀的短名、缺段或两段 MCP 名(`mcp__figma` ⇒ 写 `mcp__figma__*`)、反斜杠、超 512 字符 ⇒ 整请求 422(7.114.x 收下这些名字形);内容形 / 参数级 allow(`Bash(npm:*)`、`Read(./x)`、`WebFetch(domain:…)`)⇒ 整请求 422(7.114.x 同样 422,只是那一版的指路句是名单的读法,本版改指持久规则店)。
32
+ **反方向的一处(由拒变收)**:`Bash(*)` / `Read(*)` / `Edit(*)` 这类「工具名 + 通配内容」形,7.114.x 是 422(名单按整串比对,带括号的名字匹配不上任何工具);现在收下,编成那只工具的整工具 allow 规则(与写 `Bash` / `Read` / `Edit` 同读 —— 只表态,引擎的写门 / shell 检查照问)。
33
+ ② *怎么改*:要「只许这些工具」改用真旋钮 —— 会话规则 `PUT /v1/sessions/:id/policy` 的 `toolAllow`(不在名单 ⇒ 拒;非运维只许收紧)、每请求 `excludeTools` / `excludeAllTools`、或部署场景工具面;内容形的常设放行进持久规则店(`/v1/rules/cc-import` 导入,或在卡上答 yes 时留规则;`POST /v1/rules` 是收紧方向的单步写口,不收 allow);坏形名字按 422 文案改。
34
+ 客户端据 `GET /v1/capabilities` 的 `settings.permissions.allowGrant === true` 判断这台 server 是否按新语义收 allow(键缺席 = 旧 server,别投这只桶)。
35
+ ③ *不改会怎样*:以 allow 收窄工具面的请求,名单外的工具照常可用(收窄失效);带坏形 allow 名字的请求 422。最坏的组合:把它当白名单用、同时请求 `permissionMode: "bypassPermissions"` 的调用方 —— 升级后名单外的工具不再被拒、也不会被问(7.114.x 上只许名单内)。
36
+ - **「永远不会生效的条目」拒因句改形(码不变)**:请求体 `settings.permissions.{allow,ask,deny}` 的 422、`APPROVAL_REQUIRE` / `APPROVAL_DENY` / `APPROVAL_NEVER_AUTO` / `APPROVAL_GRANT` 的启动拒、
37
+ 配置中心 `governance.approvalRequire` 的整组拒,句首由 `… names tool(s) that can never match a live tool (these lists are RAW whole-name comparisons — …)` 改成
38
+ `… has an entry / entries that can never take effect — it / each would never match a live tool or cannot be compiled into a rule (names are read exactly as written: no alias folding, no protocol prefix added)`;
39
+ 逐条 `名 → 指引` 不变。按句子前缀匹配的消费方改按错误码分派。
40
+ - **能力位(additive)**:`GET /v1/capabilities` 新顶层键 `settings` = `{ permissions: { allowGrant: true } }`(恒真版本位)。`taskSettings.permissions` 仍是布尔 `true`,不变。零动作;想投 allow 的客户端按上条读它。
41
+ - **PreToolUse hook 结果映射(行为面)**:hook **显式**回 `permissionDecision: "allow"` ⇒ 当作 hook 的放行交给引擎;hook 只给 `additionalContext` / 只给 `updatedInput` ⇒ 不再被当成放行。
42
+ 7.114.x 上这两处映射的差别**看不出来**:那一版的引擎对 hook 的放行不读(只读 deny / ask),服务器工具不论 hook 怎么写都零卡直跑;7.115.0 起差别才可见 ——
43
+ 显式 `allow` 的 hook 让服务器工具不再出「没人表态」卡,只给上下文 / 只改写的 hook 不再替谁表态,服务器工具照问。
44
+ PreToolUse hook 的显式 `allow` 同样只是表态:它让没人表态的服务器工具调用不再出卡,引擎对内置工具(编辑、shell)自己的询问照出。这与参照实现不同(那边 hook 批准即跳过询问);引擎一侧已立项按参照实现收。
45
+ 零配置动作:想让 hook 放行的,显式回 `allow`。放行只管那条 hook 看见的参数:最终交给工具的参数若不是它看见的那一份(后面的 hook、或同一组里的另一条 hook 改写了)⇒ 这次放行不算数(要让改写后的参数也算表态,改写的那条 hook 自己回 `allow`)。部署级 hook 与任务级 hook 的复合体同一条规则。
46
+ - **`APPROVAL_REQUIRE` 名单 × 服务器工具**:人在卡上选「本会话不再询问」之后、以及 `APPROVAL_AUTO_BUDGET` 额度内的自动批,同一只服务器工具不再被引擎再问一次(这两处现在对引擎说明「是名单门放行的」)。零动作。
47
+ - **🔴 升级时仍停驻、提交时带了 `settings.permissions.allow` 的任务:续跑被拒(持久数据面硬破坏,零 DDL;升级前排空)**。① *谁受影响*:升级那一刻有停驻中的任务(等审批 / 计划审阅 / 资源停车 / 可唤醒)、
48
+ 且它提交时请求体带了非空 `settings.permissions.allow` 的部署 —— 那份 `allow` 是按名单写的,本版不按新读法(allow 规则)重放它(那会让名单外的工具在续跑时静默放开):
49
+ 这些任务的每一个续跑入口(`/decide`、`/resume`、`/plan_review`、带 / 不带消息的 `/wake`)答 `409 checkpoint.unsupported_version`(`retriable:true` = 卡仍 pending,**不是**「重试会成功」;挂着 run 的停驻可以取消,唤醒停驻没有可取消的 run),
50
+ 任务字节不动、仍停驻(工具审批 / 计划审阅 / 资源停车三类挂着 `suspended` 的 run 行、会话锁仍在;`task_done` 唤醒停驻的原任务早已终局,没有 run 行、不占会话锁,被拒后那张停驻留到过期);SLA 到期拒扫跳过它们(不逐拍重试),收割器在终局时刻收。7.115.0 起新停驻的任务不受影响,提交时没带 `allow`(或带空数组)的不受影响。
51
+ ② *怎么改*:升级**前**在仍跑 7.114.x 的部署上按 `docs/DEPLOY-PREREQS.md` 7.115.0 节的配方数这类任务(SQL 两后端一条一句;`DB_BACKEND=local` 跑**本版(7.115.0)包里**的随包脚本 `scripts/drain-check-local.mjs` ——
52
+ 仍跑 7.114.x 时已装的包里只有旧名脚本、没有本条判据,读出的「干净」是假的;不必先升级服务:`d=$(mktemp -d) && (cd "$d" && npm pack @sema-agent/server@7.115.0 && tar xzf sema-agent-server-7.115.0.tgz) && node "$d/package/scripts/drain-check-local.mjs"`,
53
+ 看 `PERMISSIONS-ALLOW-LIST rows=` 那一行),期望 0;非 0 ⇒ 让它们跑完,或 `POST /v1/runs/:id/cancel` 取消(唤醒停驻没有可取消的 run:唤醒它跑完,或升级后在那个会话上直接新提交)。已经升级才发现 ⇒ 有 run 行的三类取消后重提、唤醒停驻直接在该会话上新提交(要「只许这些工具」改用会话规则 `toolAllow` / 请求 `excludeTools`),或一次性回滚到 7.114.x 续跑完再升回来。
54
+ ③ *不改会怎样*:这些任务续不了:有 run 行的三类只能取消后重提,取消之前会话锁一直被它们占着(同一会话上新提交 409 `conflict.session_active_run`);唤醒停驻续不了,但不挡同一会话上的新提交。这条拒是一次性迁移:等所有 7.114.x 停驻的任务结束后,后续版本把它删掉。
55
+ **配方数不到的一个入口**:workflow 子代的审批被决定后,决定投到**宿主**会话上起新一跑;宿主会话体里带非空 `allow`、续跑上下文没有读法记号且尚未被回收时,这一跑同样被拒(同一句 409)。
56
+ 配方只数「本会话有待决行」的会话,宿主会话自己没有待决行,数不到它;撞得上的窗口 = 宿主会话闲置未超过 `REAP_RUN_STALE_SEC`(出厂 120 s,调大则更久)。
57
+ 出路:升级前把待决的 workflow 子代审批先决掉;升级后撞上 ⇒ 在该宿主会话上重新提交任务。
58
+ - **🔴 随包排空脚本改名(硬破坏,零别名):`scripts/retired-mode-seed-drain-local.mjs` → `scripts/drain-check-local.mjs`**。7.115.0 起这只脚本一次跑两条判据
59
+ (`permissions-allow-list` 与 `retired-mode-seed`),旧名只说了一条;新名与 `docs/DEPLOY-PREREQS.md` 里 SQL 配方的标记 `-- drain-check:<判据>:<后端>` 同一个词。
60
+ ① *谁受影响*:按旧文件名调这只脚本的运维脚本 / runbook(7.113.0 与 7.114.0 的包里它就叫旧名)。
61
+ ② *怎么改*:换文件名 —— 同一只脚本,参数(可选的数据根)、退出码(0 / 1 / 2)、两条汇总行(`PERMISSIONS-ALLOW-LIST …` 与恒在最后的 `RETIRED-MODE-SEED …`)的名字与顺序都不变。
62
+ ③ *不改会怎样*:7.115.0 的包里没有旧名的文件(也没有转发壳)⇒ `node` 报找不到文件。⚠️ 找不到文件时 `node` 的退出码是 **1**,而这只脚本自己的 rc 1 是「有命中、有停驻行」—— 只看退出码的下游会把「脚本没跑起来」读成「有停驻行」;
63
+ 调它的运维脚本请把文件名换了再核一遍读数(末行要见到 `RETIRED-MODE-SEED rows=…`,不要只看 rc)。
64
+ - **停驻后台子代的跨副本赎回不读请求体的 `settings.permissions`(如实;7.114.0 起如此,本版不变)**:跨副本赎回停驻的后台子代时,父任务请求体里的 `settings.permissions`(allow / ask / deny 三桶)不进那条腿,只有部署层在(审批基线、部署治理、`APPROVAL_GRANT`)。
65
+ - **滚动升级窗 / 回滚(如实)**:新旧副本并存时,客户端在新副本上读到 `settings.permissions.allowGrant` 后提交的 `allow`,若落到旧副本,被按**名单**读(收窄,方向安全);
66
+ 旧副本上停驻的这种任务没有读法记号,换成新副本后续跑同样被上一条拒(出路同上一条)。新副本停驻、带 `allow` 的任务若在窗内被旧副本续跑,旧副本不认记号、按名单读(同样是收窄)。
67
+ 回滚到 7.114.x:7.115.0 停驻的任务里的 `allow` 同样被按名单读;带 `unstated` 类的待决卡,7.114.x 的审批队列读不出这个类词(卡照列,只是没有 `askClass` / `classes` 两键,
68
+ `server.approval-card.closed-word-out-of-set` 计数加一),审批流内卡店里带它的行重放时被跳过、同一问幂等重入走停驻(fail-safe)。7.114.x 的引擎能不能续跑 7.40.x 写下的这张卡:未核。
69
+ - **续跑入口的取消(引擎 7.40.1)**:带消息的 `POST /v1/sessions/:id/wake` 在部署 `userPromptSubmit` 门还没答完时被 `POST /v1/runs/:id/cancel` ⇒ 合成 run 行取消终局(`errorCode: "cancelled"`),那张 park 按取消语义收掉,
70
+ 再 `wake` ⇒ 404 `not_found.parked_checkpoint`(继续会话 = 新提交一条任务)。7.114.x 同一操作报 `steering.blocked_by_hook`(409,`retriable: true`)且 park 留着 —— 那是错码的副作用。只按 `errorCode` 分派取消的消费方零动作;把这一形当「hook 拦了、稍后重试」的要改读。
71
+ - **「只续已有会话」两处拒的 `errorCode` 变了(状态码不变)**。① *谁受影响*:提交体带 `requireExistingSession:true`、按 `errorCode` 分派的客户端 —— `sessionId` 在本部署不存在的 404,`errorCode` 由 `not_found.resource` 变 `resume.session_not_found`;没给 `sessionId` 的 422,由 `request.unprocessable` 变 `request.field_conflict`。
72
+ ② *怎么改*:按新码分派(`resume.session_not_found` = 要续的会话不在;`request.field_conflict` = 请求自相矛盾,补上 `sessionId` 或去掉 `requireExistingSession`)。422 那一句的句尾括注 `(resume.session_not_found)` 同时去掉(码是 `request.field_conflict`,句子不再括注另一个码);404 那一句不变。
73
+ ③ *不改会怎样*:按旧码写的分支不再命中,这两处拒落进客户端的通用 404 / 422 处理;从 422 句子里解析 `resume.session_not_found` 的客户端解析不到。
74
+ - **`limits.brainCallGuardrailMs` 由受理(无效)变 400**。① *谁受影响*:提交体带 `limits.brainCallGuardrailMs`(毫秒数、`false` 或 `0`)的调用方 —— 7.114.x 受理(`POST /v1/tasks` 200 / `POST /v1/runs` 202)但这个键不生效,单次脑调用的外墙照旧是引擎缺省的一小时;现在 `400 request.field_invalid`,句子点名这个键。
75
+ ② *怎么改*:删掉这个键(本服务没有给调用方设这道外墙的口;要别的外墙值,先要一个部署级旋钮)。
76
+ ③ *不改会怎样*:带这个键的提交整条被拒,任务不跑。
77
+ - **用户可见(引擎 7.40.0,零配置动作)**:`settings.permissions.ask` 里的 `Read(<path>)` 规则现在也管 Grep / Glob 在该路径下的搜索(各出一张 Read 卡;无审批面 ⇒ 拒),`Edit(<path>)` ask 规则也管 Write / NotebookEdit;
78
+ Read 工具结果不再带常驻的内容安全提醒(依赖那句约束弱模型的部署,把同一句放进自己的 `appendSystemPrompt`)。
79
+
10
80
  ## 7.114.0(车UW,S-995;车UY)—— `settings.hooks` 工具事件 matcher:别名写成正则形的同一个错误,提交当场 400(7.112.0 只拒字面形) · `@sema-agent/settings-schema` 7.0.1 + `@sema-agent/core` 7.39.2:车道 / 存储词表改读正本 · 写了 `remote-docker` 的 `config.d/remote-exec.json` 拒启 · local 车道坏会话规则文件改拒 · 被拒的 `resumeAt` 不再挪会话叶
11
81
 
12
82
  - **🔴 settings 面硬破坏(响亮拒,无迁移代码、无兼容形)**。① *谁受影响*:提交体 `settings.hooks` 里 PreToolUse / PostToolUse / PostToolUseFailure 的 `matcher` **选中引擎工具别名、却选不中它的正名**的 —— 7.112.0 只拒字面的 `ListPeers` / `Read|ListPeers`,
@@ -16,7 +86,7 @@
16
86
  **不在本规则内(仍受理)**:大小写错的别名(`"listpeers"` —— CC 同形:区分大小写、静默不跑)与「选不到目录里任何名」另一类(含目录之外的 MCP 工具名,受理面无从区分),后者候设计(见 CHANGELOG `## Unreleased` 7.114.0 线 · 车 UW)。
17
87
  **正则只是顺带选中别名也会被拒**:规则问的是「这只 matcher 选不选别名 / 选不选正名」,不问写的人本意 —— 一只写来匹配目录外 MCP 工具、只是顺带也选中 `ListPeers` 的正则(如 `.*Peers`、`mcp__.*|.*Peers`)同样 400,
18
88
  而错误句说的是「改成 `ListAgents`」。出路:在正则里把别名排除掉(如 `(?!ListPeers$).*Peers`),或改成不会选中它的写法。
19
- - **🔴 运行期 matcher 求值与 CC 2.1.280 同形(行为面;不是配置面硬破坏 —— 是钩子开始触发)**。① *谁受影响*:`settings.hooks` 里 matcher 带**两端空白、分隔符两侧空白或用逗号分隔**的(`" Bash"` / `"Bash "` / `"Read | Bash"` / `"Read,Bash"`)—— CC 容忍,sema 此前把它们当「选不中」(钩子永不触发、无任何声音)。② *怎么改*:无需改 —— 它们现在按作者本意命中(PreToolUse 的 deny 钩子也真拦);**区分大小写、不剥词内空白**(`bash` / `"Ba sh"` 照旧不命中,CC 同形)。含 `.` / `*` / 括号等正则字符的 matcher **一字不动**(整体当锚定正则)。③ *不改会怎样*:升级后,**以前因空白永不触发的钩子会触发** —— 若你依赖「带空白的钩子是死的」(不是合法用法),删掉那条。耐久 resume 的存量 hooks 里带空白的 matcher 在续跑腿同样开始命中。**读法**:只含 `[A-Za-z0-9_-]`、`|`、`,`、空格的 matcher 按 `[|,]` 拆、逐段 trim、大小写敏感、词内空白不剥,对所有带 matcher 的事件(含 StopFailure / PreCompact / PostCompact 与 `if` 条件名部)同一只读法。**已知与 CC 的差异(未做)**:CC 的空白 / 逗号放行按事件分档(sema 不分);CC 正则不锚定。依据:对 CC 2.1.280 的黑盒实测(空白 / 大小写);逗号分隔按参照实现的读法对齐,没有黑盒实测。
89
+ - **🔴 运行期 matcher 求值与 CC 2.1.280 同形(行为面;不是配置面硬破坏 —— 是钩子开始触发)**。① *谁受影响*:`settings.hooks` 里 matcher 带**两端空白、分隔符两侧空白或用逗号分隔**的(`" Bash"` / `"Bash "` / `"Read | Bash"` / `"Read,Bash"`)—— CC 容忍,sema 此前把它们当「选不中」(钩子永不触发、无任何声音)。② *怎么改*:无需改 —— 它们现在按作者本意命中(PreToolUse 的 deny 钩子也真拦);**区分大小写、不剥词内空白**(`bash` / `"Ba sh"` 照旧不命中,CC 同形)。含 `.` / `*` / 括号等正则字符的 matcher **一字不动**(整体当锚定正则)。③ *不改会怎样*:升级后,**以前因空白永不触发的钩子会触发** —— 若你依赖「带空白的钩子是死的」(不是合法用法),删掉那条。耐久 resume 的存量 hooks 里带空白的 matcher 在续跑腿同样开始命中。**读法**:只含 `[A-Za-z0-9_-]`、`|`、`,`、空格的 matcher 按 `[|,]` 拆、逐段 trim、大小写敏感、词内空白不剥,对所有带 matcher 的事件(含 StopFailure / PreCompact / PostCompact 与 `if` 条件名部)同一只读法。**已知与 CC 的差异(未做)**:CC 的空白 / 逗号放行按事件分档(sema 不分);CC 正则不锚定。依据:对 CC 2.1.280 的黑盒实测(空白 / 大小写 / 逗号分隔;逗号的三种写法在参照实现上各 3/3 命中)。
20
90
  - **多租户车道(`requirePrincipal: true`):字面判保留,另有一处放宽见下条**:该车道 `settings.hooks` 整只被忽略(`task_hooks_ignored`),受理面走**字面档 = 7.112.0 的字面判保留**(裸 `ListPeers` / `Read|ListPeers` 照旧 400;正则写法照旧 200),且**零调用方正则执行**;字面档与运行期共用同一只字面 / 名字表读法,所以**带空白或逗号分隔的别名**(`" ListPeers"` / `"Read | ListPeers"` / `"Read,ListPeers"`)在该车道也同拒 400(只多拒、不放宽)。上面的正则求值只在单用户车道。
21
91
  - **受理面判序与资源闸(行为面,零新码;两条车道同一条顺序)**:matcher 超 8 192 字符 ⇒ 两条车道都不读、「未判」、照旧受理;字面 / 名字表读法(只含名字字符、`|`、`,`、空格)两条车道都先走、不占求值预算;
22
92
  只剩正则形时,单用户车道在一次受理共享的 100 ms 墙钟预算里求值(灾难回溯的正则用尽预算 ⇒ 那一只与其后各只正则形「未判」、照旧受理),多租户车道对正则形不判。
@@ -54,6 +124,8 @@
54
124
  - **`DB_BACKEND=local`(File 店)车道的排空**:没有 SQL 可跑 —— 用随包的只读脚本 `scripts/retired-mode-seed-drain-local.mjs`(7.113.0 起在 npm 包里)。
55
125
  **升级前你装的还是 ≤7.112.0,脚本不在已装的包里,但不必先升级服务**:`d=$(mktemp -d) && (cd "$d" && npm pack @sema-agent/server@7.113.0 && tar xzf sema-agent-server-7.113.0.tgz) && node "$d/package/scripts/retired-mode-seed-drain-local.mjs"`
56
126
  (已升级的,直接 `node <安装目录>/node_modules/@sema-agent/server/scripts/retired-mode-seed-drain-local.mjs`)。
127
+ **改名**:7.115.0 起这只脚本叫 `scripts/drain-check-local.mjs`(同一只、多一条判据,见本文件 7.115.0 节);上面的旧文件名是 7.113.0 / 7.114.0 包里的名字(取 7.113.0 包那条命令里的旧名不用改),
128
+ 已升到 7.115.0 及以后的安装目录里请按新名调。
57
129
  **读哪个数据根**:不带参时 = 服务自己的那条解析 —— `LOCAL_DATA_ROOT` → `CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `~/.ai-agent`(脚本取的是服务 `loadConfig` 调的同一只函数,不是它自抄的一份),
58
130
  所以请在**与服务同一份 env** 下跑;也可以把数据根当唯一参数递进去。末行 `RETIRED-MODE-SEED rows=… root=<实际读的数据根>` 回显读了哪个目录。
59
131
  只读、不取锁;它把 `checkpoints/checkpoints.snapshot.jsonl`(压实后的行)与 `checkpoints/checkpoints.jsonl`(之后的事件)折成当前状态再判 pending 行(不要用 grep 代替:压实后老行只在快照里)。
package/USAGE.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # 调用方手册(OA / 智能体 / 你自己)
2
2
 
3
3
  > 一句话心智模型:**你只对一个地址发普通 HTTP 请求——LB `http://<host>:8090`;请求体里只写"要做什么"。**
4
- > 工具面与安全策略由服务端 `resolveSpec` 注入,请求体**替换不了**它们(`tools` 不是受理键,传了 400;按请求只能**减**:`excludeTools` / `settings.permissions`)。
4
+ > 工具面与安全策略由服务端 `resolveSpec` 注入,请求体**替换不了**它们(`tools` 不是受理键,传了 400;按请求只能**减**:`excludeTools` / `settings.permissions` 的 deny · ask;7.115.0 起 `settings.permissions.allow` 是 allow 规则:allow 规则给出「有人表态」—— 它回答的是没人表态的服务器工具 / A2A 技能调用;内置工具上引擎自己的写门、shell 检查照问,deny / ask 照赢;能力位 `settings.permissions.allowGrant`)。
5
5
  > `model` 与 `systemPrompt` / `appendSystemPrompt` 是受理键,但 `model` 只能点服务端已配置目录里的名字 / 档位词 / 模型 id(未知 ⇒ 400)。凭据永远不进模型 / 请求体。
6
6
  > 你不"调度 worker",你只发任务;LB + TiDB 自己协调。每个字段都来自当前真实代码。
7
7
 
@@ -113,6 +113,7 @@ ANTHROPIC_API_KEY=sk-ant-… # 可选:ANTHROPIC_BASE_URL / ANTHROPIC_VERSION /
113
113
  `@名` / `settings.model` / `compactionModel` / side-query)⇒ 400 `request.model_unavailable`。**7.100.0 及以前**这种条目会落到网关
114
114
  brain、带着**它自己的** `apiKeyEnv` 钥匙打到网关。不合 settings-schema 形的条目同律(码 `config.model_entry_invalid`,如非 http(s)
115
115
  的 `baseUrl` —— 此前被「退回部署网关」;读腿逐条丢掉的条目同样留名字,点它照样 400);目录里**每条**都不可用 ⇒ 那一代模型面这一组被拒。
116
+ 🔴 **日志与拒因句里的条目名先过名筛**(7.115.0 起):被拒条目的名字写进启动 / 刷新日志(`sema_registry_model_no_brain` / `_entry_invalid` 的 `model`、`sema_registry_models` 行的 `unavailable`、缺省 / 档位 / 角色绑定三条 warn 里指向被拒条目的名字)与「缺省模型缺席」「被拒名被增广铸出」两句拒因之前,先问 settings-schema 的 `isReportableRefusalName`(`[A-Za-z0-9._:/@+-]` 闭形、无密钥形、无带 userinfo 的 URL 形)。**不可披露的名只写码**:warn 不带 `model`、改带 `modelNameWithheld: true`(`unavailable` 只列可披露的名并带 `unavailableNamesWithheld` 个数);拒因句里该位写 `(name withheld)`。引擎自己的判决不受影响(显式点它照样拿拒因码);**要在日志里认出是哪一条**,把目录条目名改成名筛的闭形字符集(或按 `code` / `provider` / `fields` 在目录里找)。显式选中不可用条目的 400 回显不在此列(它回显的是调用方自己点的那个名)。
116
117
  🔴 **被拒条目的名字不许被档位 / 别名铸到别的条目上**(S-712):不可用条目恰好叫 `sonnet` / `opus` / `pro` / `best` 这类档位词或 CC 别名、
117
118
  而档位表会铸出这个词时,这份候选的模型面整组被拒(码 `config.model_refused_name_minted`,warn `sema_registry_model_plane_refused` 点名
118
119
  「被拒名 → 目标条目」)—— 否则显式点它、或绑它的角色,会静默跑在另一条条目的端点与账号上;出路:改名 / 修好条目,或改绑那条档位绑定。
@@ -454,6 +455,7 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
454
455
  `multi-tenant-shared-lane` / `device-durable-store` / `device-durable-approval`)自 7.104.0 起判在这一步,**不在** `loadConfig()` 里 ——
455
456
  只调 `loadConfig()` 的装配对这几形照起。`ctx.configSource === "local"` 时它先读 `config.d/remote-exec.json` 覆写车道(`drivesFromFile`
456
457
  = 你的装配驱动得了的文件车道),再判;不走文件覆写的装配直接调同一份判据 `assertExecutionLaneInvariants(config)`。
458
+ `ctx` 的型 `ExecutionLaneComposeCtx` 也从包根导出(自己的装配里要写它的签名 / 抽 helper 时直接 import,不必手抄一份形)。
457
459
  - **库形嵌入的 caps `store` 位**(7.106.0 起,S-866 ①;7.106.0 合并复审 X9 补一句):`createHttpServer` 的 `storePosture` dep **不传** ⇒ `GET /v1/capabilities` 的
458
460
  `store` 取诚实下限 `{declared: <config.dbBackend>, backend: "memory", durable: false}`,且**没有 `store.sql`**(即使递了 `sqlEngineFacts`、真有 SQL 后端 ——
459
461
  7.105.0 上这类嵌入方的顶层 `sql` 是有值的)。要 caps 说真话就把你装出来的后端事实经 `storePosture` 递进来(形 = `src/store-posture.ts` 的 `StorePosture`)。
@@ -879,6 +881,36 @@ UNATTENDED_APPROVAL_POLICY=park
879
881
  (`config.env_key_retired`,任何值都拒);从 `.env` 里删掉它。要让机器多答某一类问,先立案,不开旋钮。
880
882
  - 写面:`default` 模式不带写面 ⇒ 工作区根内(`roots`,与 7.105.0 同)。 `roots` 只约束 Write / Edit 这类结构化文件工具;shell 的根外写、Read 工具的根外读属 core `ordinary`,这些腿上由机器答(落地结果与 ≤7.108.0 的 bypass 放行同,门记录归属词见 MIGRATION 7.109.0「审计读者」)。
881
883
 
884
+ **可选 — 运维的 allow 规则 `APPROVAL_GRANT`(7.115.0 起;缺省关)**
885
+
886
+ ```bash
887
+ APPROVAL_GRANT='mcp__figma__*,a2a__planner__*' # 逗号分隔的工具名规则:运维替这些服务器工具 / A2A 技能表态
888
+ ```
889
+ - **为什么要它**:引擎 7.40.0 起,询问类模式下 MCP 服务器工具 / A2A 技能的调用若**没有任何层表态**,就出一张「没人表态」的卡
890
+ (`askClass: "unstated"`)。请求腿可以在请求体里表态(`settings.permissions.allow`);`run-local` 的 spec 是本服务自己铸的,没有请求体 ——
891
+ 运维接的服务器在那里没人能表态:无席 ⇒ 拒、有 durable 审批块 ⇒ 每次停驻等人。本键就是运维为自己接的服务器表态的地方。
892
+ - **allow 规则写在哪,按腿分**:请求腿(`POST /v1/tasks` / `/v1/runs`)= 请求体 `settings.permissions.allow`;`run-local` = 本键。
893
+ 本键在请求腿上**同样生效**(一条规则,HTTP 提交(`POST /v1/tasks` / `/v1/runs`)、durable 续跑(每一次从持久化的提交体重建,含 leader worker 停驻后经 `/decide` 续跑的那一段)、
894
+ 停驻后台 agent 的跨副本赎回、`run-local` 四条腿同装)。**到不了**:leader worker 的首跑与修复腿(它们的工具策略在 leader 编排里自己折出来,不经部署治理的构造口);
895
+ 这两条腿今天不挂 MCP / A2A,所以本键在那里没有对象。
896
+ - **值形**:精确名(`mcp__figma__get_file`)、`mcp__<server>__*`(整台服务器,含它将来新增的工具)、`a2a__<peer>__*`(整个 peer);
897
+ 与请求体的 allow 桶**同一只**判据。坏值 ⇒ **启动拒**(错误句点名 `APPROVAL_GRANT`、哪一条、怎么改):退役名(`Task` ⇒ `Agent`)、
898
+ 无协议前缀的短名(`figma__x`)、两段形(`mcp__figma` ⇒ 写 `mcp__figma__*`)、反斜杠、超 512 字符、内容形 / 参数级
899
+ (`Bash(npm:*)`、`Read(./x)` —— allow 规则只收工具名形,内容形的常设放行在持久规则店:`/v1/rules/cc-import` 导入,或在卡上答 yes 时留规则)。
900
+ - **不越过任何一层的 deny / ask**:`APPROVAL_DENY` 拒、`APPROVAL_REQUIRE` 问、请求 settings 的 deny / ask、会话规则(`toolDeny` /
901
+ 白名单 `toolAllow`)照旧压过它;plan 模式下服务器工具是 plan 自己的写问,本键不放它。引擎自己的安全地板(不可逆探针、写保护表、
902
+ 读边界)照旧。无人值守腿的按类作答席不因本键多答任何一类 —— 本键只让运维**点名过的**调用不再出卡。
903
+ - **不是白名单**:它只给出表态,不收窄任何东西(「只许这些工具」是会话规则 `PUT /v1/sessions/:id/policy` 的 `toolAllow`)。
904
+ - **射程如实**:规则对**每一个租户、每一条腿**生效;`mcp__<server>__*` 也覆盖那台服务器将来新增的工具。按需写窄;要收回某一只,写
905
+ `APPROVAL_DENY` / `APPROVAL_REQUIRE`(压过本键),或删掉规则重启。它给的只是「没人表态」那一句答,**不放掉任何一层自己的问**:内置工具的
906
+ 精确名(例 `Write` / `Bash`)写进来,`default` 模式下引擎自己的写门 / 形状层照问(问压过放行)—— 对内置工具它几乎不改变任何事。
907
+ - **A2A 如实**:运维声明的对端(配置中心 / `config.d`,刻意不收 `toolAxes`)上,每次技能调用都是引擎自己的外发标记问(`tool_marks_a2a`),不是「没人表态」那一问 —— `a2a__<peer>__*` 不放它,无人值守腿上 A2A 技能调用照旧被拒 / 停驻 / 问人;只有请求体自带的对端(单用户闸)能在 `toolAxes` 里把某个技能的 `egress` 降为 `false`,那之后才是「没人表态」那一问,这时 allow 规则才答得上。
908
+ - **委派子代**:子代理看不到父任务挂的 MCP 服务器工具(引擎的子代工具池只认宿主自己交的工具实例),所以本键在子代上没有对象。
909
+ - **看得见**:起服日志一行 `approval_grant_in_force`(条数 + 规则名);`GET /v1/diagnostics/wiring`(运维面)`serverGates.approvalGrant`
910
+ = 规则名原样(空表 = 未设);`GET /v1/config/catalog` 一行。客户端能力面(`/v1/capabilities`)不报它。
911
+ - **启动告警 `tool_policy_only_sensitive_baseline`**(既有事件,名字不改):没有审批席(`DURABLE_APPROVAL` 关)且不是单用户的部署上,若部署唯一的策略是「不是门」的那几层 —— 敏感路径守卫集(`SENSITIVE_WRITE_PATTERNS`),或 7.115.0 起只有本键 —— 起服时发一次:它们让 `spec.toolPolicy` 在场、引擎自己的 UNGATED 告警因此不再出现,而它们自己又不判写。`detail` 点名是哪一片,`guardedPatterns` / `grantRules` 两个计数。
912
+ - v1 只有 env 一个写入通道(配置中心与 settings 契约没有这个键)。
913
+
882
914
  **可选 — 写保护名表(S-138;缺席=引擎缺省表在岗)**
883
915
 
884
916
  ```bash
@@ -1367,7 +1399,7 @@ bake-runner 要用 ⇒ `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW` 点名放回。
1367
1399
  | `repo` / `council` / `debate` | — | `repo` 为 `code-review`/`scan` 必填;`council`/`debate` 仅 `code-review`,见 §5 |
1368
1400
  | `systemPrompt` / `appendSystemPrompt` | — | 调用方自带的系统提示 / 追加段(各 ≤16384 字符,超限 400 `request.field_invalid`;`appendSystemPrompt` 必须非空串)—— 任务自带时优先于角色 / 全局缺省(见 §模型角色) |
1369
1401
  | `model` | — | 本次用哪个模型:只能点服务端**已配置目录**里的名字 / 档位词 / 模型 id(目录见 `GET /v1/models`;请求体带不进新模型)。未知 ⇒ 400 `request.unknown_reference`(带 `available` 列表);目录里有、但本部署服务不了 ⇒ 400 `request.model_unavailable`;空串 / 非字符串 ⇒ 400 `request.field_invalid` |
1370
- | ~~tools~~ | 🚫 | **不是受理键**(传了 ⇒ 400 `request.body_shape`,`unknownKeys` 点名)——工具面走场景装配;按请求只能**减**:`excludeTools` / `settings.permissions` |
1402
+ | ~~tools~~ | 🚫 | **不是受理键**(传了 ⇒ 400 `request.body_shape`,`unknownKeys` 点名)——工具面走场景装配;按请求只能**减**:`excludeTools` / `settings.permissions` 的 deny · ask(`allow` 7.115.0 起是 allow 规则:给出「有人表态」,回答没人表态的服务器工具 / A2A 调用;不加工具、不放掉任何一层自己的问) |
1371
1403
 
1372
1404
  **`TaskResult`(同步 / `done` 事件里拿到的;7.64.0 起终局是**一条** `terminal` 因由,不再有 `status`/`errorCode`/`errorMessage`/`blockedReason` 四个平面键):**
1373
1405
  ```json
@@ -1385,6 +1417,7 @@ bake-runner 要用 ⇒ `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW` 点名放回。
1385
1417
  | `terminal.kind` | `completed` / `blocked` / `failed` / `paused`(闭集;`timeout` 已于 core 5.8.0/server 6.0.0 退役——见 §9) |
1386
1418
  | `terminal.code` | `failed` 臂的程序化分支:如 `"conflict"`(乐观锁丢失,可重试) |
1387
1419
  | `terminal.reason` | `blocked` 臂:为什么做不了(缺信息/权限) |
1420
+ | `stats.toolCalls` | 工具调用的**尝试**次数,不是「真执行了几次」:模型每发出一次工具调用请求就计一次,计在工具查找与入参校验**之前** —— 请求了没挂载的工具(例:执行车道未点名时的 `Bash`)照样计 `1`;同一条 `Bash` 请求在真执行车道(文件落盘)与未点名车道(没挂、文件不存在)两臂同读 `1`。所以**不能**拿它判「任务真跑了工具」:判「真跑了没有」看 `tool_end.isError`(逐调用,`false` = 这次调用执行成功)与 `wiring_manifest.hands`(逐腿「有没有手」),不看这个数。它是**本腿**计数(续跑腿只计本腿)。 |
1388
1421
 
1389
1422
  > ⚠️ **任务成败以 `terminal.kind`/`terminal.code` 为准,勿以 HTTP 状态码判**:同步腿 200=提交受理成功(任务可能
1390
1423
  > `terminal.kind:"failed"`+`code`);`POST /v1/runs` 202=已受理(执行期结局落在终态 run 记录/`done` 帧)。执行期
@@ -12,7 +12,19 @@
12
12
  # MODEL_GATEWAY_BASEURL MODEL_ID MODEL_API_KEY
13
13
  # ALLOW_SMALL_CLUSTER=true 放行 <3 机(dev/CI 单节点验流程;生产必须 >=3 奇数)
14
14
  set -euo pipefail
15
- HERE="$(cd "$(dirname "$0")" && pwd)"
15
+ # >>> 自身目录(五个入口逐字同一段)
16
+ # HERE = 本脚本**真实文件**所在目录。经 npm bin 软链(node_modules/.bin/sema-up)调用时 $0 是软链路径,
17
+ # 按它的目录找 lib/ 会落进 .bin/ ⇒ 逐跳解软链。只用不带参数的 readlink(macOS 自带 bash 3.2 / BSD readlink 没有 -f)。
18
+ # 库要在知道 HERE 之后才找得到,所以这段不能收进库里:五个入口各持同一段,逐字一致由测试判。
19
+ _self="$0"; _hops=0
20
+ while [ -L "$_self" ]; do
21
+ _hops=$((_hops + 1)); [ "$_hops" -le 40 ] || { echo "软链层数过多(成环?):$0" >&2; exit 1; }
22
+ _dir="$(cd "$(dirname "$_self")" && pwd)"; _self="$(readlink "$_self")"
23
+ case "$_self" in /*) ;; *) _self="$_dir/$_self" ;; esac
24
+ done
25
+ HERE="$(cd "$(dirname "$_self")" && pwd)"
26
+ unset _self _hops _dir
27
+ # <<< 自身目录
16
28
  # `.env` 的文法与全部读写原语只在 lib/envf.sh 定义(S-618 续):本脚本不再有自己的 setk / `. "$ENVF"`。
17
29
  . "$HERE/lib/envf.sh"
18
30
  # 「壳怎么接这台引擎」的渲染(S-850;sema-up.sh / kube-up.sh 同一只)。
@@ -13,7 +13,19 @@
13
13
  # MODEL_GATEWAY_BASEURL MODEL_ID MODEL_API_KEY
14
14
  # [NAMESPACE=sema][STORAGE_CLASS=集群默认][CNPG_VERSION]
15
15
  set -euo pipefail
16
- HERE="$(cd "$(dirname "$0")" && pwd)"
16
+ # >>> 自身目录(五个入口逐字同一段)
17
+ # HERE = 本脚本**真实文件**所在目录。经 npm bin 软链(node_modules/.bin/sema-up)调用时 $0 是软链路径,
18
+ # 按它的目录找 lib/ 会落进 .bin/ ⇒ 逐跳解软链。只用不带参数的 readlink(macOS 自带 bash 3.2 / BSD readlink 没有 -f)。
19
+ # 库要在知道 HERE 之后才找得到,所以这段不能收进库里:五个入口各持同一段,逐字一致由测试判。
20
+ _self="$0"; _hops=0
21
+ while [ -L "$_self" ]; do
22
+ _hops=$((_hops + 1)); [ "$_hops" -le 40 ] || { echo "软链层数过多(成环?):$0" >&2; exit 1; }
23
+ _dir="$(cd "$(dirname "$_self")" && pwd)"; _self="$(readlink "$_self")"
24
+ case "$_self" in /*) ;; *) _self="$_dir/$_self" ;; esac
25
+ done
26
+ HERE="$(cd "$(dirname "$_self")" && pwd)"
27
+ unset _self _hops _dir
28
+ # <<< 自身目录
17
29
  # `.env` 的文法与全部读写原语(envf_get / envf_has / envf_load / envf_put / setk / setk_new / resetk)只在这一处
18
30
  # 定义,sema-up.sh / cluster-up.sh / smoke.sh 同源(S-618 续):本脚本不再有自己的 setk / has_env / `. "$ENVF"`。
19
31
  . "$HERE/lib/envf.sh"
@@ -4,7 +4,19 @@
4
4
  # (sema cloud deploy 渲染用,同一交互协议的 infeasible:{reason,suggestion} 字段)。
5
5
  # 只读探测,零改动,幂等。退出码:0=可部署(可能有 WARN),1=存在 BLOCK 项。
6
6
  set -u
7
- HERE="$(cd "$(dirname "$0")" && pwd)"
7
+ # >>> 自身目录(五个入口逐字同一段)
8
+ # HERE = 本脚本**真实文件**所在目录。经 npm bin 软链(node_modules/.bin/sema-up)调用时 $0 是软链路径,
9
+ # 按它的目录找 lib/ 会落进 .bin/ ⇒ 逐跳解软链。只用不带参数的 readlink(macOS 自带 bash 3.2 / BSD readlink 没有 -f)。
10
+ # 库要在知道 HERE 之后才找得到,所以这段不能收进库里:五个入口各持同一段,逐字一致由测试判。
11
+ _self="$0"; _hops=0
12
+ while [ -L "$_self" ]; do
13
+ _hops=$((_hops + 1)); [ "$_hops" -le 40 ] || { echo "软链层数过多(成环?):$0" >&2; exit 1; }
14
+ _dir="$(cd "$(dirname "$_self")" && pwd)"; _self="$(readlink "$_self")"
15
+ case "$_self" in /*) ;; *) _self="$_dir/$_self" ;; esac
16
+ done
17
+ HERE="$(cd "$(dirname "$_self")" && pwd)"
18
+ unset _self _hops _dir
19
+ # <<< 自身目录
8
20
  # 自动数据根的路径形只在 lib/dataroot.sh 定义一次(S-851):下面「多盘机」建议句与 sema-up 自动选的根同一条规则。
9
21
  . "$HERE/lib/dataroot.sh"
10
22
  # 取值旗的形(S-621):判据与词表同 sema-up.sh 一处定义。
@@ -14,7 +14,19 @@
14
14
  # 本机装 k3s(后两态=kube-up.sh,helm chart 驱动)
15
15
  set -euo pipefail
16
16
 
17
- HERE="$(cd "$(dirname "$0")" && pwd)"
17
+ # >>> 自身目录(五个入口逐字同一段)
18
+ # HERE = 本脚本**真实文件**所在目录。经 npm bin 软链(node_modules/.bin/sema-up)调用时 $0 是软链路径,
19
+ # 按它的目录找 lib/ 会落进 .bin/ ⇒ 逐跳解软链。只用不带参数的 readlink(macOS 自带 bash 3.2 / BSD readlink 没有 -f)。
20
+ # 库要在知道 HERE 之后才找得到,所以这段不能收进库里:五个入口各持同一段,逐字一致由测试判。
21
+ _self="$0"; _hops=0
22
+ while [ -L "$_self" ]; do
23
+ _hops=$((_hops + 1)); [ "$_hops" -le 40 ] || { echo "软链层数过多(成环?):$0" >&2; exit 1; }
24
+ _dir="$(cd "$(dirname "$_self")" && pwd)"; _self="$(readlink "$_self")"
25
+ case "$_self" in /*) ;; *) _self="$_dir/$_self" ;; esac
26
+ done
27
+ HERE="$(cd "$(dirname "$_self")" && pwd)"
28
+ unset _self _hops _dir
29
+ # <<< 自身目录
18
30
  # `.env` 文法(写口 envf_representable + envf_encode / 读口 envf_canonical_line)的唯一定义 —— 写读两半从这一处取判据。
19
31
  . "$HERE/lib/envf.sh"
20
32
  # 数据根归属(S-851)与「壳怎么接」的渲染(S-850)各一处定义;preflight / kube-up / cluster-up 读同一份。
@@ -5,7 +5,19 @@
5
5
  # 远程 URL 模式: smoke.sh --url=http://host:8090 --token=T (引擎面:health/auth/真任务)
6
6
  # 每项 OK/FAIL 一行;FAIL 带原因+建议(§3d-2 报错规范);exit 0=全绿。
7
7
  set -u
8
- HERE="$(cd "$(dirname "$0")" && pwd)"
8
+ # >>> 自身目录(五个入口逐字同一段)
9
+ # HERE = 本脚本**真实文件**所在目录。经 npm bin 软链(node_modules/.bin/sema-up)调用时 $0 是软链路径,
10
+ # 按它的目录找 lib/ 会落进 .bin/ ⇒ 逐跳解软链。只用不带参数的 readlink(macOS 自带 bash 3.2 / BSD readlink 没有 -f)。
11
+ # 库要在知道 HERE 之后才找得到,所以这段不能收进库里:五个入口各持同一段,逐字一致由测试判。
12
+ _self="$0"; _hops=0
13
+ while [ -L "$_self" ]; do
14
+ _hops=$((_hops + 1)); [ "$_hops" -le 40 ] || { echo "软链层数过多(成环?):$0" >&2; exit 1; }
15
+ _dir="$(cd "$(dirname "$_self")" && pwd)"; _self="$(readlink "$_self")"
16
+ case "$_self" in /*) ;; *) _self="$_dir/$_self" ;; esac
17
+ done
18
+ HERE="$(cd "$(dirname "$_self")" && pwd)"
19
+ unset _self _hops _dir
20
+ # <<< 自身目录
9
21
  # `.env` 的文法与读原语只在 lib/envf.sh 定义(S-618 续):本脚本读 `.env` 只经 envf_load,不 source。
10
22
  . "$HERE/lib/envf.sh"
11
23
  # 取值旗的形(S-621):判据同 sema-up.sh 一处定义。
@@ -25,7 +25,6 @@ export declare const AdoptionSourceSchema: z.ZodObject<{
25
25
  kind: z.ZodLiteral<"principal">;
26
26
  principal: z.ZodString;
27
27
  }, z.core.$strict>;
28
- export type AdoptionSource = z.infer<typeof AdoptionSourceSchema>;
29
28
  /** per-store 腿的动作词(183 §6 闭集;form b 只会产出前两个 + `none`)。 */
30
29
  export declare const AdoptionLegActionSchema: z.ZodEnum<{
31
30
  none: "none";
@@ -239,7 +238,6 @@ export declare const AdoptionRejectDetailSchema: z.ZodObject<{
239
238
  overlapping: z.ZodNumber;
240
239
  }, z.core.$strict>>;
241
240
  }, z.core.$strict>;
242
- export type AdoptionRejectDetail = z.infer<typeof AdoptionRejectDetailSchema>;
243
241
  /**
244
242
  * 拒绝码的**闭集**(与附录 A 的 `adoption.*` 族逐字同名)。住在 wire 契约里而不是实现里,因为落库的
245
243
  * `adoption_log.reject_code` 与 wire 上的 `errorCode` 必须是**同一个词表** —— 两处各写一份字符串字面量,
@@ -252,5 +250,4 @@ export declare const AdoptionRequestSchema: z.ZodObject<{
252
250
  fromPrincipal: z.ZodString;
253
251
  toPrincipal: z.ZodString;
254
252
  }, z.core.$strict>;
255
- export type AdoptionRequest = z.infer<typeof AdoptionRequestSchema>;
256
253
  //# sourceMappingURL=wire.d.ts.map
@@ -269,6 +269,23 @@ export type ProbeCauseProjection = z.infer<typeof ProbeCauseSchema>;
269
269
  * (截出来的是另一个机器码);`shown` 超限 ⇒ **截条目**,`total` 逐字保真。
270
270
  */
271
271
  export declare function readProbeCause(req: unknown): ProbeCauseProjection | undefined;
272
+ /**
273
+ * `AskRequest.probeReason`(core 5.33.0 [ref]:`probeCause` 的**散文**兄弟 —— 探针对「为什么没清掉这一问」的一句话)的
274
+ * **边界窄读**,活卡帧(`ToolApprovalFrame.probeReason`)的**唯一**铸造点。
275
+ *
276
+ * 🔴 **只有活卡帧**:`card_json` / 呈卡帧不携本键(`ApprovalCardSchema` 是 `.strict()` 的耐久信封,加键 = 回滚窗内旧二进制读不动新行,
277
+ * 要另行裁定)。耐久面的对偶 = `gate.riskDescriptor.probeReason`(park 行整体透传,出门前过 `redactRiskDescriptor` 的 `text` 叶,
278
+ * 与本函数同一只脱敏规则 ⇒ 对同一句话两面同值)。
279
+ *
280
+ * 姿势逐条(与 {@link readProbeCause} / {@link readRuleEvidence} 同款分工):
281
+ * · `safeParse`,非串 ⇒ 按缺席处置(宪法 [ref]);空串 / 纯空白 ⇒ 缺席(core 自己也只在 `trim() !== ""` 时铸)。
282
+ * · 长度:超过 {@link MAX_PROBE_REASON_CODEPOINTS} ⇒ **整只丢,不截**(判据与 `probeCause.code` 超限同形);
283
+ * 先判长度再脱敏 ⇒ 喂给脱敏器的永远是个短串(脱敏器的 RangeError 面是超大输入)。
284
+ * · 内容族(部署自写的探针散文,人眼信任边界上的文本)⇒ `redactSecrets`。脱敏后**不再截**:耐久面同样不截,
285
+ * 两面逐字节同值比「帧上短一截」重要(`persistedRuleShadowed` R2-[medium] 的反面:那里有卡面要对齐,这里没有)。
286
+ * · 缺席 ≠「没有原因」:只有 maybe 档收紧且探针真给了 reason 的 ask 才有它。
287
+ */
288
+ export declare function readProbeReason(req: unknown): string | undefined;
272
289
  /**
273
290
  * [ref](core 7.19.0)—— `readRootCandidate.dir` 的**上限**。
274
291
  *
@@ -512,26 +529,6 @@ declare const DenialLimitFallbackSchema: z.ZodObject<{
512
529
  autoDenyAfterMs: z.ZodNumber;
513
530
  }, z.core.$strict>;
514
531
  export type DenialLimitFallback = z.infer<typeof DenialLimitFallbackSchema>;
515
- /**
516
- * ⚰️ **退役成员的形(server 7.69.0–7.71.0 铸过,7.72.0 起永不再铸)** —— core 7.14.0 [ref] 把
517
- * `classifierUnavailable` 从 ask 侧**整族**删掉(事实改骑 `GateDisposition.denied.cause`),所以本仓
518
- * 的**铸造**腿(`buildApprovalCard` / 活卡帧 / inbox 行)本批全部摘掉;窄读函数
519
- * `readClassifierUnavailable` 与对 core 型面的合规钉连同它们的素材一起删(素材没了,钉的两端只剩一端)。
520
- *
521
- * 🔴 **这只 schema 本身留着,而且是承重的,不是没删干净**:`ApprovalCardSchema` 是 `.strict()`,
522
- * 而它同时是 `approval_ask.card_json` 这张**耐久列**的读契约(读侧三个 `safeParse`:重放腿、幂等重入、
523
- * 对账扫描)。把成员从 strict schema 上删掉 ⇒ 7.69.0–7.71.0 铸下、升级时**仍然 pending** 的那些行
524
- * 当场判假 ⇒ 「跳过 + 一次 warn」:人手上那张卡在重连后变不回来、幂等重入退成 park。
525
- * ⇒ 一条耐久投影的 schema **必须读得懂它自己写过的每一版**;写侧只写当前引擎供得出的键,读侧接受
526
- * 全部历史成员。这不是给退役键开特例,这是 strict + durable 这一组合的内生义务(同样的处置将适用于
527
- * 未来任何一次键退役)。
528
- *
529
- * 形保持原样(单成员 `cause` 必填、`.strict()`):它描述的是**已经写在库里的字节**,不许再动。
530
- */
531
- declare const ClassifierUnavailableSchema: z.ZodObject<{
532
- cause: z.ZodString;
533
- }, z.core.$strict>;
534
- export type ClassifierUnavailable = z.infer<typeof ClassifierUnavailableSchema>;
535
532
  /**
536
533
  * `AskRequest.denialLimitFallback`(core 7.4.0 [ref])的**边界窄读** —— 活卡帧与 `card_json` 的唯一铸造点
537
534
  * (与 {@link readRuleOffersAbsence} / {@link readProbeCause} 同款分工),两面结构性同值。
@@ -651,8 +648,8 @@ export declare const ApprovalCardSchema: z.ZodObject<{
651
648
  }, z.core.$strict>>>;
652
649
  personalRuleDotsAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
653
650
  }, z.core.$strict>>;
654
- askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary">>;
655
- classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary">>>;
651
+ askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>;
652
+ classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>>;
656
653
  fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
657
654
  sourceTaskId: z.ZodOptional<z.ZodString>;
658
655
  sourceAgentName: z.ZodOptional<z.ZodString>;
@@ -766,8 +763,8 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
766
763
  }, z.core.$strict>>>;
767
764
  personalRuleDotsAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
768
765
  }, z.core.$strict>>;
769
- askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary">>;
770
- classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary">>>;
766
+ askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>;
767
+ classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>>;
771
768
  fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
772
769
  sourceTaskId: z.ZodOptional<z.ZodString>;
773
770
  sourceAgentName: z.ZodOptional<z.ZodString>;
@@ -160,6 +160,18 @@ export function readProbeCause(req) {
160
160
  ...(raw.further !== undefined ? { further: family(raw.further) } : {}),
161
161
  };
162
162
  }
163
+ const MAX_PROBE_REASON_CODEPOINTS = 201;
164
+ const ProbeReasonEnvelopeSchema = z.object({ probeReason: z.string().optional() });
165
+ export function readProbeReason(req) {
166
+ const parsed = ProbeReasonEnvelopeSchema.safeParse(req);
167
+ const raw = parsed.success ? parsed.data.probeReason : undefined;
168
+ if (raw === undefined || raw.trim() === "")
169
+ return undefined;
170
+ if ([...raw].length > MAX_PROBE_REASON_CODEPOINTS)
171
+ return undefined;
172
+ const redacted = redactSecrets(raw);
173
+ return redacted === "" ? undefined : redacted;
174
+ }
163
175
  export { READ_ROOT_CANDIDATE_DIR_MAX };
164
176
  const ReadRootCandidateEnvelopeSchema = z.object({
165
177
  readRootCandidate: z.object({ dir: z.string().min(1), clearsThisAsk: z.literal(true), covers: z.literal("exact").optional() }).optional(),
package/dist/approval.js CHANGED
@@ -74,6 +74,9 @@ function createNameSetMatcher(names) {
74
74
  return (toolName) => exact.has(toolName);
75
75
  return (toolName) => exact.has(toolName) || covering.some((rule) => namespacedRuleNameCovers(rule, toolName));
76
76
  }
77
+ function grantedByRule() {
78
+ return { action: "allow", decisionReason: "rule" };
79
+ }
77
80
  export function createDurableAskPolicy(opts) {
78
81
  const require = createNameSetMatcher(opts.requireApproval);
79
82
  const deny = createNameSetMatcher(opts.deny ?? []);
@@ -105,7 +108,7 @@ export function createDurableAskPolicy(opts) {
105
108
  }
106
109
  if (exempted) {
107
110
  opts.onExempted?.(req.toolName, req.toolName);
108
- return { action: "allow" };
111
+ return grantedByRule();
109
112
  }
110
113
  return decideRequired(req, requireHit);
111
114
  })();
@@ -122,7 +125,7 @@ export function createDurableAskPolicy(opts) {
122
125
  }
123
126
  if (heldRuleName(req, isQuestionTool) === undefined && budget > 0 && autoApproved < budget) {
124
127
  autoApproved++;
125
- return { action: "allow" };
128
+ return grantedByRule();
126
129
  }
127
130
  return operatorAsk(`APPROVAL_REQUIRE(${requireHit})`, `approval required to run "${req.toolName}"`);
128
131
  }
@@ -3,10 +3,11 @@ import type { Logger } from "../observability/logger.js";
3
3
  export interface ProcessExitBoundary {
4
4
  /**
5
5
  * **优化点**,不承担不变量:受控退的收尾链在它自己的终点早按一次子进程腿(理由逐字写在
6
- * `boot/shutdown.ts` 的调用点上 —— 那之后进程还活十秒量级,后台腿的软→硬梯子跑得完)。
6
+ * `boot/shutdown.ts` 的调用点上 —— 调用方随后在退出前等一次后台宽限,后台腿的软→硬梯子跑得完)。
7
7
  * 工作区腿**刻意不给**这条早按:停机中段 `rm -rf` 会把在飞 turn 正在写的目录从它脚下抽掉。
8
+ * 返回本次软停的后台行数(core `reapAllSessionBackground` 的返回;腿抛错 ⇒ 0,warn 已记)—— 调用方据此决定欠不欠宽限。
8
9
  */
9
- reapChildProcessesEarly(): void;
10
+ reapChildProcessesEarly(): number;
10
11
  /** 摘掉本次装配的 `exit` 钩。幂等(同进程内反复装配的调用方=测试必须用它收尾)。 */
11
12
  dispose(): void;
12
13
  }
@@ -5,8 +5,10 @@ import { liveEphemeralHostWorkspaceCount, reapEphemeralHostWorkspaces } from "..
5
5
  const PROCESS_EXIT_INCLUDES_RETAINED = false;
6
6
  export function installProcessExitBoundary({ logger }) {
7
7
  const reapOwnChildProcesses = () => {
8
+ let backgroundSoftStopped = 0;
8
9
  try {
9
10
  const reaped = defaultTaskRegistry.reapAllSessionBackground();
11
+ backgroundSoftStopped = reaped;
10
12
  if (reaped > 0)
11
13
  logger.info("shutdown_background_reaped", { reaped });
12
14
  }
@@ -23,6 +25,7 @@ export function installProcessExitBoundary({ logger }) {
23
25
  catch (err) {
24
26
  logger.warn("shutdown_foreground_reap_failed", { err: String(err) });
25
27
  }
28
+ return backgroundSoftStopped;
26
29
  };
27
30
  const reapBackgroundHard = () => {
28
31
  try {
@@ -9,7 +9,7 @@ import { gateScenarioRequest, mergeUserSkills, selectScenario } from "../capabil
9
9
  import { stampHandsLane, toolPolicyForHands } from "../capabilities/hands-lane.js";
10
10
  import { buildNoticeLaneFacts, shouldDiscloseNoPersistentMemory, withNoPersistentMemoryNotice } from "../capabilities/memory-notice.js";
11
11
  import { applyLongtailDefer } from "../capabilities/tool-defer.js";
12
- import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs } from "../deployment-governance.js";
12
+ import { assertGuardPatternsUsable, buildApprovalGrantNotice, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs } from "../deployment-governance.js";
13
13
  import { singleUserAutoAcceptBaselineApplies } from "../approval.js";
14
14
  import { acceptShellScratchpadDir, buildEnvFacts, egressForRemoteExec, ensureScratchpadDir, resumeFactsForLane, scratchpadCoveringTaskRoot } from "../env-facts.js";
15
15
  import { FleetEventBus } from "../fleet/fleet-bus.js";
@@ -59,6 +59,9 @@ export function createResolveSpec(ctx) {
59
59
  };
60
60
  if (onlySensitiveBaseline)
61
61
  logger.warn(onlySensitiveBaseline.event, onlySensitiveBaseline.fields);
62
+ const approvalGrantNotice = buildApprovalGrantNotice(config);
63
+ if (approvalGrantNotice)
64
+ logger.info(approvalGrantNotice.event, approvalGrantNotice.fields);
62
65
  const resolveSpec = async (body, req, auth, opts) => {
63
66
  assertRequestMcpUnlocked(body.mcpServers, lockedKeys);
64
67
  if (opts?.leg === "fresh")
@@ -96,6 +96,25 @@ export interface ShutdownCtx {
96
96
  * `backend.close()` 之前等它 quiesce —— 在飞的 `put`(审批 park / 资源挂起…,与审批协调器在不在场无关)不许被抽掉连接池。 */
97
97
  parkCommits: ParkCommitWatch | undefined;
98
98
  }
99
+ /**
100
+ * 受控退的**后台宽限**还要等多久(ms)—— hardShutdown 软停后台 shell 之后、`process.exit` 之前,给软停过的行**一次**
101
+ * `HOST_BG_KILL_GRACE_MS` 收完合作清理(host 梯子:SIGTERM → 宽限 → 组 SIGKILL)。
102
+ *
103
+ * 没有这一步时,**无活连接**的干净停机里 `server.close` 回调立刻落地、链上其余步骤都是毫秒级,`exit` 几乎紧跟软停,退出边界钩同拍一枪
104
+ * 组 SIGKILL ⇒ 收到 SIGTERM 后要一点时间停容器 / 刷盘的后台 shell 被腰斩。**只缩不延**的两条:
105
+ * · 起点是软停时刻(不是「别的步骤做完之后」)—— 慢 close / 审批面收尾已经耗掉的时间算进宽限里,链上别的等待不与它相加,取较晚者;
106
+ * · 封顶在 hardShutdown 起点算的**同一个到期点**(`deadlineMs`,与审批面收尾 / 店 quiesce 共读)—— 运维把宽限调得再长,
107
+ * 退出也不晚于到期点,一定早于 15 s 的 `finalExit`,不延长停机总上限。
108
+ * `softStopped` = 本次软停的后台行数(core `reapAllSessionBackground` 的返回;0 ⇒ 没有梯子在跑、不欠宽限,空闲引擎的退出不变慢)。
109
+ * 该数里含后台 agent / workflow 行(core 不拆开数),它们没有 shell 梯子 ⇒ 只有这一形会**多**等(≤ 一个宽限),不会少等。
110
+ */
111
+ export declare function backgroundGraceRemainingMs(a: {
112
+ softStopped: number;
113
+ softStoppedAt: number;
114
+ now: number;
115
+ graceMs: number;
116
+ deadlineMs: number;
117
+ }): number;
99
118
  /**
100
119
  * `installShutdownHandlers` 的**摘除把手**([ref] 二轮重扫件4)。
101
120
  *