@lqc123qwe/car-runtime 1.5.0 → 1.7.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/README.md CHANGED
@@ -5,7 +5,8 @@ Composable Agent Runtime — S1 迭代产物:微内核骨架(F1 可逆注册
5
5
  ## 运行
6
6
 
7
7
  ```bash
8
- # 测试(零依赖,Node 22.19+ 内置能力)
8
+ npm install # 1.5 起 zod 为常规运行时依赖(D-22)——CLI/doctor 传递加载
9
+ # 测试(Node 22.19+;sqlite 需 --experimental-sqlite flag)
9
10
  node --experimental-transform-types --test test/*.spec.ts
10
11
  ```
11
12
 
@@ -199,9 +200,35 @@ node --experimental-transform-types --test test/*.spec.ts
199
200
  - **1.4 工程收口(2026-09-27)**:W1 完整四块兑现——**car run 首次真实调模型**(多步工具循环 E2E 全链),模型配置通道(llm.* + env + flags)、system prompt 落链(§3.5.5 红线兑现)、TurnAborted 首次实抛、CarM8Error 保形(A080001 引导文案可达)、TLS loopback 豁免(D-12a 兑现)、MCP/PTC 进真实 turn、遥测 turn/step span 出站。全量 **282(279 PASS + 3 能力门控 skip / 0 fail)** + tsc strict 0 + secrets-scan 105 文件 0 命中。**1.4-BUG-1(fetch 缺 POST)/BUG-2(win32 退出崩溃)/BUG-3(IPv6 方括号)均为生产接线暴露的真缺陷**。
200
201
  - **1.4.0 正式发布(2026-09-27,S9 收官,全自动第二次实战)**:npm `@lqc123qwe/car-runtime@1.4.0` 在架——**dist-tags latest → 1.4.0、rc 保留指向 1.0.0-rc.1** + provenance。Release `1.4.0`(agent 经 GCM 凭据 API 创建)→ **j16 run 36328295548 全步 success**(构建/keyless 签名/自动上传五件套/npm publish)→ **post-publish 12 PASS / 1 DRY-RUN / 0 FAIL(G-09 实查)**;外部等价检查 **SHA256 MATCH(`55ce08fd…`)+ cosign keyless Verified OK**。
201
202
  - **遗留登记**:anthropic 适配器(冻结);远程 MCP 传输(SSE/HTTP);写类工具双 toolResult 投影观察项;car.config.ts+zod;jiti。规划:`car-docs/10-内核v1/1.4-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.4-收口终验报告.md`。
203
+
204
+ ## 1.5 状态(provider 扩展 + 远程 MCP + 配置完整形态)
205
+
206
+ | `src/runtime-core/llm.ts` | 1.5-S1(D-20)anthropic 适配器:Messages API 流式 SSE(data-only 解析;`{baseUrl}/v1/messages`;x-api-key + anthropic-version 2023-06-01;system 顶层化〔多条 \n\n 连接〕;toolResult 收敛单 user 多 tool_result 块〔is_error〕;tools → input_schema;**max_tokens 必填缺省 4096**;stop_reason 映射未映射 fail-visible;凭据 provider='anthropic')+ **streamSsePost 引擎抽取**(§3.5.4 重试/超时/取消语义单点共用——openai-compat/anthropic 不漂移)+ assertTlsOrLoopback 抽取(D-12a 口径不变) | 1.4 冻结表 anthropic 出清 |
207
+ | `src/runtime-core/chatStep.ts` | **1.5-BUG-1 修复:toolCall 投影丢弃**——deriveMessages 的 assistant 工具调用形态为 `{role:'assistant', toolCall}`(log.ts 口径),chatStep 仅透传 content → 工具调用历史在后续请求静默消失(真实 provider 对无 tool_calls 前驱的 tool 消息 400);anthropic spawn E2E 生产调用点核查暴露,openai-compat 同受益 | 1.5-GO-5 双向核查产出 |
208
+ | `src/mcp/httpClient.ts` | 1.5-S2(D-21)远程 MCP 传输:Streamable HTTP(2025-06-18 单轨)+ **懒 initialize 握手**(恰好一次;Mcp-Session-Id 捕获回传)+ JSON/SSE 双形态响应(SSE 命中即收口流显式 destroy)+ **出站 node:http(s) 不走 undici fetch**(1.4-BUG-2 已验证口径;本地 spawn E2E 曾复现 win32 退出 0xC0000409,换通道后消除)+ TLS 门复用 + resolveMcpHeaders(`${ENV_VAR}` 值内插值 + 字面明文凭据拒绝 CR-05) | 1.4 冻结表远程 MCP 出清 |
209
+ | `src/load/config.ts` | 1.5-S3(D-22)配置完整形态:**car.config.ts 载体**(动态 import 与插件加载同纪律;发现序 ts > json〔并存 ts 胜出〕;warmCarConfig 预热保持 loadCarConfig 同步签名——未预热显式报错禁静默回退 json)+ **zod ^3 校验层**(首个常规运行时依赖,D-11a ① 修订;CAR-E-CONFIG fail-visible 保持;issue 渲染 `路径 原因`;car.config.json 键位兼容)+ mcp.servers url/headers 键位(command/url 互斥恰好其一;通道专属键 fail-visible) | 1.1-S3「届时迁移」预登记出清 |
210
+ | `src/dx/doctor.ts` + `src/runtime-core/credentials.ts` + `src/cli.ts` | 1.5-S4 配套:doctorModelReadiness provider 感知(adapterId anthropic → ANTHROPIC_API_KEY;providerEnvVars 导出复用)+ `--adapter-id anthropic` 装配分支 + HELP/错误文案双载体与 anthropic 引导 | 1.5 DX 配套 |
211
+ | `test/s33~s35` | s33 8 测(出站体/stop_reason 映射/流容错/chatStep 多块聚合/基线同款 TLS·凭据·重试·取消/doctor 感知/spawn E2E)+ s34 6 测(握手序/会话头/SSE 形态/closed 拒绝/headers 凭据规则/config 键位/spawn `--mcp` url 全链)+ s35 4 测(zod 键位兼容/发现序/预热协议+epoch 击穿/TS 形态校验) | 1.5-GO |
212
+
213
+ - **1.5 工程收口(2026-09-28,`ec5a4c4`)**:三大登记项全部兑现(1.4 冻结表「登记后续」清零)。全量 **300(297 PASS + 3 能力门控 skip / 0 fail)** + tsc strict 0 + secrets-scan commit 后复跑 114 文件 0 命中 + contract-check 4/4(golden 不动)。**1.5-BUG-1 为生产调用点核查直接产出**(1.4 的 E2E 假服务不校验 tool_calls 历史故未暴露)。**1.5-插曲-1(HELP 模板串 `${ENV_VAR}` 未转义 → CLI 全 spawn 挂)**——模板字面量内插值转义教训。**1.5-CI-1:test/dx-smoke jobs 补 npm install**(零依赖 CI 前提随 D-22 失效——j02/j03/j04/j10/j11/j12;改 workflow 后 PyYAML 复验执行)。
214
+ - **1.5.0 正式发布(2026-09-28,S6 收官)**:npm `@lqc123qwe/car-runtime@1.5.0` 在架——**dist-tags latest → 1.5.0、rc 保留 1.0.0-rc.1** + provenance(slsa.dev/provenance/v1)。Release `1.5.0`(agent GCM 凭据 API 创建)→ **j16 run 36394117668 全步 success**(构建/keyless 签名/自动上传五件套/npm publish)→ **post-publish 12 PASS / 1 DRY-RUN / 0 FAIL(G-09 实查)**;外部等价检查 **SHA256 MATCH(`2edae513…`)+ Rekor 透明日志交叉验证**(logIndex 2981593515,bundle↔API 双端一致;cosign 二进制本地缺席,以 Rekor API 对拍替代——登记口径)。**G-04 门禁口径随 D-22 更新**:零依赖断言 → npm audit(omit=dev high,0 findings)。
215
+ - **遗留登记**:jiti 首载;host 工具面;写类工具双 toolResult 投影观察项;MCP legacy HTTP+SSE(2024-11-05)不追溯。规划:`car-docs/10-内核v1/1.5-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.5-收口终验报告.md`。
216
+
217
+ ## 1.6 状态(宿主工具面 + 投影收敛 + jiti 载体)
218
+
219
+ | `src/session/log.ts` | 1.6-S1(D-23)**双 toolResult 投影收敛**:deriveMessages 同 id toolResult last-wins——写类工具授权决策(runtime/toolResult)与执行结果(plugin/toolResult)成对落链(M4 既有语义冻结:审计全量留痕不动),模型可见流只保留最终结果(真实 provider 重复 tool_call_id/tool_use_id 拒绝或未定义面出清);**id 缺席不收敛**(hostRaw/无 id 载荷形态不变,golden ④ 兼容);授权拒绝单条即唯一结果原样保留;适配器侧零改动 | 1.4 观察项出清 |
220
+ | `src/host/facade.ts` + `src/host/hostGateway.ts` | 1.6-S2(D-25)**宿主工具面透传真实接线**(S13 占位退役):RuntimeFacade opts 加 `tools?: Map<string, ToolReg>`(缺省行为不变)——toolList 出 name/description/parameters(**declaredSideEffect 不出站**:权限面只进权限门红线;未装配 = 空面兼容)+ toolCall 权限门(T-22 缺省 write 收口;write 类 policy 拒绝——mcp-serve 无人审回路 deny-by-default;readonly 执行;未知工具显式报错;工具异常=成对错误结果 US-5 同款)+ 成对落链 actor='user'(宿主=用户代理)turnId='T0'(会话级能力面,sessionTurn 从 T1 起不冲突) | 1.4 冻结表 host 工具面出清 |
221
+ | `src/host/stdio.ts` | **1.6-插曲-1 修复:tools/call 异步并发派发响应乱序**(请求 id 4 响应晚于 id 7——JSON-RPC 合法但宿主按序解析 + 会话态竞态面)→ callChain 请求序串行派发 + serve 收口 await callChain(响应写完才退出——win32 退出同源预防) | s36 spawn E2E 七请求全序断言钉死 |
222
+ | `src/cli.ts` | 1.6-S2 装配:mcp-serve 插件工具跨源同名 **CAR-E-DUP fail-closed** 启动中止(1.4-S1 纪律同款,禁静默覆盖)+ toolMap 注入 facade + 装载文案更新(「会话内执行接线属 W1 登记后续」口径退役) | mcp-serve 工具面闭环 |
223
+ | `src/load/config.ts` + package.json | 1.6-S3(D-24)**car.config.ts jiti 载体**:loadTsConfigFile 改 jiti 自包含转译(每 epoch 新实例 + moduleCache 关 = 击穿语义保持;**interopDefault 显式关**——jiti 缺省 true 把无 default 模块的 exports 整体当 default,「缺 default export」fail-visible 失效);Node 22.19+ 无需 --experimental-transform-types 载配置;非可擦除语法(enum)可用——**erasable-only 预检登记项出清(被取代)**;jiti ^2 第二个常规运行时依赖(零传递依赖 bundled;D-11a ① 二次修订;G-04 npm audit 0 findings);首载时延实测 292ms(spawn 冷启) | 1.5 报告 §7 jiti 候选出清 |
224
+ | `test/s36~s38` | s36 9 测(声明面权限面不出站/readonly 执行落链/write+未声明双路径拒绝/未知工具/工具异常/未装配 fail-visible/spawn E2E ×3 含 CAR-E-DUP exit 1)+ s37 6 测(granted 双事件单投影/denied 单条/id 缺席 golden ④ 兼容/N1 一致性/**双适配器出站体单 tool 消息**)+ s38 3 测(enum 真载/strip-only spawn 差分含负控+首载时延/CLI 真路径) | 1.6-GO |
225
+
226
+ - **1.6 工程收口(2026-09-28,`6e0315d`)**:候选池三块全部兑现(1.4/1.5 冻结表「登记后续」全数出清)。全量 **318(315 PASS + 3 能力门控 skip / 0 fail)** + tsc strict 0 + secrets-scan commit 后复跑 117 文件 0 命中 + contract-check 4/4(golden 不动)。**1.6-插曲-1(stdio 响应乱序)为 spawn E2E 按序断言直接产出**;**1.6-BUG-1(jiti interopDefault 缺省 true)被 s35 零改动验收线捕获**。
227
+ - **1.6.0 正式发布(2026-09-28,S5 收官)**:npm `@lqc123qwe/car-runtime@1.6.0` 在架——**dist-tags latest → 1.6.0、rc 保留 1.0.0-rc.1** + provenance。Release `1.6.0`(id 398135305,agent GCM 凭据 API 创建)→ **j16 run 36408091143 全步 success**(构建/keyless 签名/自动上传五件套/npm publish)→ **post-publish 13 PASS / 0 DRY-RUN / 0 FAIL(REMOTE_CHECK=1 全实查)**;外部等价检查 **SHA256 MATCH(`dadc1a3c…`)+ Rekor 交叉验证**(logIndex 2981799459 双端一致 + 透明日志 body 内制品哈希三方一致——tree 键形态差异登记见收口报告 §5)。
228
+ - **遗留登记(1.7 候选池)**:遥测 OTLP 增强(两度延后);MCP legacy HTTP+SSE 维持不追溯;写类工具人审 confirm 通道宿主面外推(D-25 口径);插件 jiti 轨收敛观察项(插件仍走原生剥离,可选演进非缺陷)。规划:`car-docs/10-内核v1/1.6-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.6-收口终验报告.md`。
202
229
  ## 状态
203
230
 
204
- - **版本主线**:S1-S4 48/48 | M2 89/89(v0.2.0)| M3 127/127(v0.3.0)| M4 139/139(**1.0.0-rc.1 已发布**)| M5 156/156(DEC-4 预埋项转正)| M6 179/179(2026-09-15 收口)| M7 全量 195(192 PASS + 3 能力门控 skip,2026-09-19 收口)| **M5-S28 装载签名门 全量 204(201 PASS + 3 skip,2026-09-26)** | **M8 运行时底座 全量 231(228 PASS + 3 skip,2026-09-26 收口,§3.2 八模块全部收口)** | **1.1 签名收尾最小集 全量 249(246 PASS + 3 skip,2026-09-27 收口,1.1.0 已发布,enforce 翻转准备就绪)** | **1.2 会话/遥测小项 全量 263(260 PASS + 3 skip,2026-09-27 收口,1.2.0 已发布)** | **1.3 发布资产自动化 全量 270(267 PASS + 3 skip,2026-09-27 收口,1.3.0 已发布,全链零人工拖拽兑现)** | **1.4 真实模型接入 全量 282(279 PASS + 3 skip,2026-09-27 收口,1.4.0 已发布)**
231
+ - **版本主线**:S1-S4 48/48 | M2 89/89(v0.2.0)| M3 127/127(v0.3.0)| M4 139/139(**1.0.0-rc.1 已发布**)| M5 156/156(DEC-4 预埋项转正)| M6 179/179(2026-09-15 收口)| M7 全量 195(192 PASS + 3 能力门控 skip,2026-09-19 收口)| **M5-S28 装载签名门 全量 204(201 PASS + 3 skip,2026-09-26)** | **M8 运行时底座 全量 231(228 PASS + 3 skip,2026-09-26 收口,§3.2 八模块全部收口)** | **1.1 签名收尾最小集 全量 249(246 PASS + 3 skip,2026-09-27 收口,1.1.0 已发布,enforce 翻转准备就绪)** | **1.2 会话/遥测小项 全量 263(260 PASS + 3 skip,2026-09-27 收口,1.2.0 已发布)** | **1.3 发布资产自动化 全量 270(267 PASS + 3 skip,2026-09-27 收口,1.3.0 已发布,全链零人工拖拽兑现)** | **1.4 真实模型接入 全量 282(279 PASS + 3 skip,2026-09-27 收口,1.4.0 已发布)** | **1.5 provider 扩展+远程 MCP+配置完整形态 全量 300(297 PASS + 3 skip,2026-09-28 收口,1.5.0 已发布)** | **1.6 宿主工具面+投影收敛+jiti 载体 全量 318(315 PASS + 3 skip,2026-09-28 收口,1.6.0 已发布)**
205
232
  - **1.0.0 正式发布(2026-09-26,S29/S30)**:npm `@lqc123qwe/car-runtime@1.0.0` 在架——**dist-tag latest → 1.0.0,rc 保留指向 1.0.0-rc.1**(§1.1 口径);SLSA provenance(Actions OIDC,透明日志在案);GitHub Release v1.0.0 六资产(tgz/bundle/SHA256SUMS/SBOM/审计/公钥);**post-publish 全链 13 PASS / 0 DRY-RUN / 0 FAIL** + 外部验证者等价检查(Release 实下载件 SHA256 MATCH + cosign Verified OK)。**宽限期时钟:2026-09-26 起算,2026-12-25 到期 verifier 默认值 warn→enforce 翻转(计划内变更)**。终验报告:`car-docs/10-内核v1/M5-S30-1.0收口终验报告.md`。发布工程事件:j16 首跑 E403(npm 2FA 策略 vs granular token 无 bypass——清单预设回退路径兑现)→ bypass token re-run 绿;G-09 期望 tag 动态化(`4c84b17`)在真实发布兑现价值。
206
233
  - **1.0.0-rc.1 已发布(2026-09-09)**:npm `@lqc123qwe/car-runtime@1.0.0-rc.1`(rc + latest 双 tag,SLSA provenance 在案)+ GitHub Release 6 制品;发布预演 **13 PASS / 0 DRY-RUN / 0 FAIL**(c762b72)。S22 GO 清单已全绿收口。
207
234
  - **轨道 A 三项全部完成**:E-1 WSL2 逃逸矩阵 **20/20 PASS 零逃逸**(2026-09-07,G-07 转 PASS)| E-3 远端发布通道 **13/13 全门禁 PASS**(2026-09-09,G-08/09/10 转 PASS)| E-2 Windows koffi FFI **四判据双环境全 PASS**(S24/S25/S26,2026-09-09~10;本机非提权 + windows-latest 提权 runner 双绿,CI 门禁已接入)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lqc123qwe/car-runtime",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Composable Agent Runtime (CAR) - microkernel plugin system with sandboxed PTC, append-only hashed session log, and MCP host bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -26,6 +26,7 @@
26
26
  "@types/node": "^26.6.1"
27
27
  },
28
28
  "dependencies": {
29
+ "jiti": "^2.7.0",
29
30
  "zod": "^3.25.76"
30
31
  }
31
32
  }
@@ -69,9 +69,26 @@ const record = (stage: string, gate: string, ok: boolean, detail = '', dryRun =
69
69
  for (const p of secretPatterns) if (p.test(content)) hits++
70
70
  }
71
71
  record(stage, 'G-03 硬编码密钥扫描 = 0', hits === 0, `${hits} hits(扫描 ${scanned} 个 .ts,豁免:secrets.ts 规则库 + s7 标定基准,豁免清单见脚本注记)`)
72
- // 依赖审计:零运行时依赖 → 无漏洞面(pnpm audit 在引入依赖后启用)
72
+ // 依赖审计:1.5-S3(D-22)zod 为首个常规运行时依赖 → 门禁口径从「零依赖断言」切换为
73
+ // npm audit(omit=dev,high 阻塞)——与 ci.yml j01 依赖审计同款;audit 不可达(离线)= DRY-RUN
73
74
  const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'))
74
- record(stage, 'G-04 依赖审计', Object.keys(pkg.dependencies ?? {}).length === 0, '零运行时依赖(node:sqlite/node:crypto 内置),pnpm audit 在引入首依赖后启用')
75
+ const depCount = Object.keys(pkg.dependencies ?? {}).length
76
+ let auditOk = true
77
+ let auditDetail = '无运行时依赖(audit 免除)'
78
+ if (depCount > 0) {
79
+ if (process.env.REMOTE_CHECK === '1') {
80
+ try {
81
+ execSync('npm audit --omit=dev --audit-level=high', { cwd: ROOT, stdio: 'pipe' })
82
+ auditDetail = `运行时依赖 ${depCount} 项(D-22/D-24:zod ^3 + jiti ^2);npm audit --omit=dev --audit-level=high 0 findings`
83
+ } catch {
84
+ auditOk = false
85
+ auditDetail = `运行时依赖 ${depCount} 项;npm audit --audit-level=high 有 findings(阻塞——G-04 D-22 口径)`
86
+ }
87
+ } else {
88
+ auditDetail = `运行时依赖 ${depCount} 项(D-22/D-24:zod ^3 + jiti ^2);npm audit 在 REMOTE_CHECK=1 实跑(DRY-RUN)`
89
+ }
90
+ }
91
+ record(stage, 'G-04 依赖审计', auditOk, auditDetail)
75
92
  }
76
93
 
77
94
  // ── 阶段 2:构建与制品 ──
package/src/cli.ts CHANGED
@@ -461,9 +461,9 @@ async function main(): Promise<number> {
461
461
  // E-6 宿主实连入口(M4-S18):CAR-as-MCP-Server——宿主(Claude Code/Codex)经 stdio JSON-RPC 接入
462
462
  // 1.1-S4 装载接线:--plugin <file|dir>(可重复)/ CAR_PLUGINS env → 六阶段流水线(verify 门先于
463
463
  // import())挂载进 Context + car_load_total/car_unsigned_confirmed 全链计数。
464
- // 边界(1.1 规划 W2-4,防口径外推):此处是「装载接线 + 采集全链」——插件被真实装配
465
- // (factory 执行 + bindCore 冲刷注册项),但宿主会话执行插件工具的执行接线属 W1 登记后续
466
- // (sessionTurn 仍为事件批归一化)。
464
+ // 1.6-S2(D-25)宿主工具面透传接线:插件工具 Map 注入 RuntimeFacade——tool_list 出声明面
465
+ // (declaredSideEffect 不出站:权限面只进权限门红线)、tool_call 经权限门(write 类 policy
466
+ // 拒绝/readonly 执行)+ 成对落链;跨源同名 CAR-E-DUP fail-closed(1.4-S1 纪律同款)。
467
467
  const hostIdx = rest.indexOf('--host')
468
468
  const { HOST_MAPPINGS } = await import('./host/mappings.ts')
469
469
  // 默认宿主取映射表首项(数据驱动,静态架构断言红线:宿主标识只在 mappings 数据文件)
@@ -487,11 +487,12 @@ async function main(): Promise<number> {
487
487
  counters.onCount(name, labels)
488
488
  if (otel.enabled) otel.getMeter().createCounter(name).add(1, labels as Record<string, string> | undefined)
489
489
  }
490
+ // 1.6-S2:hostTools 提升到块外——跨源同名查重与 toolMap 装配在装载块之后统一收口
491
+ const hostTools: Array<{ name: string; description?: string; parameters?: unknown; declaredSideEffect?: 'readonly' | 'write'; run: (args: Record<string, unknown>) => Promise<unknown> }> = []
490
492
  if (pluginsArg.sources.length) {
491
493
  const { loadPlugins, formatLoadReport } = await import('./load/report.ts')
492
494
  const { Context } = await import('./kernel/context.ts')
493
495
  const ctx = new Context()
494
- const hostTools: any[] = []
495
496
  for (const src of pluginsArg.sources) {
496
497
  const result = await loadPlugins({ source: src, signature: { ...sig.options, onCount: (n, l) => onCount(n, l) } })
497
498
  if (result.report.stages.some(s => s.status === 'FAIL')) {
@@ -511,17 +512,45 @@ async function main(): Promise<number> {
511
512
  } })
512
513
  }
513
514
  const names = result.plugins.map(p => `${p.manifest.name}@${p.manifest.version}`).join(', ')
514
- console.error(`[car mcp-serve] plugins loaded: ${names}(tools: ${hostTools.map(t => t.name).join(', ') || '无'};装载已接线,会话内执行接线属 W1 登记后续)`)
515
+ console.error(`[car mcp-serve] plugins loaded: ${names}(tools: ${hostTools.map(t => t.name).join(', ') || '无'};装载+工具面透传已接线——1.6-S2)`)
515
516
  }
516
517
  }
517
518
  const { HostGateway } = await import('./host/hostGateway.ts')
518
519
  const { createRuntimeFacade } = await import('./host/facade.ts')
519
520
  const { createStdioServer } = await import('./host/stdio.ts')
520
521
  const profiles = new Map(HOST_MAPPINGS.map(h => [h.hostId, h]))
521
- const facade = createRuntimeFacade({ profiles })
522
+ // 1.6-S2:跨源同名显式报错(禁静默覆盖——last-wins Map 语义下同名即歧义,fail-closed 启动中止)
523
+ const dupTool = hostTools.find((t, i) => hostTools.findIndex(x => x.name === t.name) !== i)
524
+ if (dupTool) { console.error(`CAR-E-DUP: 工具 "${dupTool.name}" 跨源同名——禁静默覆盖(mcp-serve fail-closed 启动中止)`); return 1 }
525
+ const toolMap = new Map(hostTools.map(t => [t.name as string, t]))
526
+ const facade = createRuntimeFacade({ profiles, ...(toolMap.size ? { tools: toolMap } : {}) })
522
527
  const gw = new HostGateway({ facade, audit: e => onCount('car_registry_decision', { source: 'registry' }) })
523
528
  for (const h of HOST_MAPPINGS) gw.registerHost({ hostId: h.hostId, profile: h, transport: {} as never })
524
- const server = createStdioServer((tool, args) => gw.handle(hostId, tool, args))
529
+ // 1.7-S3(D-28):sessionTurn 挂 turn 级 span(无 step span——mcp-serve 无 model() 循环)。
530
+ // 挂点在装配层(1.4-S6 纪律同款);facade 内部自增派生 turnId(T{n})——调用前不可知,
531
+ // 经 setTurnId 回填;outcome = 归一化 turnEnd reason(枚举面零内容)。缺省关 = noop 句柄零出站。
532
+ const tracer = new TurnTracer(otel)
533
+ const server = createStdioServer(async (tool, args) => {
534
+ if (tool !== 'session_turn') return gw.handle(hostId, tool, args)
535
+ const turn = tracer.startTurn('')
536
+ try {
537
+ const r = await gw.handle(hostId, tool, args)
538
+ if (!r.ok) {
539
+ // handle 层吞错为 {ok:false}(显式错误结果不抛出)——span 面以 error 收口留痕
540
+ turn.recordException(new Error(r.error ?? 'host call failed'))
541
+ turn.end('error')
542
+ return r
543
+ }
544
+ const payload = (r.result ?? {}) as { reason?: string; steps?: number }
545
+ turn.setTurnId(`T${payload.steps ?? 0}`)
546
+ turn.end(String(payload.reason ?? 'completed'))
547
+ return r
548
+ } catch (e) {
549
+ turn.recordException(e)
550
+ turn.end('error')
551
+ throw e
552
+ }
553
+ })
525
554
  const n = await server.serve(process.stdin, process.stdout)
526
555
  // 采集窗口收口(登记表兜底通道):快照写 stderr(stdout 为协议通道不可污染)
527
556
  const snap = counters.snapshot()
@@ -7,14 +7,24 @@
7
7
  * 未命中事件走 hostRaw 降级(零静默);turnEnd reason 由最后一条归一化 turnEnd 决定(缺省 completed);
8
8
  * - 模型循环由宿主驱动(「统一边界而非统一内部」):CAR 承载日志/审计/工具面,step 循环不暴露;
9
9
  * - 会话内存态 Map 承载(v0.2.0 SessionLog 语义),持久化经 sessionExport(取证包)与后续 S13 文件落盘增强。
10
+ * - 1.6-S2(D-25)宿主工具面透传:opts.tools 注入插件工具 Map(mcp-serve 装配)——
11
+ * toolList 出 name/description/parameters(declaredSideEffect 不出站:权限面只进权限门红线);
12
+ * toolCall 经权限门(T-22 缺省 write 收口;write 类 policy 拒绝——mcp-serve 无人审回路,
13
+ * 人审 confirm 属 turn 循环 preset.authorize 通道不外推到宿主面)+ 成对落链
14
+ * (actor='user' 宿主=用户代理 / turnId='T0' 会话级能力面,sessionTurn 从 T1 起不冲突);
15
+ * 未装配 tools 时显式报错(S13 占位「静默空面」退役——fail-visible)。
10
16
  */
11
17
  import { SessionLog, type EventKind } from '../session/log.ts'
12
18
  import { forkCrossHost, loadForkedLog } from '../session/fork.ts'
13
19
  import { deriveSessionId, normalizeHostEvent } from './mappings.ts'
14
20
  import type { HostProfile } from './mappings.ts'
15
21
  import type { RuntimeFacade } from './hostGateway.ts'
22
+ import type { ToolReg } from '../load/loader.ts'
16
23
 
17
- interface SessionState { log: SessionLog; profile: HostProfile; hostSessionId: string; turnCount: number; lastReason: string }
24
+ interface SessionState { log: SessionLog; profile: HostProfile; hostSessionId: string; turnCount: number; lastReason: string; hostToolSeq: number }
25
+
26
+ /** 宿主工具面声明形态(toolList 出站——权限面字段不出站) */
27
+ export interface HostToolDescriptor { name: string; description?: string; parameters?: unknown }
18
28
 
19
29
  /** 迁移事件里已用过的最大 turn 序号(T{n})——import 侧续跑 turnId 不与迁移段冲突 */
20
30
  function maxTurnIndex(log: SessionLog): number {
@@ -26,7 +36,7 @@ function maxTurnIndex(log: SessionLog): number {
26
36
  return max
27
37
  }
28
38
 
29
- export function createRuntimeFacade(opts: { profiles: Map<string, HostProfile> }): RuntimeFacade {
39
+ export function createRuntimeFacade(opts: { profiles: Map<string, HostProfile>; tools?: Map<string, ToolReg> }): RuntimeFacade {
30
40
  const sessions = new Map<string, SessionState>()
31
41
 
32
42
  const bySession = (sessionId: string): SessionState => {
@@ -47,12 +57,12 @@ export function createRuntimeFacade(opts: { profiles: Map<string, HostProfile> }
47
57
  if (importJsonl !== undefined) {
48
58
  const { log } = loadForkedLog(importJsonl, { hostId, hostSessionId })
49
59
  const turnCount = maxTurnIndex(log)
50
- sessions.set(sessionId, { log, profile, hostSessionId, turnCount, lastReason: 'completed' })
60
+ sessions.set(sessionId, { log, profile, hostSessionId, turnCount, lastReason: 'completed', hostToolSeq: 0 })
51
61
  return { sessionId }
52
62
  }
53
63
  const log = new SessionLog(sessionId)
54
64
  log.append('runtime', 'hostRaw', 'T0', { hostEvent: 'session_registered', hostId, hostSessionId })
55
- sessions.set(sessionId, { log, profile, hostSessionId, turnCount: 0, lastReason: 'completed' })
65
+ sessions.set(sessionId, { log, profile, hostSessionId, turnCount: 0, lastReason: 'completed', hostToolSeq: 0 })
56
66
  return { sessionId }
57
67
  },
58
68
 
@@ -115,11 +125,38 @@ export function createRuntimeFacade(opts: { profiles: Map<string, HostProfile> }
115
125
  },
116
126
 
117
127
  async toolList() {
118
- return { tools: [] } // S13:宿主工具面透传(登记制接入宿主侧工具)
128
+ if (!opts.tools) return { tools: [] } // 未装配 = 空面(与占位形态兼容);装配后出声明面
129
+ const out: HostToolDescriptor[] = []
130
+ for (const t of opts.tools.values()) {
131
+ out.push({ name: t.name, ...(t.description !== undefined ? { description: t.description } : {}), ...(t.parameters !== undefined ? { parameters: t.parameters } : {}) })
132
+ }
133
+ return { tools: out }
119
134
  },
120
135
 
121
- async toolCall() {
122
- return { ok: false, error: 'tool 面透传在 S13 实装(tool_list/tool_call)' }
136
+ // 1.6-S2(D-25):权限门语义——未知工具显式报错;T-22 缺省 write;write 类 policy 拒绝
137
+ //(mcp-serve 无人审回路,deny-by-default);readonly 执行;工具异常=成对错误结果(US-5 同款)。
138
+ // 全路径成对落链(toolCall/toolResult)——宿主面调用 100% 留痕哈希链。
139
+ async toolCall(args) {
140
+ const { sessionId, tool, arguments: toolArgs } = args as { sessionId?: string; tool?: string; arguments?: Record<string, unknown> }
141
+ const s = bySession(String(sessionId))
142
+ if (!opts.tools) throw new Error('CAR-E-HOST: tool 面未装配(mcp-serve 未登记插件工具——tool_list/tool_call 不可用)')
143
+ const id = `host-tc-${++s.hostToolSeq}`
144
+ s.log.append('user', 'toolCall', 'T0', { id, tool: String(tool), args: toolArgs ?? {} })
145
+ const def = opts.tools.get(String(tool))
146
+ if (!def) {
147
+ s.log.append('plugin', 'toolResult', 'T0', { id, error: `unknown tool "${tool}"` })
148
+ return { ok: false, error: `unknown tool "${tool}"(tool_list 查看已装配工具面)` }
149
+ }
150
+ const sideEffect = def.declaredSideEffect ?? 'write' // T-22 收口:未声明按最高约束
151
+ if (sideEffect === 'write') {
152
+ s.log.append('runtime', 'toolResult', 'T0', { id, error: 'authorization-denied', granted: false, mode: 'host-policy' })
153
+ return { ok: false, error: 'CAR-E-HOST-AUTHZ: write 类工具经宿主面 policy 拒绝(mcp-serve 无人审回路——D-25 deny-by-default)' }
154
+ }
155
+ let result: unknown
156
+ let toolError: string | undefined
157
+ try { result = await def.run(toolArgs ?? {}) } catch (e) { toolError = String(e) }
158
+ s.log.append('plugin', 'toolResult', 'T0', { id, result, ...(toolError ? { error: toolError } : {}) })
159
+ return toolError !== undefined ? { ok: false, error: toolError } : { ok: true, result }
123
160
  },
124
161
  }
125
162
  }
@@ -29,7 +29,8 @@ export interface RuntimeFacade {
29
29
  sessionExport(args: { sessionId: string }): Promise<{ bundle: unknown }>
30
30
  /** M5-DEC4 转正:跨宿主 fork(宿主 A 侧调用,产出可迁移工件 + 宿主 B 侧确定性会话 id) */
31
31
  sessionFork(args: { sessionId: string; targetHostId: string; targetHostSessionId: string; upToSeq?: number }): Promise<{ sessionId: string; jsonl: string; migrated: number }>
32
- toolList(args: Record<string, never>): Promise<{ tools: string[] }>
32
+ /** 1.6-S2(D-25):声明面出站(name/description/parameters——declaredSideEffect 权限面不出站);未装配 = 空面 */
33
+ toolList(args: Record<string, never>): Promise<{ tools: Array<{ name: string; description?: string; parameters?: unknown }> }>
33
34
  toolCall(args: { sessionId: string; tool: string; arguments?: Record<string, unknown> }): Promise<{ ok: boolean; result?: unknown; error?: string }>
34
35
  }
35
36
 
package/src/host/stdio.ts CHANGED
@@ -21,6 +21,9 @@ export interface StdioServer {
21
21
 
22
22
  export function createStdioServer(dispatch: StdioDispatcher): StdioServer {
23
23
  let nextId = 0
24
+ // 1.6-S2:tools/call 按请求序串行派发(会话态依赖 + 响应序确定性——JSON-RPC 允许乱序,
25
+ // 但 stdio 宿主与测试按序解析;tools/list 等同步方法不受此链影响)
26
+ let callChain: Promise<void> = Promise.resolve()
24
27
  return {
25
28
  async serve(input, output) {
26
29
  let count = 0
@@ -59,15 +62,19 @@ export function createStdioServer(dispatch: StdioDispatcher): StdioServer {
59
62
  if (req.method === 'tools/call') {
60
63
  const name = req.params?.name ?? ''
61
64
  const args = req.params?.arguments ?? {}
62
- void dispatch(name, args).then(r => {
63
- if (r.ok) respond({ content: [{ type: 'text', text: JSON.stringify(r.result ?? {}) }], carOk: true })
64
- else respond(undefined, { code: -32000, message: r.error ?? 'host call failed' })
65
- }).catch(e => respond(undefined, { code: -32000, message: String(e) }))
65
+ callChain = callChain
66
+ .then(() => dispatch(name, args))
67
+ .then(r => {
68
+ if (r.ok) respond({ content: [{ type: 'text', text: JSON.stringify(r.result ?? {}) }], carOk: true })
69
+ else respond(undefined, { code: -32000, message: r.error ?? 'host call failed' })
70
+ })
71
+ .catch(e => respond(undefined, { code: -32000, message: String(e) }))
66
72
  return
67
73
  }
68
74
  respond(undefined, { code: -32601, message: `unknown method "${req.method}"(支持 initialize / tools/list / tools/call)` })
69
75
  })
70
76
  await done
77
+ await callChain // 串行链收口:进程退出前最后一批调用响应写完(win32 退出断言同源教训)
71
78
  return count
72
79
  },
73
80
  }
@@ -1,10 +1,13 @@
1
1
  /**
2
- * 1.1-S3 签名配置文件通道 → 1.5-S3 完整形态(car.config.ts + zod,D-22)
2
+ * 1.1-S3 签名配置文件通道 → 1.5-S3 完整形态(car.config.ts + zod,D-22)→ 1.6-S3 jiti 载体(D-24)
3
3
  *
4
4
  * 口径:
5
- * - 载体(D-22):发现序 **car.config.ts > car.config.json**(同目录两者并存 = ts 胜出,
5
+ * - 载体(D-22 + D-24 修订):发现序 **car.config.ts > car.config.json**(同目录两者并存 = ts 胜出,
6
6
  * json 忽略——禁静默合并);显式 --config 按扩展名分派(.ts 走 TS 载体,其余 JSON 解析);
7
- * TS 载体 = 动态 import()(Node 原生类型剥离,插件加载同纪律——jiti 继续登记不引入);
7
+ * TS 载体 = **jiti 自包含转译**(1.6-S3——零传递依赖 bundled 形态,第二个常规运行时依赖;
8
+ * D-22「jiti 继续登记不引入」到期出清):Node 22.19+ 加载 car.config.ts 不再依赖
9
+ * --experimental-transform-types,非可擦除语法(enum 等 transform-only 形态)可用——
10
+ * erasable-only 预检登记项随之出清(被取代);插件加载的原生类型剥离纪律不动。
8
11
  * 形态 = `export default {…}`(非对象/缺 default 显式报错)。
9
12
  * - **预热协议**:loadCarConfig 保持同步签名(doctor/resolveSigGate 等既有调用面不动);
10
13
  * car.config.ts 须先经 warmCarConfig 异步预热入缓存(CLI 入口统一预热),未预热显式报错
@@ -20,7 +23,6 @@
20
23
  */
21
24
  import { existsSync, readFileSync } from 'node:fs'
22
25
  import { isAbsolute, join } from 'node:path'
23
- import { pathToFileURL } from 'node:url'
24
26
  import { z } from 'zod'
25
27
  import type { SignatureGateOptions } from './sigGate.ts'
26
28
 
@@ -184,9 +186,10 @@ const tsCache = new Map<string, ConfigLoadResult>()
184
186
  let tsEpoch = 0
185
187
 
186
188
  /**
187
- * 1.5-S3(D-22):car.config.ts 载体预热(动态 import 与插件加载同纪律——Node 原生类型剥离,
188
- * Node 22 须 --experimental-transform-types / Node ≥23.6 原生支持;报错文案引导)。
189
- * CLI 入口统一预热;直接调用 loadCarConfig 的库方在存在 car.config.ts 时须先 await 本函数。
189
+ * 1.5-S3(D-22)→ 1.6-S3(D-24):car.config.ts 载体预热。
190
+ * jiti 自包含转译(每 epoch 新实例 + moduleCache 关 = 击穿语义与 1.5 等价)——
191
+ * Node 22.19+ 无需 --experimental-transform-types;直接调用 loadCarConfig 的库方
192
+ * 在存在 car.config.ts 时须先 await 本函数(CLI 入口统一预热,协议不变)。
190
193
  */
191
194
  export async function warmCarConfig(opts: { explicitPath?: string; cwd?: string } = {}): Promise<void> {
192
195
  let path: string | undefined
@@ -203,9 +206,14 @@ export async function warmCarConfig(opts: { explicitPath?: string; cwd?: string
203
206
  tsCache.set(path, result)
204
207
  }
205
208
 
206
- async function loadTsConfigFile(path: string, epoch: number): Promise<ConfigLoadResult> {
209
+ async function loadTsConfigFile(path: string, _epoch: number): Promise<ConfigLoadResult> {
207
210
  try {
208
- const mod = (await import(`${pathToFileURL(path).href}?cfg-epoch=${epoch}`)) as { default?: unknown }
211
+ // D-24:jiti 转译(零传递依赖 bundled);每 epoch 新实例 + moduleCache 关——击穿语义保持;
212
+ // interopDefault 显式关(jiti 缺省 true 会把无 default 模块的 exports 整体当 default 返回,
213
+ // 「缺 default export」fail-visible 校验即失效)
214
+ const { createJiti } = await import('jiti')
215
+ const jiti = createJiti(import.meta.url, { moduleCache: false, fsCache: false, interopDefault: false })
216
+ const mod = await jiti.import(path) as { default?: unknown }
209
217
  const raw = mod.default
210
218
  if (raw === undefined) {
211
219
  return { path, config: {}, error: `CAR-E-CONFIG: ${path} 缺 default export(car.config.ts 形态 = export default {…})` }
@@ -217,10 +225,7 @@ async function loadTsConfigFile(path: string, epoch: number): Promise<ConfigLoad
217
225
  return v.error ? { path, config: {}, error: v.error } : { path, config: v.config }
218
226
  } catch (e) {
219
227
  const err = e as { code?: string; message?: string }
220
- const hint = err.code === 'ERR_UNKNOWN_FILE_EXTENSION' || /Unknown file extension|loader/i.test(err.message ?? '')
221
- ? `——当前 Node 未启用 .ts 载体(须 --experimental-transform-types 或 Node ≥23.6 原生类型剥离)`
222
- : ''
223
- return { path, config: {}, error: `CAR-E-CONFIG: car.config.ts 载入失败(${err.message})${hint}` }
228
+ return { path, config: {}, error: `CAR-E-CONFIG: car.config.ts 载入失败(${err.message ?? String(e)})——jiti 转译失败显式拒绝,不静默忽略` }
224
229
  }
225
230
  }
226
231
 
@@ -21,6 +21,17 @@
21
21
  * 本面是 OTel 形态的出站通道,不承接 S17 的零内容红线语义(内容治理属调用方);
22
22
  * - 零依赖:OTel「标准句柄形态」自研(startSpan/attributes/end、createCounter/add),
23
23
  * 不引入 @opentelemetry/* 依赖(项目零依赖红线,README engines 纪律)。
24
+ *
25
+ * 1.7(D-26 span 语义标准化 / D-27 metrics 时序修正):
26
+ * - recordException → OTel 规范形态:span event(name='exception',exception.type/exception.message
27
+ * 于事件属性)+ span status → STATUS_CODE_ERROR;stacktrace 缺省不出站(防路径/源码面外泄——
28
+ * 隐私三原则正向收紧;可选开关不随本迭代引入);正常 span 恒 STATUS_CODE_UNSET(行为不变面);
29
+ * - addEvent → 标准 events 数组编码(name + attributes 原形态 + 事件时刻 timeUnixNano——
30
+ * 替换 1.1 起的 `event.<name>` JSON 属性串非标准形态);
31
+ * - 出站体 spans[] 加法字段 events/status(空形态合法——无事件 span 不出 events 键);
32
+ * - metrics 累计基数保持:flush 不再清空 counters——每次导出全量累计快照(真 CUMULATIVE)。
33
+ * 1.2-S3 起 clear() 与 AGGREGATION_TEMPORALITY_CUMULATIVE 标注矛盾:intervalMs>0 自动导出时
34
+ * 下游把增量解读为总量回落;内存有界由 label 基数保证(零内容枚举红线——AllowedLabels 3 计数器)。
24
35
  */
25
36
  import { randomBytes } from 'node:crypto'
26
37
  import { request as httpRequest } from 'node:http'
@@ -153,6 +164,10 @@ interface SpanRecord {
153
164
  startMs: number
154
165
  endMs: number
155
166
  attributes: Record<string, string | number | boolean>
167
+ /** 1.7-S1(D-26):标准 span events(name + 原形态属性 + 事件时刻) */
168
+ events: Array<{ name: string; atMs: number; attributes: Record<string, string | number | boolean> }>
169
+ /** 1.7-S1(D-26):'STATUS_CODE_UNSET'(缺省)| 'STATUS_CODE_ERROR'(recordException) */
170
+ status: 'STATUS_CODE_UNSET' | 'STATUS_CODE_ERROR'
156
171
  }
157
172
 
158
173
  const attrValue = (v: string | number | boolean) =>
@@ -273,7 +288,15 @@ export function createTelemetryFacade(
273
288
  startTimeUnixNano: String(s.startMs * 1e6),
274
289
  endTimeUnixNano: String(s.endMs * 1e6),
275
290
  attributes: Object.entries(s.attributes).map(([key, v]) => ({ key, value: attrValue(v) })),
276
- status: { code: 'STATUS_CODE_UNSET' },
291
+ // 1.7-S1(D-26):标准 events/status 加法字段(无事件 span 不出 events 键——空形态合法)
292
+ ...(s.events.length ? {
293
+ events: s.events.map(e => ({
294
+ timeUnixNano: String(e.atMs * 1e6),
295
+ name: e.name,
296
+ attributes: Object.entries(e.attributes).map(([key, v]) => ({ key, value: attrValue(v) })),
297
+ })),
298
+ } : {}),
299
+ status: { code: s.status },
277
300
  })),
278
301
  }],
279
302
  }
@@ -282,7 +305,9 @@ export function createTelemetryFacade(
282
305
  }
283
306
  if (counters.size) {
284
307
  const batch = [...counters.entries()]
285
- counters.clear()
308
+ // 1.7-S2(D-27):累计基数保持——不清空,每次导出全量累计快照(真 CUMULATIVE)。
309
+ // 1.2-S3 起 clear() 使后续导出仅含增量却标 CUMULATIVE——intervalMs>0 时下游解读为总量回落;
310
+ // 内存有界由 label 基数保证(零内容枚举红线:AllowedLabels 3 计数器)
286
311
  const body = {
287
312
  resource: { attributes: [{ key: 'service.name', value: { stringValue: serviceName } }] },
288
313
  scopeMetrics: [{
@@ -319,13 +344,29 @@ export function createTelemetryFacade(
319
344
  startMs: now(),
320
345
  endMs: 0,
321
346
  attributes: { ...startOpts?.attributes },
347
+ events: [],
348
+ status: 'STATUS_CODE_UNSET',
322
349
  }
323
350
  return {
324
351
  get traceId() { return record.traceId },
325
352
  get spanId() { return record.spanId },
326
353
  setAttribute(key, value) { record.attributes[key] = value; return this },
327
- addEvent(evtName, attrs) { record.attributes[`event.${evtName}`] = attrs ? JSON.stringify(attrs) : evtName; return this },
328
- recordException(err) { record.attributes['exception.message'] = String((err as Error)?.message ?? err); return this },
354
+ // 1.7-S1(D-26):标准 events 编码——原形态属性 + 事件时刻(替换 event.<name> JSON 串)
355
+ addEvent(evtName, attrs) { record.events.push({ name: evtName, atMs: now(), attributes: attrs ? { ...attrs } : {} }); return this },
356
+ // 1.7-S1(D-26):OTel 规范 exception 形态——exception.type/exception.message 事件属性
357
+ // + status ERROR;stacktrace 缺省不出站(D-26 口径:防路径/源码面外泄)
358
+ recordException(err) {
359
+ record.events.push({
360
+ name: 'exception',
361
+ atMs: now(),
362
+ attributes: {
363
+ 'exception.type': String((err as Error)?.name ?? 'Error'),
364
+ 'exception.message': String((err as Error)?.message ?? err),
365
+ },
366
+ })
367
+ record.status = 'STATUS_CODE_ERROR'
368
+ return this
369
+ },
329
370
  end() {
330
371
  record.endMs = now()
331
372
  stats.spansEnded++
@@ -7,6 +7,11 @@
7
7
  * 「内容治理属调用方」口径);设计微调登记:规划原文「runTurn opts.telemetry 注入面」改为装配层
8
8
  * 闭包挂点,语义等价(turn/step 层级与出站面不变),loop 层保持纯停止语义;
9
9
  * - attributes 零内容面:turn_id/step 序号/outcome 枚举——无会话内容/提示词/路径。
10
+ *
11
+ * 1.7-S3(D-28):mcp-serve turn span 面——sessionTurn 挂 turn 级 span(无 step span:mcp-serve
12
+ * 无 model() 循环,step span 面外推登记为不适用)。facade 内部自增派生 turnId(T{n}),装配层
13
+ * 调用前不可知——TurnSpanHandle 加法 `setTurnId` 回填面(car run 直传 turnId 路径不受影响);
14
+ * startTurn('') 空值不设 car.turn_id 属性(回填前出站形态无占位内容)。
10
15
  */
11
16
  import { randomBytes } from 'node:crypto'
12
17
  import type { TelemetryFacade } from './telemetry.ts'
@@ -19,6 +24,10 @@ export interface StepSpanHandle {
19
24
  export interface TurnSpanHandle {
20
25
  traceId: string
21
26
  spanId: string
27
+ /** 1.7-D28:turnId 事后回填(mcp-serve——facade 内部自增派生,调用前不可知;car run 直传不受影响) */
28
+ setTurnId(turnId: string): void
29
+ /** 1.7-D28:turn 级异常留痕(D-26 exception 形态在 turn span 同样成立) */
30
+ recordException(e: unknown): void
22
31
  end(outcome: string): void
23
32
  stepSpan(step: number): StepSpanHandle
24
33
  }
@@ -33,14 +42,16 @@ export class TurnTracer {
33
42
 
34
43
  startTurn(turnId: string): TurnSpanHandle {
35
44
  if (!this.#facade) {
36
- return { traceId: '', spanId: '', end() {}, stepSpan: () => ({ end() {}, recordException() {} }) }
45
+ return { traceId: '', spanId: '', setTurnId() {}, recordException() {}, end() {}, stepSpan: () => ({ end() {}, recordException() {} }) }
37
46
  }
38
47
  const tracer = this.#facade.getTracer()
39
48
  const traceId = randomBytes(16).toString('hex')
40
- const turn = tracer.startSpan('car.turn', { attributes: { 'car.turn_id': turnId }, trace: { traceId } })
49
+ const turn = tracer.startSpan('car.turn', { attributes: turnId ? { 'car.turn_id': turnId } : {}, trace: { traceId } })
41
50
  return {
42
51
  traceId,
43
52
  spanId: turn.spanId,
53
+ setTurnId(id) { turn.setAttribute('car.turn_id', id) },
54
+ recordException(e) { turn.recordException(e) },
44
55
  end(outcome) {
45
56
  turn.setAttribute('car.outcome', outcome)
46
57
  turn.end()
@@ -4,6 +4,7 @@
4
4
  * 设计口径:
5
5
  * - 哈希链(SHA-256)为 CAR 自研增强(dsh 无此物,源码级核实)——合规卖点落地
6
6
  * - 「Model-visible means logged」:断言口径 = 请求时快照 === 日志前缀 [0, atSeq) 投影
7
+ * - 1.6-S1(D-23):投影对同 id toolResult 收敛 last-wins——事件面/审计面不动,模型可见流去冗余
7
8
  * - append-only:本层仅暴露 append/verifyChain/deriveMessages/assertModelVisibleLogged
8
9
  */
9
10
  import { createHash } from 'node:crypto'
@@ -83,16 +84,33 @@ export class SessionLog {
83
84
  return null
84
85
  }
85
86
 
86
- /** 投影:从日志前缀重建模型可见消息流(不依赖模型运行时状态) */
87
+ /**
88
+ * 投影:从日志前缀重建模型可见消息流(不依赖模型运行时状态)。
89
+ * 1.6-S1(D-23)同 id toolResult 收敛 last-wins:写类工具授权决策(runtime/toolResult)
90
+ * 与执行结果(plugin/toolResult)成对落链(M4 既有语义冻结——审计全量留痕不动),
91
+ * 模型可见流只保留最终结果(真实 provider 对重复 tool_call_id/tool_use_id 拒绝或未定义,
92
+ * 1.4 观察项经真实适配器落地升级为缺陷面)。id 缺席不收敛(hostRaw/无 id 载荷形态不变,
93
+ * golden ④ 投影语义兼容);授权拒绝单条即唯一结果原样保留。适配器侧零改动(消费已收敛上游)。
94
+ */
87
95
  deriveMessages(upTo?: number): Array<{ role: string; [k: string]: unknown }> {
96
+ const scoped = upTo !== undefined ? this.events.slice(0, upTo) : this.events
97
+ const lastSeqById = new Map<string, number>()
98
+ for (const e of scoped) {
99
+ if (e.kind !== 'toolResult') continue
100
+ const id = (e.payload as { id?: unknown } | null)?.id
101
+ if (typeof id === 'string') lastSeqById.set(id, e.seq)
102
+ }
88
103
  const out: Array<{ role: string; [k: string]: unknown }> = []
89
- for (const e of this.events) {
90
- if (upTo !== undefined && e.seq >= upTo) break
104
+ for (const e of scoped) {
91
105
  if (e.kind === 'system') out.push({ role: 'system', content: e.payload })
92
106
  else if (e.kind === 'user') out.push({ role: 'user', content: e.payload })
93
107
  else if (e.kind === 'assistant') out.push({ role: 'assistant', content: e.payload })
94
108
  else if (e.kind === 'toolCall') out.push({ role: 'assistant', toolCall: e.payload })
95
- else if (e.kind === 'toolResult') out.push({ role: 'toolResult', content: e.payload })
109
+ else if (e.kind === 'toolResult') {
110
+ const id = (e.payload as { id?: unknown } | null)?.id
111
+ if (typeof id === 'string' && lastSeqById.get(id) !== e.seq) continue // D-23:后续有同 id 最终结果,本条为授权中间记录
112
+ out.push({ role: 'toolResult', content: e.payload })
113
+ }
96
114
  }
97
115
  return out
98
116
  }