@sema-agent/server 7.127.2 → 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.
- package/MIGRATION.md +12 -0
- package/USAGE.md +48 -2
- package/dist/audit.js +4 -1
- package/dist/boot/execution-lane.d.ts +11 -0
- package/dist/boot/execution-lane.js +6 -0
- package/dist/boot/resolve-spec.js +3 -1
- package/dist/boot/web-admin.js +15 -11
- package/dist/capabilities/mcp-probe.js +11 -5
- package/dist/config-catalog.js +1 -1
- package/dist/config-provider.d.ts +17 -4
- package/dist/config-provider.js +8 -6
- package/dist/config-types.d.ts +2 -2
- package/dist/hooks/hook-runner.d.ts +1 -0
- package/dist/hooks/hook-runner.js +10 -4
- package/dist/http/wire-types.d.ts +11 -3
- package/dist/main.js +2 -0
- package/dist/mcp-assertion-header.d.ts +89 -0
- package/dist/mcp-assertion-header.js +49 -0
- package/dist/mcp-tool-axes.d.ts +62 -0
- package/dist/mcp-tool-axes.js +47 -0
- package/dist/plugins/host-platform.d.ts +43 -13
- package/dist/plugins/host-platform.js +67 -18
- package/dist/plugins/remote-env-host.js +3 -3
- package/dist/run-local.js +5 -3
- package/dist/task-mcp.d.ts +31 -3
- package/dist/task-mcp.js +42 -4
- package/dist/trace/project.js +1 -1
- package/dist/trace/tool-output-media.d.ts +8 -3
- package/dist/trace/tool-output-media.js +4 -4
- package/dist/web-admin-client/facade.d.ts +1 -1
- package/dist/web-admin-client/facade.js +1 -1
- package/dist/web-admin-client/http-client.d.ts +2 -2
- package/dist/web-admin-client/skills-mcp.d.ts +19 -16
- package/dist/web-admin-client/skills-mcp.js +27 -8
- package/dist/web-admin-client/types.d.ts +18 -0
- package/package.json +1 -1
package/MIGRATION.md
CHANGED
|
@@ -7,6 +7,18 @@
|
|
|
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
|
+
|
|
10
22
|
## 7.127.2 —— 账本行 details 卡的媒体字节不落账(行为修复)· 缺省部署自动整理被引擎拒(披露)· 7.127.0 节三条订正(升级零动作)
|
|
11
23
|
|
|
12
24
|
升级零动作:无 DDL、无配置变化。本节是对 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,16 +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
|
|
1683
|
+
> 账本面不存媒体字节 —— 账本行与 `GET /v1/runs/:id/events`(断线重连面;对 `POST /v1/runs` 提交的异步 run 它**就是活流**,所以这类客户端拿不到整图,整图只在本流、子代 tail 流与会话存档 `GET /v1/sessions/:id` 上)
|
|
1645
1684
|
> 里,带 `data` 的媒体块换成 `{ type, mimeType?, bytes, truncated: true }`(帧级 `truncated: true` 与 `totalChars` = 原值真实大小 —— 文本块字符数 + 媒体块 `data` 串长,引擎已截断的帧保留引擎给的值);无 `data` 且 `truncated: true` 的块渲染成「未送达」。
|
|
1646
1685
|
> **同一条律也管 `tool_end.structured`**:Read 读图像 / PDF 的卡 `{ type: "image" | "document", file: { base64?, type, originalSize, … } }` 里 `file.base64` 是同一份字节的第二份编码(引擎只在 ≤48K 字符时随卡),
|
|
1647
1686
|
> 本流上同样逐字(不过文本脱敏,卡上其余成员照常脱敏),积压判据把它与 `output` 块的字节同一帧一起计、降级时一并去掉;账本面(账本行与 `/events`、`GET /v1/tasks/:id/turns`、`GET /v1/tasks/:id/stream` 的回放)里卡上**没有** `file.base64`
|
|
1648
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 起;此前这一位也被文本脱敏改写)。
|
|
1649
1689
|
|
|
1650
1690
|
## 7. 其它接口(按需)
|
|
1651
1691
|
| 端点 | 用途 |
|
|
1652
1692
|
|---|---|
|
|
1653
|
-
| `GET /v1/sessions/<id>` | 审计回溯:当前上下文 + 摘要(owner 校验);消息里模型 / 工具产出的文本与 live 帧、账本帧同一只脱敏(密钥形换占位,其余字节不变) |
|
|
1693
|
+
| `GET /v1/sessions/<id>` | 审计回溯:当前上下文 + 摘要(owner 校验);消息里模型 / 工具产出的文本与 live 帧、账本帧同一只脱敏(密钥形换占位,其余字节不变);Read 图像 / PDF 的媒体卡(工具结果消息的 `details`)的 `file.base64` 是字节编码,逐字 |
|
|
1654
1694
|
| `GET /v1/approvals?owner=user:42` | 高危写审批(durable,需 `DURABLE_APPROVAL=true`):**operator** 看待办队列(可按 owner 过滤);非 operator 只看自己的 |
|
|
1655
1695
|
| `POST /v1/approvals/<sessionId>/decide` `{"decision":"approve"|"deny","reason":"…"}` | 批/否并恢复挂起任务(CAS,重复决议 409);**仅 operator**(非 operator → 403)。5.0.0 起旧轮询腿 `POST /v1/approvals/<id>` 已退役 |
|
|
1656
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) |
|
|
@@ -2436,6 +2476,12 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
|
|
|
2436
2476
|
- 置空(名单外 / 凭据名)时日志一行 `hook_http_header_env_blanked`(名字与原因,不带值)。
|
|
2437
2477
|
例:`"headers": { "Authorization": "Bearer ${HOOK_TOKEN}" }`、`"allowedEnvVars": ["HOOK_TOKEN"]`,并在请求 `settings.env` 里给 `HOOK_TOKEN`。7.125.x 及更早的读法不同(升级见 MIGRATION)。
|
|
2438
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
|
+
|
|
2439
2485
|
### 9.6 trace 脱敏的射程:与出站请求体无关
|
|
2440
2486
|
|
|
2441
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: "
|
|
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,
|
package/dist/boot/web-admin.js
CHANGED
|
@@ -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,
|
|
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
|
|
157
|
+
const candidateSource = () => {
|
|
157
158
|
if (configProvider === undefined)
|
|
158
|
-
throw new Error("config candidate
|
|
159
|
-
return configProvider
|
|
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,
|
|
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
|
-
|
|
376
|
-
|
|
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
|
|
696
|
-
skills = await
|
|
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
|
-
|
|
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
|
-
|
|
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";
|
package/dist/config-catalog.js
CHANGED
|
@@ -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
|
|
82
|
-
*
|
|
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
|
|
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 的选目录旋钮)。 */
|
package/dist/config-provider.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/dist/config-types.d.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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);
|