@sema-agent/server 7.127.1 → 7.128.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 (41) hide show
  1. package/MIGRATION.md +39 -0
  2. package/USAGE.md +53 -3
  3. package/dist/audit.js +4 -1
  4. package/dist/boot/execution-lane.d.ts +11 -0
  5. package/dist/boot/execution-lane.js +6 -0
  6. package/dist/boot/resolve-spec.js +3 -1
  7. package/dist/boot/web-admin.js +15 -11
  8. package/dist/capabilities/mcp-probe.js +11 -5
  9. package/dist/config-catalog.js +1 -1
  10. package/dist/config-provider.d.ts +17 -4
  11. package/dist/config-provider.js +8 -6
  12. package/dist/config-types.d.ts +2 -2
  13. package/dist/hooks/hook-runner.d.ts +1 -0
  14. package/dist/hooks/hook-runner.js +10 -4
  15. package/dist/http/wire-types.d.ts +11 -3
  16. package/dist/main.js +2 -0
  17. package/dist/mcp-assertion-header.d.ts +89 -0
  18. package/dist/mcp-assertion-header.js +49 -0
  19. package/dist/mcp-tool-axes.d.ts +62 -0
  20. package/dist/mcp-tool-axes.js +47 -0
  21. package/dist/observability/fail-open.d.ts +1 -1
  22. package/dist/observability/fail-open.js +1 -1
  23. package/dist/plugins/host-platform.d.ts +43 -13
  24. package/dist/plugins/host-platform.js +67 -18
  25. package/dist/plugins/remote-env-host.js +3 -3
  26. package/dist/run-local.js +5 -3
  27. package/dist/task-mcp.d.ts +31 -3
  28. package/dist/task-mcp.js +42 -4
  29. package/dist/trace/project.d.ts +3 -1
  30. package/dist/trace/project.js +2 -2
  31. package/dist/trace/self-minted-wire.d.ts +19 -0
  32. package/dist/trace/self-minted-wire.js +2 -0
  33. package/dist/trace/tool-output-media.d.ts +21 -8
  34. package/dist/trace/tool-output-media.js +44 -17
  35. package/dist/web-admin-client/facade.d.ts +1 -1
  36. package/dist/web-admin-client/facade.js +1 -1
  37. package/dist/web-admin-client/http-client.d.ts +2 -2
  38. package/dist/web-admin-client/skills-mcp.d.ts +19 -16
  39. package/dist/web-admin-client/skills-mcp.js +27 -8
  40. package/dist/web-admin-client/types.d.ts +18 -0
  41. package/package.json +1 -1
package/MIGRATION.md CHANGED
@@ -7,6 +7,45 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.128.0 —— 本地 `config.d/skills.json` 生效 · 部署侧 MCP `toolAxes` 进引擎(坏形整条不挂)· 宿主 shell 两行运维行 · 7.127.2 第 1 条两句订正(升级零动作)
11
+
12
+ 1. **本地部署的 `config.d/skills.json` 开始生效(行为修复,零 DDL、无配置变化)**:`CONFIG_PROVIDER=local` 的部署此前只装出厂技能,`config.d/skills.json` 的条目从不进名册(零提示;
13
+ 同一目录的 `mcp.json` 是生效的)。本版起 HTTP 服务起服与 `run-local` 都按名把这些条目盖在出厂技能上,与配了中心的部署同一条路径;`enabled: false` 的不装。
14
+ - **谁受影响**:写着 `skills.json` 的本地部署 —— 升级后那些技能真进名册,模型看得见的技能清单(从而可用的工具面)可能变化,同名条目盖掉出厂技能。
15
+ - **做法**:不想要的条目写 `enabled: false` 或删掉文件;起服日志每装上一条一行 `web_admin_skill`(带 `overrodeBuiltin`),拿不到正文的一条一行 `web_admin_skill_failed`(`reason`)。
16
+ 与 MCP 同,改了 `skills.json` 要重启才生效(刷新拍后 `/health` 的 `restart.reasons` 含 `skills`)。技能的来源与覆盖序见 USAGE「技能从哪来」。
17
+ 2. **部署侧 MCP 条目的 `toolAxes` 进引擎(新能力,零动作)**:`config.d/mcp.json` 与配置中心的 MCP 条目若写了 `toolAxes`(按工具名声明 `effect` / `egress` / `irreversibility`),本版起进引擎 spec、喂审批门;此前只承载不读。坏形(词外 / `egress` 非布尔 / 未知成员)⇒ **这条 server 整条不挂**,起服一行 `web_admin_mcp_tool_axes_invalid` 点名 server 与工具名、兄弟 server 照挂 —— 此前坏形被静默忽略、server 照挂。
18
+ - **谁受影响**:条目里早就写着 `toolAxes` 的部署 —— 合法的本版起真生效(调用的审批姿态可能变),坏形的那条 server 本版起不挂(改条目即恢复)。
19
+ 3. **宿主车道两行新运维行(零动作)**:`REMOTE_EXEC=host`(含 `run-local`)起服时解析一次本机 shell;Windows 上找不到 Git Bash ⇒ `warn host_shell_unavailable { err, remedy }`,解析到 / 装好后拾起 ⇒ `info host_shell_resolved { shell }`(按状态转换打,每条 Bash 不打);macOS / Linux 不打。找不到不挡起服,Bash / hooks 到 spawn 时以真因拒。
20
+ 4. **订正 7.127.2 节第 1 条两句**(已发节正文只读):(a)「账本面不存媒体字节」是**读面与新写行**的律 —— 7.127.0 / 7.127.1 写下的存量行里的字节仍在店里(file 后端 `events.jsonl` 逐位不变),读面隐藏而不改写存量行,回滚到旧版又读得到;要清掉只能走会话删除或留存到期。(b)「整图只在活流与子代 tail 流上」按字面不成立 —— 会话存档 `GET /v1/sessions/:id`(会话树自己的内容,续轮回送模型)的 `messages[].content[].data` 与 `messages[].details.file.base64` 两处都带整图,7.127.x 与本版同;本版只是让后一处的字节座位不再过文本脱敏(此前碰巧成凭据形的子串会被改写成占位、图读坏)。
21
+
22
+ ## 7.127.2 —— 账本行 details 卡的媒体字节不落账(行为修复)· 缺省部署自动整理被引擎拒(披露)· 7.127.0 节三条订正(升级零动作)
23
+
24
+ 升级零动作:无 DDL、无配置变化。本节是对 7.127.0 节的三条订正(已发节正文只读,订正记在此处)。
25
+
26
+ 1. **订正 7.127.0 节第 1 条「账本面只存描述符 … 这类客户端本版拿不到整图」(行为修复,持久数据面,升级零动作)**:7.127.0 / 7.127.1 上只有 `output` 的媒体块在账本面是描述符,
27
+ 同一行里 Read 图像 / PDF 的 details 卡 `structured`(`{ type: "image" | "document", file: { base64?, … } }`)的 `file.base64`(引擎只在 ≤48K 字符时随卡)仍随账本行落库,并在
28
+ `GET /v1/runs/:id/events`、`GET /v1/tasks/:id/turns`、`GET /v1/tasks/:id/stream` 的回放上出现 —— 描述符想省的那份字节还在行里。本版起一条律:**媒体字节不落账,不论它骑在 `output` 的块上还是 `structured` 卡的字段上**。
29
+ - **账本面**:卡上去掉 `file.base64` 这一个键(其余成员逐字;与引擎对超 48K 的卡自己略去该键的形一致),存量行在读面上同样不再带;帧级 `truncated` / `totalChars` 仍只说 `output`。
30
+ - **活面**:`structured` 逐字(`file.base64` 不过文本脱敏,卡上其余成员照常脱敏);连接积压判据把它与 `output` 媒体块的字节同一帧一起计,降级时一并去掉。
31
+ - **谁需要**:读账本回放面的消费方与存账本的部署;**谁受伤**:从回放面的 `structured.file.base64` 取图的客户端(整图本来只在 `/v1/tasks/stream` 与子代 tail 流上);**补偿**:要图读活流;按「`file.base64` 缺席」渲染(引擎超 48K 的卡本就是这个形)。
32
+ 2. **订正 7.127.0 节漏项:k8s 车道沙箱出网策略(安全面,部署;7.127.0 起已生效)**:7.127.0 的 CHANGELOG 段写了这一条,随包的本文件 7.127.0 节漏了。`REMOTE_EXEC=k8s` 的部署读下面几行:
33
+ - **出网边界**:沙箱 ns 的 egress 白名单对集群外只放行 **IPv4 公网面** —— 私网 / 共享 / 链路本地 / 环回六段(`10/8`、`172.16/12`、`192.168/16`、`100.64/10`、`169.254/16`、`127/8`)整段不放行,
34
+ 公网面上的集群地址按 `clusterCidrs` 与 `nodeCidrs`(`kube-up.sh` 遍历**全部节点**的全部 IPv4 Internal / External 地址自动填;读不出 ⇒ 拒装)再除外。此前是一张点名表,只盖第一只节点的第一只 InternalIP。
35
+ - **IPv6**:不写放行规则(NetworkPolicy 只有放行规则,写 `::/0` 只会放宽);缺省 `K8S_SANDBOX_IPV6=enabled` **不关** pod 的 IPv6 —— 只执行 IPv4 的策略实现(k3s 单栈缺省即是)管不到 pod 的 IPv6,链路本地 v6 照通,
36
+ 起服自检每次起服一行 `sandbox_egress_policy_gap{kind: "ipv6_enabled"}` 提醒。要在 pod 侧关,设 `K8S_SANDBOX_IPV6=disabled`(chart `sandbox.ipv6: disabled`;前提:**每个节点**的 kubelet 带 `--allowed-unsafe-sysctls` 放行
37
+ `net.ipv6.conf.all.disable_ipv6` 与 `net.ipv6.conf.default.disable_ipv6`,沙箱 ns 不 enforce PodSecurity `baseline` / `restricted`;没放行的节点上沙箱 pod 起不来)。
38
+ - **谁受伤**:沙箱要访问集群外**私网地址**上的包源 / 代码托管 / 对象存储的部署(升级后到不了);手写 values 在 `nodeCidrs` / `clusterCidrs` 里写了 IPv6 或不带前缀的项的部署(chart 渲染拒并点名);
39
+ 手写 RBAC 的部署(chart 多两只只读授权:沙箱 ns 的 `networkpolicies` get / list,与集群级 `nodes` get / list —— 没给时起服自检一行 `unreadable`)。
40
+ - **补偿**:在沙箱 ns 另建一条 NetworkPolicy 放行那个私网地址(或那台主机的 /32)—— 策略按并集叠加,chart 升级不碰它;手写 RBAC 的部署补上面两只只读授权;没有上报进节点对象的公网地址(1:1 NAT 的弹性 IP 一类)要自己写进 `nodeCidrs`。
41
+ 这张策略是收窄、不是边界:所在节点的往来流量由 CNI 的实现决定,宿主上被 DNAT 的发布端口在策略之前改写目的地址,CNI 不执行 NetworkPolicy 时整张不生效(射程见 USAGE §9.3e)。
42
+ 3. **订正 7.127.0 节第 4 条「记忆整理按人的路径规则判写」(披露:引擎缺陷,本版行为不变)**:那一条在缺省部署上不成立 —— 只要部署的敏感路径守卫在场(`SENSITIVE_WRITE_PATTERNS` 未设即在场),
43
+ 自动整理**每一次**都被拒(`memory.consolidation_refused`,`detail.refusalCode = memory.consolidation_write_refused`),与有没有点名库路径的规则、权限模式无关:引擎问写判官时递给部署策略的是一只已中止的信号,
44
+ 守卫因此判不了库目录的真实落点、按 `target_unresolved` 拒。通告的人读句只带拒因的前 200 字,`detail` 没有原因成员。修复在引擎,随下一次引擎提货生效。
45
+ - **谁受影响**:开着自动整理的部署(缺省开)。用户任务不受影响,库不被改写。
46
+ - **做法**:要整理就用手动阀门 `POST /v1/admin/memory/consolidation/run`(它不问这只判官,与第 4 条所写同);按 `memory.consolidation_refused` 告警的,知道它今天是这个成因。
47
+ 不要为此设 `SENSITIVE_WRITE_PATTERNS=off`(那会撤掉敏感路径的写拦截)。
48
+
10
49
  ## 7.127.1 —— Windows 宿主车道:策略链读 `cdPath` 不再因 shell 未落定而拒每条 Bash(行为修复,升级零动作)
11
50
 
12
51
  升级零动作:无 DDL、无配置变化、无 wire 变化。**谁受影响**:Windows 宿主部署里 server 进程环境找不到 Git Bash 的(PATH 无 Git、`ProgramFiles` 不对、经 ssh 起服白名单环境等)—— 7.127.0 及之前每条 Bash 都被策略链拒成「policy check threw / aborted」且真因被吞;本版起策略链读 `cdPath` 不再碰 shell,命令真要跑时才以真因拒(装 Git for Windows 或设 `HOST_SHELL_PATH`)。装了 Git Bash 的普通部署行为不变。回滚到 7.127.0 安全。
package/USAGE.md CHANGED
@@ -646,6 +646,13 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
646
646
  `projectContext` / `callerCwd` 回答(那两位的判据多一条单用户约束;多租户 ∧ host 部署自 7.104.0 起拒启,这一形的分叉不再可达)。
647
647
  端点/凭据/镜像/`serial` 一个字都不上这条 wire,那些仍只在 operator 面 `GET /v1/config/catalog`。
648
648
 
649
+ **宿主车道的 shell(Windows 宿主;7.128.0 起成文)**
650
+ - 宿主车道(`REMOTE_EXEC=host`,含 `run-local`)的 Bash、后台 shell 与 hooks 的 command 条目都用本机 POSIX shell 起。Windows 上是 Git Bash:`HOST_SHELL_PATH` 显式覆写,否则 Git for Windows 安装目录,
651
+ 再否则 PATH 里的 `bash.exe`;`System32` 下的 `bash.exe` 是 WSL 启动器,刻意不认。**起服时解析一次**,找不到不挡起服。
652
+ - 两行运维行(进程级,**按状态转换打**:同态重复解析、每条 Bash 都不再打):`warn host_shell_unavailable { err, remedy }` —— 找不到可用的 bash;`err` 是脱敏后的原因,`remedy` 是出路句
653
+ (装 Git for Windows,或把 `HOST_SHELL_PATH` 设到 bash.exe);`info host_shell_resolved { shell }` —— 解析到了(起服成功,或失败之后装好 / 改好被下一次解析拾起),`shell` 是解析到的路径。
654
+ - 找不到时:宿主车道的其它工具照常;要起 shell 的调用到 spawn 时以同一句真因拒(`shell_unavailable` / hook 故障帧),装好后下一次调用自动拾起,不用重启。macOS / Linux 不打这两行。
655
+
649
656
  **workspace 浏览面 —— 已随整树快照纪元退役**
650
657
  - 旧四端点(list/tree/file/archive)恒 `501 capability.workspace_retired`,`capabilities.workspace`
651
658
  恒 `false`。理由:rewind 换代为 per-file 历史(只备份代理**编辑过**的文件,无树遍历、无体积门),
@@ -1183,6 +1190,38 @@ TASK_WRITE_FACE_ESCALATION=allow # 词表 allow|refuse;坏词启动期响亮
1183
1190
  配置管道);`GET /v1/sessions/:id/mcp` 面板与执行面同一台账(被撤服务器的工具不再展示)。
1184
1191
  纯 env 配置的 `MCP_SERVERS` 部署无中心撤销语义,行为逐字不变。
1185
1192
 
1193
+ **技能从哪来(来源与覆盖序)**
1194
+
1195
+ 一个 run 装着的技能按下面的顺序叠出来(每条技能的 `scenarios` 为空 = 进所有场景,写了 = 只进这些场景):
1196
+
1197
+ 1. **出厂技能 = `SKILLS_DIR` 这一个目录**(缺席 = 包内自带的 `skills/`,按模块位置解析,不是进程 cwd)。目录里每个 `<名>.md`(frontmatter `name` / `description` / `scenarios`)
1198
+ 或 `<名>/SKILL.md`(同目录的其余文件作附件)是一条。**设了 `SKILLS_DIR` 就整个替换包内出厂技能,不叠加** —— 想在出厂集上加几条,把包内 `skills/` 拷过去再加;
1199
+ 目录不存在 = 零出厂技能(不报错)。
1200
+ 2. **配置源的技能**:配了中心 = web-admin 的 skills 域;`CONFIG_PROVIDER=local` = `config.d/skills.json`(`{ "skills": [{ "name", "description", "content", "scenarios"?, "enabled"? }] }`,
1201
+ 正文写在 `content` 里)。**按名盖在出厂技能上**(同名 ⇒ 配置源这条赢),`enabled: false` 的不装;两种配置源同一条路径(7.128.0 起;此前本地形整域不生效)。
1202
+ 起服每装上一条一行 `web_admin_skill`(`overrodeBuiltin` 说盖没盖出厂的);拿不到正文的一条一行 `web_admin_skill_failed`(`reason`:`body_absent` / `read_failed`),
1203
+ 这一条不装、同名出厂技能保留、起服照常。重启生效(与 MCP 同:改了之后 `/health` 的 `restart.reasons` 含 `skills`)。
1204
+ 3. **插件带的技能**(plugins 域):只补**没被占的名字**,撞名让位(起服行 `center_plugins_applied` 的 `letGo`)。
1205
+ 4. **请求体自带的技能**(`skills[]`):只补这一 run **没被占的名字** —— 与部署的技能同名的那条被丢(一行 `user_skills_dropped`),不覆盖;
1206
+ `disableSkills: true` ⇒ 这一 run 一只技能都不装(部署的、请求的都不装)。
1207
+
1208
+ **MCP 条目的工具风险轴(`toolAxes`,7.128.0 起)**
1209
+
1210
+ `config.d/mcp.json` 的 `servers[]`(或配置中心下发的同形条目)可带 `toolAxes`,为这台 server 的**每只工具**声明风险轴;声明逐字上引擎的 `McpServerSpec.toolAxes`,喂审批门与计划模式围栏:
1211
+
1212
+ ```json
1213
+ { "name": "crm", "scenarios": [], "transport": { "kind": "stdio", "command": "crm-mcp" },
1214
+ "toolAxes": { "search_accounts": { "effect": "read" }, "send_mail": { "egress": true, "irreversibility": "always" } } }
1215
+ ```
1216
+
1217
+ - **形**:键 = 该 server 的**原始(未加命名空间)**工具名;值 = `{ effect?, egress?, irreversibility? }` —— `effect` 取引擎的 `ToolEffect` 词(`read` / `write` / `idempotent`)、
1218
+ `egress` 是布尔、`irreversibility` 取 `always` / `never`;词表以引擎为准,本服务不另立一份。成员之间能不能并存(例如 `effect:"read"` 与 `egress:true`)由引擎判,本服务只判形。
1219
+ - **坏形整条 server 不挂**:出词表、`egress` 非布尔、未知成员(笔误)、某只工具的值不是对象、整份不是对象、工具名为空 ⇒ 那一台 server 不挂,兄弟 server 照挂,
1220
+ 起服日志一行 `web_admin_mcp_tool_axes_invalid { server, problem, note }`(`problem` 指到工具名与成员名,不带值);它的名字仍被部署占着,请求体里的同名条目补不了位。
1221
+ 一只工具坏,整条的声明都不收。缺席、`{}`、全空条目 = 没声明。
1222
+ - **重启生效**(与 MCP 条目其余部分同:改了之后 `/health` 的 `restart.reasons` 含 `mcp`)。
1223
+ - **请求体自带的 `mcpServers[].toolAxes`**(单用户闸:调用方即信任根)不变;这一条只管部署侧声明。
1224
+
1186
1225
  **可选 — MCP 入站表单(elicitation,E23):默认关**
1187
1226
 
1188
1227
  ```bash
@@ -1641,13 +1680,17 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
1641
1680
  > - **`tool_end.output` 两形**:串,或块数组。块里可有图像 / 文档 `{ type, data, mimeType }` —— 本流上整块逐字(`data` 是字节编码,不过文本脱敏),
1642
1681
  > 一帧最大约为部署媒体预算的 4/3(缺省约 40 MiB),请按块 `data` 长界帧;读得慢的连接(本帧媒体 + 还没送出去的字节 > 64 MiB)上本帧媒体降成描述符。
1643
1682
  > 媒体块 `data` 的内容由引擎准入负责:引擎本版只对自己产出的块验字节(读图像 / PDF、提交的图像、用户附件),MCP 服务器与宿主自定义工具交回的块不验(引擎下一 minor 收);本服务不在 wire 上对 `data` 做任何改写。
1644
- > 账本面只存描述符 —— 账本行与 `GET /v1/runs/:id/events`(断线重连面;对 `POST /v1/runs` 提交的异步 run 它**就是活流**,所以这类客户端本版拿不到整图,整图只在本流与子代 tail 流上)
1645
- > `{ type, mimeType?, bytes, truncated: true }`(帧级 `truncated: true` 与 `totalChars` = 原值真实大小 —— 文本块字符数 + 媒体块 `data` 串长,引擎已截断的帧保留引擎给的值);无 `data` 且 `truncated: true` 的块渲染成「未送达」。
1683
+ > 账本面不存媒体字节 —— 账本行与 `GET /v1/runs/:id/events`(断线重连面;对 `POST /v1/runs` 提交的异步 run 它**就是活流**,所以这类客户端拿不到整图,整图只在本流、子代 tail 流与会话存档 `GET /v1/sessions/:id` 上)
1684
+ > 里,带 `data` 的媒体块换成 `{ type, mimeType?, bytes, truncated: true }`(帧级 `truncated: true` 与 `totalChars` = 原值真实大小 —— 文本块字符数 + 媒体块 `data` 串长,引擎已截断的帧保留引擎给的值);无 `data` 且 `truncated: true` 的块渲染成「未送达」。
1685
+ > **同一条律也管 `tool_end.structured`**:Read 读图像 / PDF 的卡 `{ type: "image" | "document", file: { base64?, type, originalSize, … } }` 里 `file.base64` 是同一份字节的第二份编码(引擎只在 ≤48K 字符时随卡),
1686
+ > 本流上同样逐字(不过文本脱敏,卡上其余成员照常脱敏),积压判据把它与 `output` 块的字节同一帧一起计、降级时一并去掉;账本面(账本行与 `/events`、`GET /v1/tasks/:id/turns`、`GET /v1/tasks/:id/stream` 的回放)里卡上**没有** `file.base64`
1687
+ > (与引擎对超 48K 的卡自己略去该键的形一致,`originalSize` / `type` 仍在)。7.127.0 / 7.127.1 上这份字节(≤48K 字符)仍随账本行落库并回放,7.127.2 起不带。
1688
+ > 会话存档读面同律:`GET /v1/sessions/:id` 的 `messages[]` 里,工具结果消息的 `details` 就是这张卡 —— 媒体卡的 `file.base64` 逐字(不去键、不过文本脱敏),卡上其余成员与非媒体卡照常脱敏(7.128.0 起;此前这一位也被文本脱敏改写)。
1646
1689
 
1647
1690
  ## 7. 其它接口(按需)
1648
1691
  | 端点 | 用途 |
1649
1692
  |---|---|
1650
- | `GET /v1/sessions/<id>` | 审计回溯:当前上下文 + 摘要(owner 校验);消息里模型 / 工具产出的文本与 live 帧、账本帧同一只脱敏(密钥形换占位,其余字节不变) |
1693
+ | `GET /v1/sessions/<id>` | 审计回溯:当前上下文 + 摘要(owner 校验);消息里模型 / 工具产出的文本与 live 帧、账本帧同一只脱敏(密钥形换占位,其余字节不变);Read 图像 / PDF 的媒体卡(工具结果消息的 `details`)的 `file.base64` 是字节编码,逐字 |
1651
1694
  | `GET /v1/approvals?owner=user:42` | 高危写审批(durable,需 `DURABLE_APPROVAL=true`):**operator** 看待办队列(可按 owner 过滤);非 operator 只看自己的 |
1652
1695
  | `POST /v1/approvals/<sessionId>/decide` `{"decision":"approve"|"deny","reason":"…"}` | 批/否并恢复挂起任务(CAS,重复决议 409);**仅 operator**(非 operator → 403)。5.0.0 起旧轮询腿 `POST /v1/approvals/<id>` 已退役 |
1653
1696
  | `GET /v1/agents/roster` | 后台 agent **名册**(7.29 起,需 durable background-agent store,否则 501 `capability.background_agent_store_required`):调用方自己 scope 的 `a…` 行,新→旧。行只带 `handle`/`name`/`agentType`/`status`/`spawnedAt`/`updatedAt`/`settledAt`(content-free;子代产物正文走 `GET /v1/runs/<id>/subagents/<handle>/output`)。查询:`?limit=`(1..100,默认 20)、`?before=`(上一页的 `nextBefore` 游标)、`?status=`(running\|parked\|completed\|failed\|killed)、`?session=`(按会话树窄化)、`?scope=`(**仅** service-token/单用户形;带 principal 的调用方锁定自己)。回体 `{agents,total,nextBefore?,truncated?}`;`truncated:true` = 租户行数超过名册窗(500),`total` 此时是窗内数。非属主的行**不出现也不计数**(无存在性 oracle) |
@@ -1781,6 +1824,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
1781
1824
  > - **自动整理**(任务结束按建议起跑)用这条腿自己的判官 —— 该 run 主体的人的持久路径规则 + 部署的调用方策略,与模型自己的 `Write` 同一套判:
1782
1825
  > 规则点名库目录 ⇒ 在任何占位与模型调用之前拒,运维行 `engine_notice` 一条 `memory.consolidation_refused`(`detail.refusalCode: "memory.consolidation_write_refused"`);
1783
1826
  > 只点名产物文件 ⇒ 应用时拒、什么都不写,一条 `memory.consolidation_incomplete`(`stop: "refused"`);用户任务不受影响。没有主体的 run 读不到任何人的持久规则(只剩部署策略那一层)。
1827
+ > ⚠️ **引擎 7.50.x 上这条腿在缺省部署上每一次都被拒**:只要部署的敏感路径守卫在场(`SENSITIVE_WRITE_PATTERNS` 未设即在场),引擎问写判官时递给部署策略的是一只已中止的信号,守卫判不了库目录的真实落点、按 `target_unresolved` 拒 —— 与规则、权限模式无关(`memory.consolidation_refused`,人读句只带拒因前 200 字)。修复随引擎提货;要整理用手动阀门,不要为此设 `SENSITIVE_WRITE_PATTERNS=off`(见 MIGRATION 7.127.2 第 3 条)。
1784
1828
  > - **手动阀门**(`POST /v1/admin/memory/consolidation/run`)没有腿,拿不到引擎的那只判官;本服务不另造一只 ⇒ 它的写判官答「无异议」(与 7.126.0 该口行为逐字同),
1785
1829
  > 每次被问记一次 fail-open 计数 `server.memory-consolidation.operator-valve-unjudged`(计数 + 探针行 + 一次性告警)。驱动的每一条通告 —— 非收敛停的 `memory.consolidation_incomplete`、
1786
1830
  > 问判官**之前**推不出库家落点时同码拒的 `memory.consolidation_refused`(生产上可达,与判官答什么无关)—— 都经同一只 `engine_notice` 转发器进运维行;早问拒仍按路由的码臂答 `409 memory.consolidation_refused`。
@@ -2432,6 +2476,12 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
2432
2476
  - 置空(名单外 / 凭据名)时日志一行 `hook_http_header_env_blanked`(名字与原因,不带值)。
2433
2477
  例:`"headers": { "Authorization": "Bearer ${HOOK_TOKEN}" }`、`"allowedEnvVars": ["HOOK_TOKEN"]`,并在请求 `settings.env` 里给 `HOOK_TOKEN`。7.125.x 及更早的读法不同(升级见 MIGRATION)。
2434
2478
 
2479
+ ### 9.5d MCP 服务器 http 传输的身份头:`principalHeader` 与 `assertionHeader` 二选一(请求体与部署声明同一种读法)
2480
+
2481
+ `mcpServers[].transport`(`kind:"http"`)上有两个「用哪个头送身份」的可选键,**只能设一个**:`principalHeader`(runner 把该任务已认证的终端用户 principal 注入这个头)与 `assertionHeader`(托管连接器断言所用的头**名**)。
2482
+ - 请求体(`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs`)与部署声明(中心下发 / 本地 `config.d/mcp.json`)判据相同:两键同设 ⇒ 请求体新提交腿 `400 request.body_shape`(拒句点名条目与两键,与契约包解析期的拒句逐字相同);`assertionHeader` 不是字符串 ⇒ `400 request.field_invalid`;部署声明里同设的那一条不进。耐久 resume 腿重放存量请求体时不拒、也不摘 server:server 照挂,`principalHeader` 胜、`assertionHeader` 不生效,一行 warn `mcp_assertion_header_ignored`(`server` / `reason` / `note`)。
2483
+ - 本版只承载、不发送断言:声明了 `assertionHeader` 的 server 照常挂载,但不带断言头;每台这样的 server 一行 info `mcp_assertion_header_not_sent`(`server` / `note`)。探测口 `POST /v1/capabilities/mcp/probe` 对声明了该键的条目整条拒 `400 request.field_invalid`(探测不拨一份少了断言头的配置)。
2484
+
2435
2485
  ### 9.6 trace 脱敏的射程:与出站请求体无关
2436
2486
 
2437
2487
  🔴 **脱敏 ≠ 出站控制**:trace 脱敏(账本 / SSE / turns / 审批预览里的 `«redacted»` 标记)只作用于
package/dist/audit.js CHANGED
@@ -4,6 +4,7 @@ import { PgSessionStorage } from "./plugins/pg-session-storage.js";
4
4
  import { createLocalSessionRepo } from "./plugins/local-session-store.js";
5
5
  import { redactModelText } from "./observability/run-terminal.js";
6
6
  import { redactDeepWith } from "./trace/redact.js";
7
+ import { projectToolStructured } from "./trace/tool-output-media.js";
7
8
  import { INJECTED_BLOCK_RE } from "./user-authored-text.js";
8
9
  export function buildRewindHandles(anchors, reader) {
9
10
  const byEntry = new Map();
@@ -79,10 +80,12 @@ function withRewindHandles(messages, entries, handles) {
79
80
  }
80
81
  const deepScrub = (v) => redactDeepWith(v, redactModelText);
81
82
  const textSlot = (v) => (typeof v === "string" ? redactModelText(v) : deepScrub(v));
83
+ const readToolCard = (v) => projectToolStructured(v, deepScrub);
82
84
  function readLeaf(treatment, v) {
83
85
  switch (treatment) {
84
86
  case "text": return textSlot(v);
85
87
  case "structure": return deepScrub(v);
88
+ case "toolCard": return readToolCard(v);
86
89
  case "parts": return readParts(v);
87
90
  case "verbatim": return v;
88
91
  default: {
@@ -140,7 +143,7 @@ const MESSAGE_READ_TREATMENT = {
140
143
  providerCondition: "verbatim", providerConditionSource: "verbatim", providerResetAtMs: "verbatim", apiErrorRoute: "verbatim",
141
144
  errorBodyIncomplete: "verbatim", modelOutputInvalid: "verbatim", finishReasonUnknown: "verbatim", streamEndIgnored: "verbatim",
142
145
  },
143
- toolResult: { role: "verbatim", toolCallId: "verbatim", toolName: "verbatim", content: "parts", details: "structure", isError: "verbatim", timestamp: "verbatim" },
146
+ toolResult: { role: "verbatim", toolCallId: "verbatim", toolName: "verbatim", content: "parts", details: "toolCard", isError: "verbatim", timestamp: "verbatim" },
144
147
  bashExecution: {
145
148
  role: "verbatim", command: "text", output: "text", exitCode: "verbatim", cancelled: "verbatim", truncated: "verbatim",
146
149
  fullOutputPath: "verbatim", timestamp: "verbatim", excludeFromContext: "verbatim",
@@ -17,6 +17,17 @@ export interface ExecutionLaneComposeCtx {
17
17
  }
18
18
  /** 合成 + 判。覆写就地改写 `config.remoteExec`;坏文件 / 不变量不过 ⇒ 抛(拒启)。 */
19
19
  export declare function composeExecutionLane(config: ServiceConfig, ctx: ExecutionLaneComposeCtx): Promise<void>;
20
+ /**
21
+ * **宿主车道的 shell 起服解析**(一次)—— 宿主车道(`provider === "host"`)的命令用本机 shell 起(win32 = Git Bash),起服时把它解析落定,
22
+ * 找不到就在这里响亮地说一次(`warn host_shell_unavailable { err, remedy }`),而不是等第一个用户的第一条 Bash 撞上。
23
+ * 一条律:**shell 只属于 spawn,每一处 spawn 都在解析落定之后;解析器的状态转换是唯一的运维信号源** —— 本步只是把观察者装上并让第一次
24
+ * 转换提前发生,各条 spawn 腿(四条 exec + 两条 hook)自己仍先 await 解析,不依赖本步先跑(细节与行名见 `plugins/host-platform.ts`)。
25
+ * 失败不挡起服:宿主车道的其它工具照常,Bash 到 spawn 时以真因拒。
26
+ *
27
+ * 两个入口调同一只(与 {@link composeExecutionLane} 同形):HTTP 服务在第 5 段(执行环境装配)之后,`run-local` 在宿主车道定下之后。
28
+ * 非宿主车道零动作 —— 不解析、不装观察者。
29
+ */
30
+ export declare function settleHostLaneShell(config: Pick<ServiceConfig, "remoteExec">, logger: Pick<Logger, "info" | "warn">): Promise<void>;
20
31
  /**
21
32
  * 车道合成之后的跨域不变量 —— 表(`config-invariants.ts`)里读 `remoteExec.provider` 的那几条,boot 期 `live === next`。
22
33
  * 同步、纯判据:只读 config,不碰文件(测试可以对 `loadConfig()` 的产物直接调它)。
@@ -1,12 +1,18 @@
1
1
  import { loadRemoteExec } from "@sema-agent/settings-schema/node";
2
2
  import { crossInvariant } from "../config-invariants.js";
3
3
  import { executionLaneOf, laneIsolated } from "../execution-lane-caps.js";
4
+ import { settleHostShellAtBoot } from "../plugins/host-platform.js";
4
5
  export const FILE_SOURCED_LANES = ["host", "local-docker"];
5
6
  export async function composeExecutionLane(config, ctx) {
6
7
  if (ctx.configSource === "local")
7
8
  await applyRemoteExecFile(config, ctx);
8
9
  assertExecutionLaneInvariants(config);
9
10
  }
11
+ export async function settleHostLaneShell(config, logger) {
12
+ if (config.remoteExec?.provider !== "host")
13
+ return;
14
+ await settleHostShellAtBoot(logger);
15
+ }
10
16
  export function assertExecutionLaneInvariants(config) {
11
17
  const judge = (id) => crossInvariant(id).check({ live: config, next: config });
12
18
  const leader = judge("leader-host-lane");
@@ -35,7 +35,7 @@ import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, norma
35
35
  import { isAbsoluteHostPath } from "@sema-agent/settings-schema";
36
36
  import { cwdHonored, deviceCwdHonored, effectiveHostWorkspace, inProcessSingleUserLane, parseAdditionalDirectories, satisfiedByProcessCwd, shellEnvMismatchCount } from "../task-cwd.js";
37
37
  import { assertRequestA2aUnlocked, resolveRequestA2a } from "../task-a2a.js";
38
- import { assertRequestMcpContentOrigin, assertRequestMcpUnlocked, resolveRequestMcp } from "../task-mcp.js";
38
+ import { assertRequestMcpContentOrigin, assertRequestMcpIdentityHeaders, assertRequestMcpUnlocked, resolveRequestMcp } from "../task-mcp.js";
39
39
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, acceptAppendSystemPrompt, applyTaskSettings, effectivePermissionMode, effectiveThinking, hasConstitutionAnchors, parseTaskSettings, taskHooksHonored, providerDropsAppend, settingsPermissionBucketIssue, withPermissionMode } from "../task-settings.js";
40
40
  import { skillDenyInertEntryOf } from "../rule-entry-list.js";
41
41
  import { TENANCY_WALL_CLOCK_BASE_SEC, enableForkFromBody, normalizeRetainSubagentSessions, selfOrchestrationFromBody } from "../task-workflow.js";
@@ -70,6 +70,8 @@ export function createResolveSpec(ctx) {
70
70
  assertRequestMcpUnlocked(body.mcpServers, lockedKeys);
71
71
  if (opts?.leg === "fresh")
72
72
  assertRequestMcpContentOrigin(body.mcpServers);
73
+ if (opts?.leg === "fresh")
74
+ assertRequestMcpIdentityHeaders(body.mcpServers);
73
75
  if (opts?.leg === "fresh") {
74
76
  const conflict = scratchpadCoveringTaskRoot({
75
77
  cwd: body.cwd,
@@ -19,7 +19,7 @@ import { centerManagedConfigKeys } from "../web-admin-client/apply-effective.js"
19
19
  import { createConfigProvider, raceBootFetch, serviceConfigSourceKind, BOOT_FETCH_DEFERRED } from "../config-provider.js";
20
20
  import { createKeyResolver } from "../key-resolver.js";
21
21
  import { buildSealedKeyPublicFace, ensureSealedKeyStore, reportExecutionPublicKey } from "../sealed-key.js";
22
- import { applyEffective, applyCenterReadFace, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, resolveA2aPeers, restartReasons, modelPlaneChanged, withModelPlaneOf, fullyLandedForLkg, fetchPromptArtifact } from "../web-admin-client/facade.js";
22
+ import { applyEffective, applyCenterReadFace, mutateInPlace, logEffectiveDiff, applySkillsManifest, resolveMcpServers, resolveA2aPeers, restartReasons, modelPlaneChanged, withModelPlaneOf, fullyLandedForLkg, fetchPromptArtifact } from "../web-admin-client/facade.js";
23
23
  import { buildConfigHandoff, buildRestartSignal, configHandoffMissing } from "../fleet/engine-signal.js";
24
24
  import { createMcpRevocationLedger, enabledMcpEntries } from "../web-admin-client/mcp-revocation.js";
25
25
  import { buildUnreadConfigFileFields } from "../web-admin-client/read-warnings.js";
@@ -133,6 +133,7 @@ export async function createWebAdminRuntime(ctx) {
133
133
  ...(config.configProvider ? { provider: config.configProvider } : {}),
134
134
  ...(cc ? { webAdmin: { baseUrl: cc.baseUrl, credential: cc.credential, worker: cc.worker, dryRun: cc.dryRun } } : {}),
135
135
  localDir: localRoot,
136
+ ...(config.configLkgEnabled ? { skillBodyCacheDir: defaultSkillCacheDir(config) } : {}),
136
137
  })
137
138
  : undefined;
138
139
  const localDirField = configProvider?.kind === "local" ? { dir: configProvider.root } : {};
@@ -153,16 +154,16 @@ export async function createWebAdminRuntime(ctx) {
153
154
  });
154
155
  }
155
156
  };
156
- const rejectionSource = () => {
157
+ const candidateSource = () => {
157
158
  if (configProvider === undefined)
158
- throw new Error("config candidate judged without a config provider (no pipeline ⇒ nothing was pulled)");
159
- return configProvider.kind;
159
+ throw new Error("config candidate handled without a config provider (no pipeline ⇒ nothing was pulled)");
160
+ return configProvider;
160
161
  };
161
162
  const announceCandidateRejected = (r, verdict, leg, ordinal) => {
162
163
  const domains = verdict.domains.join(",");
163
164
  if (candidateRejection?.version === r.effective.version && candidateRejection.domains.join(",") === domains)
164
165
  return;
165
- noteCandidateVerdict(ordinal, candidateRejectionOf(r, verdict, rejectionSource(), Date.now()));
166
+ noteCandidateVerdict(ordinal, candidateRejectionOf(r, verdict, candidateSource().kind, Date.now()));
166
167
  logger.warn("config_candidate_rejected", buildCandidateRejectedFields(r, verdict, leg, { lkgVersion: latestEffective?.version ?? "(env)", ...localDirField }));
167
168
  };
168
169
  const selfReportListeners = [];
@@ -372,11 +373,15 @@ export async function createWebAdminRuntime(ctx) {
372
373
  }
373
374
  const lkgPath = lkgPathFor(identity);
374
375
  try {
375
- if (effectiveObj.skills && config.webAdmin) {
376
- await applyCenterSkills([], effectiveObj.skills, config.webAdmin, logger, undefined, defaultSkillCacheDir(config));
376
+ const source = candidateSource();
377
+ const cacheDir = source.skillBodyCacheDir;
378
+ if (cacheDir === undefined) {
379
+ logger.warn("config_lkg_save_failed", { err: "the config source declares no skill body cache", note: "LKG NOT written (skill bodies on disk are part of the handoff contract and this source keeps none) — handoff NOT confirmed" });
380
+ return false;
377
381
  }
382
+ if (effectiveObj.skills)
383
+ await applySkillsManifest([], effectiveObj.skills, source, logger);
378
384
  const enabledSkills = (effectiveObj.skills?.skills ?? []).filter((m) => m.enabled !== false);
379
- const cacheDir = defaultSkillCacheDir(config);
380
385
  for (const m of enabledSkills) {
381
386
  const hex = m.contentHash.replace(/^sha256:/, "").toLowerCase();
382
387
  if (!SHA256_HEX_RE.test(hex))
@@ -692,9 +697,8 @@ export async function createWebAdminRuntime(ctx) {
692
697
  return (await manualRefresh()) === "failed" ? "failed" : "triggered";
693
698
  },
694
699
  async applyCenterCapabilities(skills) {
695
- if (effective?.skills && config.webAdmin) {
696
- skills = await applyCenterSkills(skills, effective.skills, config.webAdmin, logger, undefined, config.configLkgEnabled ? defaultSkillCacheDir(config) : undefined);
697
- }
700
+ if (effective?.skills)
701
+ skills = await applySkillsManifest(skills, effective.skills, candidateSource(), logger);
698
702
  if (effective?.plugins !== undefined) {
699
703
  const pluginOut = await applyCenterPlugins(skills, effective, {
700
704
  cacheRoot: join(config.localDataRoot ?? join(homedir(), ".ai-agent"), "plugin-cache"),
@@ -4,7 +4,8 @@ import { createFixedWindowRateLimiter } from "../device-enrollment.js";
4
4
  import { canonicalJson } from "../canonical-json.js";
5
5
  import { echoKeyName, keyClosureClause, scanKeyClosure } from "../request-key-closure.js";
6
6
  import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "../mcp-content-origin.js";
7
- import { HTTP_MCP_TRANSPORT_KEYS, MAX_REQUEST_MCP_SERVERS, MCP_ENTRY_EMPTY_ARRAY_KEYS, MCP_ENTRY_EMPTY_RECORD_KEYS, MCP_SERVER_ENTRY_KEYS, MCP_TRANSPORT_EMPTY_ARRAY_KEYS, MCP_TRANSPORT_EMPTY_RECORD_KEYS, STDIO_MCP_TRANSPORT_KEYS, validateRequestMcp, } from "../task-mcp.js";
7
+ import { HTTP_MCP_TRANSPORT_KEYS, MAX_REQUEST_MCP_SERVERS, MCP_ENTRY_EMPTY_ARRAY_KEYS, MCP_ENTRY_EMPTY_RECORD_KEYS, MCP_SERVER_ENTRY_KEYS, MCP_TRANSPORT_EMPTY_ARRAY_KEYS, MCP_TRANSPORT_EMPTY_RECORD_KEYS, STDIO_MCP_TRANSPORT_KEYS, requestEntryIdentityRefusal, validateRequestMcp, } from "../task-mcp.js";
8
+ import { toCoreMcpServerSpec } from "../mcp-assertion-header.js";
8
9
  import { buildDisplaySafeUrl } from "../trace/redact.js";
9
10
  import { projectWiringManifestMcpRows } from "../trace/project.js";
10
11
  export const MCP_PROBE_REFUSAL_CODES = [
@@ -51,8 +52,12 @@ function entryKeyIssue(entry, at) {
51
52
  if (keys !== undefined)
52
53
  scanKeyClosure(t, `${at}.transport.`, new Set(keys), NO_TOLERATED_KEYS, buckets);
53
54
  }
54
- if (buckets.unknown.length === 0)
55
- return null;
55
+ if (buckets.unknown.length === 0) {
56
+ const refusal = requestEntryIdentityRefusal(entry);
57
+ if (refusal === undefined)
58
+ return null;
59
+ return refusal.errorCode === "request.body_shape" ? shapeRefusal(refusal.message) : fieldRefusal(refusal.message);
60
+ }
56
61
  buckets.unknown.sort();
57
62
  return shapeRefusal(`body: unrecognized key(s) on an MCP declaration — refused rather than dropped, because a dropped key reads as "in effect" to the caller. ` +
58
63
  `${keyClosureClause(buckets.unknown, "unknown (not an mcpServers[] key — check the spelling)")}. ` +
@@ -71,7 +76,8 @@ function entryValueIssue(entry, at) {
71
76
  if (read.badContentOrigin.length > 0) {
72
77
  return fieldRefusal(`${at}.contentOrigin must be one of ${CONTENT_ORIGIN_WORDS.map((w) => `"${w}"`).join(" | ")} — got ${describeBadContentOrigin(entry.contentOrigin)} (omit the key for the default: this server's tools classify as external content)`);
73
78
  }
74
- return declarationDowngradeIssue(entry, read.ok[0], at);
79
+ const read0 = read.ok[0];
80
+ return declarationDowngradeIssue(entry, read0 === undefined ? undefined : toCoreMcpServerSpec(read0), at);
75
81
  }
76
82
  function declarationDowngradeIssue(entry, spec, at) {
77
83
  if (spec === undefined)
@@ -171,7 +177,7 @@ export function readMcpProbeBody(raw) {
171
177
  " so nothing in your body needs changing — if you see it, this deployment has a bug worth reporting"),
172
178
  };
173
179
  }
174
- return { ok: true, specs: validated.ok };
180
+ return { ok: true, specs: validated.ok.map(toCoreMcpServerSpec) };
175
181
  }
176
182
  export function buildMcpProbeIdentity(principal) {
177
183
  return principal !== undefined && principal.length > 0 ? principal : "local";
@@ -432,7 +432,7 @@ export const CONFIG_CATALOG = [
432
432
  r("SESSION_SNAPSHOT_PREFIX", "store", "string", "快照 blob key 前缀", { derivedDefaultNote: "缺席 ⇒ blobs/(MinioBlobBackend 默认)" }),
433
433
  r("SESSION_SNAPSHOT_TTL_SEC", "store", "number", "快照 presign TTL(秒;坏值省略走默认,绝不 NaN 签名)", { derivedDefaultNote: "缺席 ⇒ 3600(MinioBlobBackend 默认)" }),
434
434
  r("SIGHUP_IDLE_GRACE_MS", "limitsHttp", "number", "SIGHUP 空闲关停窗(ms;共享引擎形防误杀)", { staticDefault: "120000", configKey: "sighupIdleGraceMs" }),
435
- r("SKILLS_DIR", "integrations", "path", "出厂 skill 目录", { derivedDefaultNote: "缺席 ⇒ 包内 skills/(相对模块解析,不是 cwd)", configKey: "skillsDir" }),
435
+ r("SKILLS_DIR", "integrations", "path", "出厂 skill 目录(设了就整个替换包内 skills/,不叠加;配置源的技能再按名盖上)", { derivedDefaultNote: "缺席 ⇒ 包内 skills/(相对模块解析,不是 cwd)", configKey: "skillsDir" }),
436
436
  r("SNAPSHOT_BLOB_ALLOW_SQL_BYTES", "store", "boolean", "云形缺 MinIO 时显式接受 SQL 店放快照字节(严格 'true';安全姿势开关不收宽松真值)", { staticDefault: "false", danger: DATA, configKey: "snapshotBlobAllowSql" }),
437
437
  r("SNAPSHOT_BLOB_SQL_MAX_BYTES", "store", "number", "SQL 店单 blob 帽(字节)", { derivedDefaultNote: "缺席:mysql 引擎 ⇒ 内建默认帽;pg ⇒ 不设", configKey: "snapshotBlobSqlMaxBytes" }),
438
438
  r("SSH_HOST", "orchestration", "string", "ssh 车道目标主机(必需件)"),
@@ -78,8 +78,9 @@ export declare function raceBootFetch<T>(fetch: Promise<T>, budgetMs: number): P
78
78
  /**
79
79
  * The two transport calls the service makes to obtain config, behind a backend-selectable seam. Both
80
80
  * signatures and return shapes match `facade.fetchEffective` / `facade.fetchSkillContent`
81
- * EXACTLY so that, ONCE WIRED, the caller (main.ts) could swap remote↔local without touching applyEffective
82
- * et al. (main.ts does not swap on this seam yet — see the NOT-YET-WIRED note at the top of this module).
81
+ * EXACTLY, so every consumer (boot / refresh / late boot in `boot/web-admin.ts`, and `run-local`) runs one code
82
+ * path whichever backend answers: candidates through `fetchEffective`, skill bodies through `fetchSkillContent`
83
+ * (the skills read port `applySkillsManifest`), the body cache wherever the backend declares it.
83
84
  */
84
85
  export interface ConfigProvider {
85
86
  /** "remote" = web-admin HTTP; "local" = on-disk FileConfigStore. */
@@ -92,11 +93,18 @@ export interface ConfigProvider {
92
93
  fetchEffective(etag: string | undefined): Promise<FetchEffectiveResult>;
93
94
  /**
94
95
  * Fetch one skill body by content hash (`sha256:<hex>`). Remote: GET the content endpoint + verify the
95
- * hash. Local: content-addressed lookup over the local skills domain. Returns `undefined` when the hash
96
+ * hash. Local: content-addressed lookup over the skills of the candidate this provider last served (the
97
+ * same read that produced the manifest — S-1556). Returns `undefined` when the hash
96
98
  * is unknown (remote 404 / local miss) so the caller keeps its baseline; throws only on a hard error
97
99
  * (remote non-404 HTTP / hash mismatch).
98
100
  */
99
101
  fetchSkillContent(contentHash: string): Promise<string | undefined>;
102
+ /**
103
+ * S-1556:这只配置源给技能正文留的**盘缓存目录**(content-addressed;`applySkillsManifest` 的读序 内存 → 盘 → 本配置源,读到即回写)。
104
+ * 远程:开着 LKG(`CONFIG_LKG_ENABLED`)时 = 配置根下的 `skill-cache` —— 中心不可达时的 LKG 起服不卡在正文拉取上,LKG 落盘的正文完整性门
105
+ * 也验这同一个目录;关着 = 缺席(不落盘)。本地:恒缺席 —— 正文就内联在本机盘上的 config.d 里,再拷一份是第二份真源。
106
+ */
107
+ readonly skillBodyCacheDir: string | undefined;
100
108
  }
101
109
  /** The config-provider knobs read off ServiceConfig (a structural subset — avoids importing the whole
102
110
  * ServiceConfig type and keeps this module testable with a plain object). */
@@ -117,6 +125,8 @@ export interface ConfigProviderInput {
117
125
  * `process.cwd()`,而工厂只在 `CONFIG_PROVIDER === "local"` 字面上递它 ⇒ 另一条走到 local 的路上配置源读 cwd、
118
126
  * 合成步读 localRoot,两个根分家。一个远程源也收它:`remote` 而无中心 URL 的形照样落回 local。 */
119
127
  localDir: string;
128
+ /** S-1556:远程配置源的技能正文盘缓存目录(见 {@link ConfigProvider.skillBodyCacheDir});本地配置源不认它(正文本就在盘上)。缺席 = 不落盘。 */
129
+ skillBodyCacheDir?: string;
120
130
  }
121
131
  /**
122
132
  * RemoteConfigProvider — DELEGATES to the exported web-admin HTTP functions. No HTTP is reimplemented;
@@ -127,13 +137,14 @@ export interface ConfigProviderInput {
127
137
  export declare class RemoteConfigProvider implements ConfigProvider {
128
138
  private readonly cc;
129
139
  private readonly deps;
140
+ readonly skillBodyCacheDir: string | undefined;
130
141
  readonly kind: "remote";
131
142
  constructor(cc: CenterEndpoint & {
132
143
  worker?: string;
133
144
  }, deps?: {
134
145
  fetchEffective?: typeof remoteFetchEffective;
135
146
  fetchSkillContent?: typeof remoteFetchSkillContent;
136
- });
147
+ }, skillBodyCacheDir?: string | undefined);
137
148
  fetchEffective(etag: string | undefined): Promise<FetchEffectiveResult>;
138
149
  fetchSkillContent(contentHash: string): Promise<string | undefined>;
139
150
  }
@@ -146,6 +157,8 @@ export declare class RemoteConfigProvider implements ConfigProvider {
146
157
  export declare class LocalConfigProvider implements ConfigProvider {
147
158
  #private;
148
159
  readonly kind: "local";
160
+ /** 本地配置源不留正文盘缓存:正文就内联在 config.d 里(同一块盘),缓存它是第二份真源。 */
161
+ readonly skillBodyCacheDir: undefined;
149
162
  private readonly store;
150
163
  /** [ref]b 可观测性:config_loaded 日志要能回答「到底读的哪个目录」——复验实录:只设 LOCAL_DATA_ROOT 时
151
164
  * config.d 静默读不到且 0 模型无告警(CONFIG_LOCAL_DIR 才是本 lane 的选目录旋钮)。 */
@@ -38,10 +38,12 @@ export function raceBootFetch(fetch, budgetMs) {
38
38
  export class RemoteConfigProvider {
39
39
  cc;
40
40
  deps;
41
+ skillBodyCacheDir;
41
42
  kind = "remote";
42
- constructor(cc, deps = {}) {
43
+ constructor(cc, deps = {}, skillBodyCacheDir = undefined) {
43
44
  this.cc = cc;
44
45
  this.deps = deps;
46
+ this.skillBodyCacheDir = skillBodyCacheDir;
45
47
  }
46
48
  async fetchEffective(etag) {
47
49
  const fn = this.deps.fetchEffective ?? remoteFetchEffective;
@@ -68,6 +70,8 @@ export class RemoteConfigProvider {
68
70
  }
69
71
  export class LocalConfigProvider {
70
72
  kind = "local";
73
+ skillBodyCacheDir = undefined;
74
+ #servedSkills = { skills: [] };
71
75
  store;
72
76
  root;
73
77
  #injectedStore;
@@ -115,6 +119,7 @@ export class LocalConfigProvider {
115
119
  const parseErrors = folded.domainErrors.map((w) => ({ domain: w.domain, error: redactConfigError(w.error) }));
116
120
  const droppedEntries = folded.droppedEntries.map(redactDroppedEntry);
117
121
  const errors = [...domainErrors.map((de) => ({ domain: de.domain, error: redactConfigError(de.error) })), ...parseErrors, ...refErrors];
122
+ this.#servedSkills = eff.skills;
118
123
  return {
119
124
  effective: withDroppedCatalogEntries(mapToServiceEffective(eff, eff.version), folded.droppedCatalogEntries),
120
125
  etag: tag,
@@ -126,10 +131,7 @@ export class LocalConfigProvider {
126
131
  };
127
132
  }
128
133
  async fetchSkillContent(contentHash) {
129
- const skills = await this.store.getDomain("skills");
130
- if (!skills)
131
- return undefined;
132
- return findSkillContent(skills, contentHash);
134
+ return findSkillContent(this.#servedSkills, contentHash);
133
135
  }
134
136
  }
135
137
  export function mapToServiceEffective(eff, version) {
@@ -234,7 +236,7 @@ export function mapToServiceEffective(eff, version) {
234
236
  }
235
237
  export function createConfigProvider(input) {
236
238
  return configProviderKind(input) === "remote" && input.webAdmin !== undefined
237
- ? new RemoteConfigProvider(input.webAdmin)
239
+ ? new RemoteConfigProvider(input.webAdmin, {}, input.skillBodyCacheDir)
238
240
  : new LocalConfigProvider(input.localDir);
239
241
  }
240
242
  export function configLocalRoot(config) {
@@ -274,10 +274,10 @@ export interface ScopedMcpServer {
274
274
  spec: McpServerSpec;
275
275
  }
276
276
  /** The deployment's MCP declaration, resolved (`resolveMcpServers`). `servers` = what actually MOUNTED (env-NAME refs
277
- * resolved, `enabled:false` / missing-ref / malformed-`toolFaces` entries skipped, each with its own loud warn);
277
+ * resolved, `enabled:false` / missing-ref / malformed-`toolFaces` / malformed-`toolAxes` entries skipped, each with its own loud warn);
278
278
  * `reservedNames` = EVERY name the deployment declared, mounted or not. A name the deployment declared is CLOSED to
279
279
  * request-side injection (`resolveRequestMcp`) whether or not it mounted — otherwise "skipped" would silently mean
280
- * "free for the caller to refill without the operator's `toolFaces` / `allowTools`" (车CF codex r2 [high]). */
280
+ * "free for the caller to refill without the operator's `toolFaces` / `toolAxes` / `allowTools`" (车CF codex r2 [high]). */
281
281
  export interface CenterMcp {
282
282
  servers: ScopedMcpServer[];
283
283
  reservedNames: readonly string[];
@@ -2,6 +2,7 @@ import { HooksConfig, type HookEventName } from "@sema-agent/settings-schema/hoo
2
2
  import type { Hooks } from "@sema-agent/core";
3
3
  import type { Logger } from "../observability/logger.js";
4
4
  import type { HookNoticeObservation } from "../fleet/fleet-bus.js";
5
+ /** 服务侧防线上限(契约本身不设量纲;不设界=单请求可塞任意大配置/任意长命令)。超界=校验错(fail-loud)。 */
5
6
  export declare const MAX_HOOK_ENTRIES_PER_EVENT = 32;
6
7
  export declare const MAX_HOOK_COMMAND_CHARS = 8192;
7
8
  /** 单条 hook 超时上限(秒;条目可自设 timeout,但被此值夹住——一个 86400 的 timeout 会挂死工具门)。 */
@@ -9,7 +9,6 @@ import { redactSecrets, buildDisplaySafeUrl } from "../trace/redact.js";
9
9
  import { ccPromptSystemFor, wrapCondition, parseCcVerdict, CC_EVALUATOR_MAX_OUTPUT_TOKENS } from "./cc-stop-prompt.js";
10
10
  import { ccAgentHookSystemFor } from "./cc-agent-hook-prompt.js";
11
11
  import { renderBranchTranscript } from "./branch-transcript.js";
12
- void resolveHostShell().catch(() => undefined);
13
12
  export const MAX_HOOK_ENTRIES_PER_EVENT = 32;
14
13
  export const MAX_HOOK_COMMAND_CHARS = 8_192;
15
14
  export const MAX_HOOK_TIMEOUT_SECONDS = 600;
@@ -391,7 +390,13 @@ function buildHookContextField(parts) {
391
390
  const text = buildHookContext(parts);
392
391
  return text === undefined ? {} : { additionalContext: text };
393
392
  }
394
- function runCommandHook(entry, payload, ctx) {
393
+ async function runCommandHook(entry, payload, ctx) {
394
+ try {
395
+ await resolveHostShell();
396
+ }
397
+ catch (e) {
398
+ return { code: null, stdout: "", stderr: "", timedOut: false, spawnError: String(e) };
399
+ }
395
400
  return new Promise((resolve) => {
396
401
  const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
397
402
  let child;
@@ -454,10 +459,11 @@ function runCommandHook(entry, payload, ctx) {
454
459
  child.stdin?.end(`${JSON.stringify(boundedToolInputPayload(payload))}\n`);
455
460
  });
456
461
  }
457
- function fireAsyncCommandHook(entry, payload, ctx, event) {
462
+ async function fireAsyncCommandHook(entry, payload, ctx, event) {
458
463
  try {
459
464
  const rewake = entry.asyncRewake === true;
460
465
  const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
466
+ await resolveHostShell();
461
467
  const sh = hostShell();
462
468
  const child = spawn(sh.shell, [...sh.args, entry.command], {
463
469
  cwd: ctx.cwd,
@@ -754,7 +760,7 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
754
760
  if (entry.type === "command" && (entry.async || entry.asyncRewake)) {
755
761
  if (entry.once)
756
762
  onceFired.add(onceKey);
757
- fireAsyncCommandHook(entry, payload, ctx, event);
763
+ void fireAsyncCommandHook(entry, payload, ctx, event);
758
764
  continue;
759
765
  }
760
766
  if (Date.now() > deadlineAt) {
@@ -6,7 +6,8 @@
6
6
  * cut: ONLY TaskRequestBody (its field types are inline or core imports — no other server.ts-local satellites);
7
7
  * sibling request/response shapes can migrate here later on the same pattern.
8
8
  */
9
- import type { A2aServerSpec, AgentDefinition, ApprovalReach, McpServerSpec, PermissionMode, SkillSpec, TaskSpec } from "@sema-agent/core";
9
+ import type { A2aServerSpec, AgentDefinition, ApprovalReach, PermissionMode, SkillSpec, TaskSpec } from "@sema-agent/core";
10
+ import type { RequestMcpServerSpec } from "../mcp-assertion-header.js";
10
11
  import type { wiringManifestFaceEventData } from "../trace/project.js";
11
12
  import type { buildSyncSubmitBody } from "./sync-submit-body.js";
12
13
  export interface TaskRequestBody {
@@ -508,8 +509,15 @@ export interface TaskRequestBody {
508
509
  * 保留 —— 丢的只是声明。core 把这个键定性为**部署面**的键,本仓对它的 reject-or-strip 义务由既有的单用户闸
509
510
  * **结构性**兑现:多租户腿整只 `mcpServers` 被忽略 ⇒ 租户够不着;单用户腿 = core 明文豁免的超管面
510
511
  * (完整论证见 `task-mcp.ts` 的 `assertRequestMcpContentOrigin` 顶注)。部署面的正门是 center 下发的
511
- * `CenterMcpServer.contentOrigin`。 */
512
- mcpServers?: McpServerSpec[];
512
+ * `CenterMcpServer.contentOrigin`。
513
+ *
514
+ * S-1515:`transport.kind:"http"` 的条目可选 `assertionHeader`(托管连接器断言头的**头名**,契约包 settings-schema ≥12.2.0 同键)。
515
+ * 与 `principalHeader` **互斥**:同设 ⇒ 新提交腿同步 400 `request.body_shape`(拒句与契约包解析期那一句逐字相同);值不是字符串 ⇒ 400
516
+ * `request.field_invalid`。耐久 resume 腿不拒(重放存量 body),也**不摘 server**(摘了,park 在它工具审批上的任务批准后撞 `resume.tool_unavailable`
517
+ * 且每次重试重放同一份 body):降级到升级前的读法 —— server 照挂、`principalHeader` 胜、`assertionHeader` 不生效,一行 warn `mcp_assertion_header_ignored`。
518
+ * ⚠️ 本版**只承载、不发送**:引擎的 http 传输尚无断言头座,
519
+ * 声明被接收、server 照常挂载但不带断言头,并有一行运维 info `mcp_assertion_header_not_sent { server }`;发送随引擎座。 */
520
+ mcpServers?: RequestMcpServerSpec[];
513
521
  /** [DESIGN-269 车1] Caller-supplied per-request A2A peers (remote agents this task may call), aligned to
514
522
  * core `A2aServerSpec`. 🔒 honored on a SINGLE-USER deployment that has neither locked the `a2a` key nor
515
523
  * had `a2a_peers` denied by its compliance posture (task-a2a.ts `a2aInjectionHonored`, the same three-veto
package/dist/main.js CHANGED
@@ -4,6 +4,7 @@ import { runStage02 } from "./boot/stage-02-web-admin-pull.js";
4
4
  import { runStage03 } from "./boot/stage-03-store-backend.js";
5
5
  import { runStage04 } from "./boot/stage-04-stores.js";
6
6
  import { runStage05 } from "./boot/stage-05-execution-env.js";
7
+ import { settleHostLaneShell } from "./boot/execution-lane.js";
7
8
  import { runStage06 } from "./boot/stage-06-runners.js";
8
9
  import { runStage07 } from "./boot/stage-07-capability-layer.js";
9
10
  import { runStage08 } from "./boot/stage-08-reapers.js";
@@ -15,6 +16,7 @@ async function main() {
15
16
  const s03 = await runStage03(s02);
16
17
  const s04 = await runStage04(s03);
17
18
  const s05 = runStage05(s04);
19
+ await settleHostLaneShell(s05.config, s05.logger);
18
20
  const s06 = runStage06(s05);
19
21
  const s07 = await runStage07(s06);
20
22
  const s08 = runStage08(s07);