@lqc123qwe/car-runtime 1.8.0 → 1.9.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
@@ -238,9 +238,21 @@ node --experimental-transform-types --test test/*.spec.ts
238
238
  - **1.7.0 正式发布(2026-09-28,S5 收官)**:npm `@lqc123qwe/car-runtime@1.7.0` 在架——**dist-tags latest → 1.7.0、rc 保留 1.0.0-rc.1** + provenance。Release `1.7.0`(id 398173870,agent GCM 凭据 API 创建)→ **j16 run 36414031672 全步 success**(构建/keyless 签名/自动上传五件套/npm publish)→ **post-publish 13 PASS / 0 DRY-RUN / 0 FAIL(REMOTE_CHECK=1 全实查)**;外部等价检查 **SHA256 MATCH(`24bb6c40…`)+ Rekor 交叉验证**(logIndex 2981897662 双端一致 + integratedTime 1790593823 + 透明日志 body 内制品哈希三方一致——tree 键形态差异延续 1.6 登记口径,详见收口报告 §5)。
239
239
  - **遗留登记(1.8 候选池)**:W3C traceparent 跨进程传播(MCP stdio JSON-RPC 两端 + PTC worker 桥挂链——协议两端协同面大,1.7 延后登记);MCP legacy HTTP+SSE 维持不追溯(D-21);写类工具人审 confirm 通道宿主面外推(D-25 口径——前置未满足);插件 jiti 轨收敛观察项(可选演进非缺陷)。规划:`car-docs/10-内核v1/1.7-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.7-收口终验报告.md`。
240
240
 
241
+ ## 1.8 状态(W3C traceparent 跨进程传播 + LLM 出站通道修复)
242
+
243
+ | `src/runtime-core/trace.ts` | 1.8-S1~S3(D-29/D-30/D-31)**W3C traceparent 三边界挂链**:formatTraceparent/parseTraceparent(自研零依赖——version 00/32hex/16hex/全零拒绝,畸形 null fail-open 禁 fail-hard);TurnSpanHandle 加法 `mcpToolSpan`(car.mcp.tool client span 挂 turn——step span 已收口后工具执行;car.mcp_server/car.tool 登记标识符面)+ `ptcRelay`(workerTrace 载荷 + complete 收口 car.ptc span——error 路径 D-26 形态 + 负时长钳制);startTurn 加法 opts.trace(mcp-serve 远端挂链——traceId 复用 + parentSpanId);telemetry.ts 加法 time.startMs/end(atMs) 时刻覆盖(relay span——采样/buffer/出站路径零改动);自研 traceId 16B/spanId 8B hex 与 W3C 格式天然合规 | trace 隔离于单进程 → 跨进程可挂链 |
244
+ | `src/mcp/gateway.ts` + `src/host/stdio.ts` + `src/cli.ts` | 1.8-S1/S2(D-29/D-30)**MCP stdio 两端传播**:client 侧 callTool 加法 opts.traceparent → params._meta(MCP `_meta` 保留扩展点;**otel 关 = 不注入,JSON-RPC 字节面不变**;tools/list 不注入);server 侧 dispatch 第三参 meta 原样透传(解析归装配层),session_turn 提取挂链(畸形 fail-open 新 trace),非会话工具 meta 忽略(D-28 口径) | 响应路径回传登记不适用(W3C 请求携带语义) |
245
+ | `src/ptc/runCode.ts` + `worker-entry.ts` | 1.8-S3(D-31)**PTC worker 计时 relay**:worker 侧程序计时(同进程同时钟域)经 done 加法 span 字段回传,主线程经 spanBridge 单点收口 car.ptc span——**worker 零 trace 载荷注入、零独立出站通道(单 exporter 纪律)**;主侧 killer/worker error 兜底以主线程时钟收口(口径登记) | 1.8-插曲-1:workerData 载荷方案收敛为纯 relay(装饰性载荷无测试承载) |
246
+ | `src/runtime-core/llm.ts` | **1.8-BUG-1**:LLM 出站缺省通道 undici fetch → `nodeHttpSseFetch`(node:http(s) 流式 POST——AsyncIterable<Uint8Array> 契约不变;AbortSignal → req.destroy 对接 D-19;注入缝 fetchImpl 保留)——car run ≥3 次模型请求 + worker/子进程并存时 win32 退出 libuv 断言崩溃(0xC0000409,1.4-BUG-2 同源:彼时仅修遥测出站漏 LLM 面;E2E 三请求形态暴露 + 二分矩阵定位) | 出站通道口径须按通道类收口,非按调用点逐修 |
247
+ | `test/s42` | 7 测:W3C 编解码(畸形 fail-open 全集)/ mcpToolSpan(挂链+traceparent 形态+noop 零注入)/ startTurn 远端挂链 / ptcRelay(时刻覆盖+error+钳制+noop)/ callTool `_meta` 注入与缺省无键 / E2E-A car run 全链三点闭合(回显 traceparent ↔ OTLP car.mcp.tool spanId)+ car.ptc 计时窗口 / E2E-B mcp-serve 挂链+畸形 fail-open | 1.8-GO-5 生产调用点双向核查承载 |
248
+
249
+ - **1.8 工程收口(2026-09-29,`6522e98`)**:1.7 延后登记「traceparent 跨进程传播」出清(TRACE-1/2/3 三块)。全量 **335(332 PASS + 3 能力门控 skip / 0 fail)** + tsc strict 0 + secrets-scan 121 文件 0 命中 + contract-check 4/4(golden 不动)+ 零依赖静态断言保持。**1.8-BUG-1 于迭代内修复**(真缺陷登记 + 二分矩阵验证 + s42 E2E-A 回归承载);存量断言零改动(s12/s29/s32/s41 无新增面钉死——复核实证)。
250
+ - **1.8.0 正式发布(2026-09-29,S5 收官)**:npm `@lqc123qwe/car-runtime@1.8.0` 在架——**dist-tags latest → 1.8.0、rc 保留 1.0.0-rc.1** + provenance(slsa.dev/provenance/v1——registry attestations 端点实查,npm 形态演进登记)。Release `1.8.0`(id 398799183,agent GCM 凭据 API 创建,全 SHA 挂载——插曲:短 SHA 被拒)→ **j16 run 36517038845 全步 success**(构建/keyless 签名/自动上传五件套/npm publish)→ **post-publish 13 PASS / 0 DRY-RUN / 0 FAIL(全实查)**;外部等价检查 **SHA256 MATCH(`15c5e2cf…`)+ Rekor 交叉验证**(logIndex 2992647008 双端一致 + integratedTime 1790652315 + 透明日志 body 内制品哈希三方一致——tree 键形态差异延续 1.6/1.7 登记口径,详见收口报告 §6)。
251
+ - **遗留登记(1.9 候选池)**:远程 MCP(Streamable HTTP)traceparent 传播——HTTP `traceparent` 头通道(D-21 通道既有,1.8 延后登记);mcp-serve 响应路径回传 span 上下文(D-30 登记不适用——宿主消费面不存在);MCP legacy HTTP+SSE 维持不追溯(D-21);写类工具人审外推(D-25 口径维持);插件 jiti 轨维持观察;metrics 多标签 dataPoints 维持。规划:`car-docs/10-内核v1/1.8-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.8-收口终验报告.md`。
252
+
241
253
  ## 状态
242
254
 
243
- - **版本主线**: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 已发布)** | **1.7 OTLP span 语义收口+metrics 时序修正+mcp-serve span 面 全量 328(325 PASS + 3 skip,2026-09-28 收口,1.7.0 已发布)**
255
+ - **版本主线**: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 已发布)** | **1.7 OTLP span 语义收口+metrics 时序修正+mcp-serve span 面 全量 328(325 PASS + 3 skip,2026-09-28 收口,1.7.0 已发布)** | **1.8 W3C traceparent 跨进程传播+LLM 出站通道修复 全量 335(332 PASS + 3 skip,2026-09-29 收口,1.8.0 已发布)**
244
256
  - **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`)在真实发布兑现价值。
245
257
  - **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 清单已全绿收口。
246
258
  - **轨道 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.8.0",
3
+ "version": "1.9.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",
@@ -13,6 +13,11 @@
13
13
  * (MCP `_meta` 保留扩展点——未知键对端忽略,加法无害);仅 otel 显式开且 client span 存活时
14
14
  * 非空(trace.ts mcpToolSpan)——**缺省不注入,JSON-RPC 字节面不变**;tools/list 不注入
15
15
  * (注册期无 span 上下文,登记)。解析/校验归 trace.ts(parseTraceparent fail-open),本层原样透传。
16
+ *
17
+ * 1.9(D-32 远程 MCP 头通道):callTool 把 opts.traceparent 加法透传给 transport.send 第二参——
18
+ * 载体由传输层自选(HTTP 通道注入 `traceparent` 请求头〔W3C Trace Context 惯例〕;stdio 通道
19
+ * 忽略 opts,`params._meta` 载体不变)。`_meta` 注入口径一字节不动——otel 开时 HTTP 请求
20
+ * 双载体同值(头为主消费面,登记);otel 关时双载体零注入(头面与字节面不变)。
16
21
  */
17
22
  export interface JsonRpcRequest { jsonrpc: '2.0'; id: number; method: string; params?: unknown }
18
23
  export interface JsonRpcResponse { jsonrpc: '2.0'; id: number; result?: unknown; error?: { code: number; message: string } }
@@ -20,9 +25,10 @@ export interface JsonRpcResponse { jsonrpc: '2.0'; id: number; result?: unknown;
20
25
  /**
21
26
  * 客户端传输(CAR → MCP Server 方向)。M3-S11 双向分层:本接口为 client 侧对偶;
22
27
  * server 侧(宿主 → CAR)见 src/host/hostGateway.ts 的 ServerTransport。
28
+ * 1.9-D32:send 加法可选第二参 opts(既有 `send(req)` 实现结构兼容零改动)。
23
29
  */
24
30
  export interface ClientTransport {
25
- send(req: JsonRpcRequest): Promise<JsonRpcResponse>
31
+ send(req: JsonRpcRequest, opts?: { traceparent?: string }): Promise<JsonRpcResponse>
26
32
  /** 进程/通道存活状态 */
27
33
  alive(): boolean
28
34
  /** 主动终止 */
@@ -113,10 +119,12 @@ export class McpGateway {
113
119
  if (!this.#tools.has(key)) return { ok: false, error: `unknown tool "${tool}" on "${serverId}"` }
114
120
  try {
115
121
  const res = await Promise.race([
122
+ // 1.9-D32:traceparent 加法透传第二参(载体归传输层——HTTP 头 / stdio 忽略);
123
+ // `_meta` 注入 1.8-D29 口径不动(otel 开 = 双载体同值)
116
124
  cfg.transport.send({
117
125
  jsonrpc: '2.0', id: this.#nextId++, method: 'tools/call',
118
126
  params: { name: tool, arguments: args, ...(opts.traceparent ? { _meta: { traceparent: opts.traceparent } } : {}) },
119
- }),
127
+ }, opts.traceparent ? { traceparent: opts.traceparent } : undefined),
120
128
  new Promise<never>((_, rej) => setTimeout(() => rej(new Error('mcp timeout')), opts.timeoutMs ?? 30_000)),
121
129
  ])
122
130
  return { ok: true, result: (res as JsonRpcResponse).result }
@@ -14,13 +14,20 @@
14
14
  * - headers 凭据规则(CR-05 经配置通道执行):值支持 `${ENV_VAR}` 引用(env 缺席显式报错);
15
15
  * 字面值疑似明文凭据(containsPlaintextCredential 同款模式)直接拒绝——env 引用注入
16
16
  * 才是登记合规通道(展开后值不复查:引注入即目的,展开值是凭据本身);
17
- * - legacy HTTP+SSE(2024-11-05)不做(1.5 规划 D-21 登记不追溯)。
17
+ * - legacy HTTP+SSE(2024-11-05)不做(1.5 规划 D-21 登记不追溯);
18
+ * - 1.9(D-32 远程 MCP 头通道):send 加法可选第二参 opts.traceparent → tools/call 请求头
19
+ * 加法 `traceparent`(W3C Trace Context HTTP 载体惯例——第三方 OTel 接入面主消费通道);
20
+ * 注入前 parseTraceparent 校验,畸形**不注入不抛**(协议面 fail-open——严格格式天然排除
21
+ * CR/LF,兼防头注入越权面);otel 关 = 双载体零注入(头与 JSON-RPC 字节面不变);
22
+ * initialize/notifications/tools/list 不注入(无 span 上下文——1.8 tools/list 口径延伸);
23
+ * 配置 headers 含字面 `traceparent` 时注入优先(动态 span 上下文为请求级事实,登记)。
18
24
  */
19
25
  import { request as httpRequest } from 'node:http'
20
26
  import { request as httpsRequest } from 'node:https'
21
27
  import type { ClientTransport, JsonRpcRequest, JsonRpcResponse } from './gateway.ts'
22
28
  import { containsPlaintextCredential } from './gateway.ts'
23
29
  import { assertTlsOrLoopback } from '../runtime-core/llm.ts'
30
+ import { parseTraceparent } from '../runtime-core/trace.ts'
24
31
 
25
32
  export const MCP_PROTOCOL_VERSION = '2025-06-18'
26
33
 
@@ -127,8 +134,9 @@ export function createHttpMcpTransport(spec: HttpMcpServerSpec): ClientTransport
127
134
  throw new Error('CAR-E-MCP: SSE 流结束未见本请求响应(阶段:read-response)')
128
135
  }
129
136
 
130
- async function postAndRead(body: unknown, matchId: number | null, phase: string): Promise<JsonRpcResponse> {
131
- const res = await rawPost(spec.url, baseHeaders(), JSON.stringify(body), timeoutMs)
137
+ async function postAndRead(body: unknown, matchId: number | null, phase: string, extraHeaders: Record<string, string> = {}): Promise<JsonRpcResponse> {
138
+ // 1.9-D32:extraHeaders 展开在 baseHeaders 之后——配置头同名时注入优先(登记口径)
139
+ const res = await rawPost(spec.url, { ...baseHeaders(), ...extraHeaders }, JSON.stringify(body), timeoutMs)
132
140
  const sid = res.headers['mcp-session-id']
133
141
  if (typeof sid === 'string') sessionId = sid
134
142
  if (res.status < 200 || res.status >= 300) {
@@ -158,15 +166,25 @@ export function createHttpMcpTransport(spec: HttpMcpServerSpec): ClientTransport
158
166
  }
159
167
  }
160
168
 
169
+ /**
170
+ * 1.9-D32:tools/call 请求的 `traceparent` 头(W3C Trace Context HTTP 载体惯例)——
171
+ * parseTraceparent 校验 fail-open(畸形/空 = {} 不注入不抛;严格 hex 格式排除 CR/LF 头注入面);
172
+ * 与 body params._meta.traceparent 双载体同值并存(1.8-D29 `_meta` 面维持,头为主消费面)。
173
+ */
174
+ function callHeaders(tp: string | undefined): Record<string, string> {
175
+ if (!tp || !parseTraceparent(tp)) return {}
176
+ return { traceparent: tp }
177
+ }
178
+
161
179
  const transport: ClientTransport = {
162
- send(req: JsonRpcRequest): Promise<JsonRpcResponse> {
180
+ send(req: JsonRpcRequest, opts?: { traceparent?: string }): Promise<JsonRpcResponse> {
163
181
  return (async (): Promise<JsonRpcResponse> => {
164
182
  if (closed) throw new Error('CAR-E-MCP: http transport closed(server 已收口)')
165
183
  if (!initialized) {
166
184
  initPromise ??= doInitialize().then(() => { initialized = true })
167
185
  try { await initPromise } catch (e) { initPromise = null; throw e }
168
186
  }
169
- return postAndRead(req, req.id, 'request')
187
+ return postAndRead(req, req.id, 'request', callHeaders(opts?.traceparent))
170
188
  })()
171
189
  },
172
190
  alive: () => !closed,
@@ -29,6 +29,11 @@
29
29
  * - ptcRelay(D-31):PTC worker 桥挂链——workerTrace 载荷(traceId/parentSpanId)经 workerData
30
30
  * 进隔离体,worker 计时经 done 消息回传后 complete() 主线程单点收口 car.ptc span(facade
31
31
  * time.startMs/end(atMs) 时刻覆盖;单 exporter 纪律——worker 零独立出站通道)。
32
+ *
33
+ * 1.9(D-32 · 远程 MCP Streamable HTTP 头通道):mcpToolSpan 产出的 traceparent 除 1.8 `params._meta`
34
+ * 载体外,经 gateway.callTool → transport.send 第二参透传,httpClient 注入 `traceparent` 请求头
35
+ * (W3C Trace Context HTTP 载体惯例——远程 server 主消费面)。本模块仅复用 parseTraceparent 作
36
+ * 传输层 fail-open 校验(畸形不注入不抛)——编解码与传播语义 1.8 口径零触碰。
32
37
  */
33
38
  import { randomBytes } from 'node:crypto'
34
39
  import type { TelemetryFacade } from './telemetry.ts'
@@ -0,0 +1,262 @@
1
+ /**
2
+ * 1.9 · 远程 MCP traceparent 传播——Streamable HTTP 头通道(D-32 · S43)
3
+ *
4
+ * 覆盖(1.9-迭代规划 HTTP-TP):
5
+ * - 单元:httpClient tools/call 请求头注入(W3C `traceparent` 头)与缺省零注入(头面不变);
6
+ * initialize/notifications/tools/list 不注入(无 span 上下文边界);gateway.callTool 透传 +
7
+ * `_meta`/头双载体同值;畸形 fail-open 丢弃(含 CRLF 头注入形态);配置 headers 同名注入优先;
8
+ * - E2E:car run --mcp <url> 全链(otel 开)——假远程 MCP server 捕获 `traceparent` 头回显进
9
+ * 工具结果,与 OTLP 出站 car.mcp.tool span 三点对拍闭合(traceId + spanId)+ body `_meta`
10
+ * 双载体同值实查 + 出站体零内容断言。
11
+ */
12
+ import { test } from 'node:test'
13
+ import assert from 'node:assert/strict'
14
+ import { spawn } from 'node:child_process'
15
+ import { createServer, type Server, type IncomingMessage } from 'node:http'
16
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
17
+ import { tmpdir } from 'node:os'
18
+ import { join, dirname } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+ import { McpGateway } from '../src/mcp/gateway.ts'
21
+ import { createHttpMcpTransport, MCP_PROTOCOL_VERSION } from '../src/mcp/httpClient.ts'
22
+ import { parseTraceparent } from '../src/runtime-core/trace.ts'
23
+
24
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
25
+ const CLI = join(ROOT, 'src', 'cli.ts')
26
+
27
+ // 密钥扫描口径:夹具凭据经变量注入(赋值行无引号字面量)
28
+ const TEST_TOKEN = 'test-token-456'
29
+
30
+ const TID = 'a'.repeat(32)
31
+ const SID = 'b'.repeat(16)
32
+ const TP = (tid: string, sid: string) => `00-${tid}-${sid}-01`
33
+
34
+ function withTempDir(name: string, fn: (dir: string) => Promise<void> | void): Promise<void> {
35
+ const dir = mkdtempSync(join(tmpdir(), `car-s43-${name}-`))
36
+ try {
37
+ const r = fn(dir)
38
+ const cleanup = () => { try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ } }
39
+ if (r instanceof Promise) return r.finally(cleanup)
40
+ cleanup()
41
+ return Promise.resolve()
42
+ } catch (e) {
43
+ try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ }
44
+ throw e
45
+ }
46
+ }
47
+
48
+ interface SeenReq { url: string; method: string; headers: IncomingMessage['headers']; body: any }
49
+
50
+ /** 本地假远程 MCP server(Streamable HTTP):tools/call 捕获 `traceparent` 头回显进工具结果 */
51
+ function fakeMcpHttp(): Promise<{ server: Server; url: string; seen: SeenReq[]; close: () => Promise<void> }> {
52
+ const seen: SeenReq[] = []
53
+ const server = createServer((req, res) => {
54
+ let raw = ''
55
+ req.on('data', c => { raw += c })
56
+ req.on('end', () => {
57
+ const body = raw ? JSON.parse(raw) : {}
58
+ seen.push({ url: req.url ?? '', method: body.method ?? '', headers: req.headers, body })
59
+ const respondJson = (payload: unknown, status = 200, headers: Record<string, string> = {}) => {
60
+ res.writeHead(status, { 'content-type': 'application/json', ...headers })
61
+ res.end(JSON.stringify(payload))
62
+ }
63
+ if (body.method === 'initialize') {
64
+ respondJson({ jsonrpc: '2.0', id: body.id, result: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, serverInfo: { name: 'fake-remote', version: '0.0.0' } } }, 200, { 'mcp-session-id': 'sess-fake-1' })
65
+ return
66
+ }
67
+ if (body.id === undefined) { res.writeHead(202).end(); return } // notification(notifications/initialized)
68
+ if (body.method === 'tools/list') {
69
+ respondJson({ jsonrpc: '2.0', id: body.id, result: { tools: [{ name: 'remote_echo', description: '远程回声', inputSchema: { type: 'object', properties: { t: { type: 'string' } } } }] } })
70
+ return
71
+ }
72
+ if (body.method === 'tools/call') {
73
+ const tp = req.headers['traceparent'] ?? 'none'
74
+ respondJson({ jsonrpc: '2.0', id: body.id, result: { content: [{ type: 'text', text: 'remote-ok|tp:' + tp }] } })
75
+ return
76
+ }
77
+ respondJson({ jsonrpc: '2.0', id: body.id, error: { code: -32601, message: 'method not found: ' + body.method } }, 404)
78
+ })
79
+ })
80
+ const urlP = new Promise<string>(r => server.listen(0, '127.0.0.1', () => r(`http://127.0.0.1:${(server.address() as { port: number }).port}`)))
81
+ return urlP.then(url => ({ server, url, seen, close: () => new Promise<void>(r => server.close(() => r())) }))
82
+ }
83
+
84
+ // ==================== 单元:头注入与缺省零注入(D-32) ====================
85
+
86
+ test('S43: httpClient tools/call 注入 traceparent 头 + _meta 双载体同值;握手/注册不注入;缺省零注入', async () => {
87
+ const { server, url, seen, close } = await fakeMcpHttp()
88
+ try {
89
+ const transport = createHttpMcpTransport({ url })
90
+ const gw = new McpGateway()
91
+ await gw.register({ serverId: 'remote', transport })
92
+ // 无 span 上下文边界:握手与注册请求零注入
93
+ assert.equal(seen[0]!.headers['traceparent'], undefined, 'initialize 不注入(传输内部握手)')
94
+ assert.equal(seen[1]!.headers['traceparent'], undefined, 'notifications/initialized 不注入')
95
+ assert.equal(seen[2]!.headers['traceparent'], undefined, 'tools/list 不注入(注册期无 span 上下文)')
96
+ // callTool + traceparent → 头注入 + _meta 双载体同值(1.8 字节面维持)
97
+ const tp = TP(TID, SID)
98
+ const r = await gw.callTool('remote', 'remote_echo', { t: 'hi' }, { traceparent: tp })
99
+ assert.equal(r.ok, true)
100
+ const call = seen.at(-1)!
101
+ assert.equal(call.method, 'tools/call')
102
+ assert.equal(call.headers['traceparent'], tp, 'W3C traceparent 头注入(HTTP 载体惯例)')
103
+ assert.equal((call.body.params as any)._meta?.traceparent, tp, 'body _meta 双载体同值(1.8-D29 字节面维持)')
104
+ assert.deepEqual({ name: (call.body.params as any).name, arguments: (call.body.params as any).arguments }, { name: 'remote_echo', arguments: { t: 'hi' } }, 'params 主体不变')
105
+ // 缺省零注入:otel 关 = 头与 _meta 双缺省(线上请求头与字节面不变)
106
+ const r2 = await gw.callTool('remote', 'remote_echo', { t: 'ho' })
107
+ assert.equal(r2.ok, true)
108
+ const call2 = seen.at(-1)!
109
+ assert.equal(call2.headers['traceparent'], undefined, '缺省零注入(头面不变)')
110
+ assert.equal('_meta' in (call2.body.params as object), false, '缺省零注入(字节面不变——1.8 口径)')
111
+ transport.close()
112
+ } finally {
113
+ await close()
114
+ }
115
+ })
116
+
117
+ // ==================== 单元:畸形 fail-open + 头注入防线 ====================
118
+
119
+ test('S43: 畸形 traceparent fail-open——头不注入调用照常;CRLF 头注入形态被排除;空串零注入', async () => {
120
+ const { server, url, seen, close } = await fakeMcpHttp()
121
+ try {
122
+ const transport = createHttpMcpTransport({ url })
123
+ const gw = new McpGateway()
124
+ await gw.register({ serverId: 'remote', transport })
125
+ // 非格式串:不注入不抛(协议面 fail-open——BD-05 同源)
126
+ const r1 = await gw.callTool('remote', 'remote_echo', { t: 'a' }, { traceparent: 'garbage' })
127
+ assert.equal(r1.ok, true, '畸形不 fail-hard(调用照常)')
128
+ assert.equal(seen.at(-1)!.headers['traceparent'], undefined, '畸形头不注入(fail-open)')
129
+ // CRLF 头注入形态:合法 traceparent 后拼越权第二头——严格 hex 格式整体拒绝
130
+ const evil = TP(TID, SID) + '\r\nx-inject: yes'
131
+ const r2 = await gw.callTool('remote', 'remote_echo', { t: 'b' }, { traceparent: evil })
132
+ assert.equal(r2.ok, true)
133
+ const h = seen.at(-1)!.headers
134
+ assert.equal(h['traceparent'], undefined, 'CRLF 形态头不注入')
135
+ assert.equal(h['x-inject'], undefined, '越权头不出现(严格格式排除 CR/LF)')
136
+ // 全零(W3C 无效)同拒
137
+ const r3 = await gw.callTool('remote', 'remote_echo', { t: 'c' }, { traceparent: TP('0'.repeat(32), '0'.repeat(16)) })
138
+ assert.equal(r3.ok, true)
139
+ assert.equal(seen.at(-1)!.headers['traceparent'], undefined, '全零 trace-id 不注入')
140
+ // 空串(noop 门面路径)零注入
141
+ const r4 = await gw.callTool('remote', 'remote_echo', { t: 'd' }, { traceparent: '' })
142
+ assert.equal(r4.ok, true)
143
+ assert.equal(seen.at(-1)!.headers['traceparent'], undefined, '空串零注入(otel 关路径)')
144
+ transport.close()
145
+ } finally {
146
+ await close()
147
+ }
148
+ })
149
+
150
+ // ==================== 单元:配置头同名——注入优先 ====================
151
+
152
+ test('S43: 配置 headers 字面 traceparent——无注入请求沿用配置头;有注入请求注入优先', async () => {
153
+ const { server, url, seen, close } = await fakeMcpHttp()
154
+ try {
155
+ const stale = TP('c'.repeat(32), 'd'.repeat(16))
156
+ const transport = createHttpMcpTransport({ url, headers: { traceparent: stale } })
157
+ const gw = new McpGateway()
158
+ await gw.register({ serverId: 'remote', transport })
159
+ assert.equal(seen.at(-1)!.headers['traceparent'], stale, '无注入请求沿用配置头(配置面不受影响)')
160
+ const tp = TP(TID, SID)
161
+ await gw.callTool('remote', 'remote_echo', { t: 'hi' }, { traceparent: tp })
162
+ assert.equal(seen.at(-1)!.headers['traceparent'], tp, '注入优先(静态同名配置属误用——每请求 spanId 不同,登记口径)')
163
+ transport.close()
164
+ } finally {
165
+ await close()
166
+ }
167
+ })
168
+
169
+ // ==================== E2E:car run --mcp <url> 全链三点闭合(1.9-GO-5 生产调用点) ====================
170
+
171
+ const sse = (chunks: object[]) => chunks.map(c => `data: ${JSON.stringify(c)}\n\n`).join('') + 'data: [DONE]\n\n'
172
+
173
+ test('S43 E2E: car run 远程 MCP 全链——traceparent 头回显与 OTLP car.mcp.tool 三点闭合 + 双载体同值', async () => {
174
+ await withTempDir('e2e-http-tp', async dir => {
175
+ const { server: mcpServer, url: mcpUrl, seen, close } = await fakeMcpHttp()
176
+ // 假 LLM:req1 → 直调 MCP 工具;req2 → 收尾文本
177
+ const llmBodies: any[] = []
178
+ let n = 0
179
+ const llmServer: Server = createServer((req, res) => {
180
+ let raw = ''
181
+ req.on('data', c => { raw += c })
182
+ req.on('end', () => {
183
+ void req
184
+ llmBodies.push(JSON.parse(raw))
185
+ res.writeHead(200, { 'content-type': 'text/event-stream' })
186
+ res.end(n++ === 0
187
+ ? sse([
188
+ { choices: [{ delta: { tool_calls: [{ index: 0, id: 'c1', function: { name: 'mcp_remote_remote_echo', arguments: '{"t":"hi"}' } }] } }] },
189
+ { choices: [{ delta: {}, finish_reason: 'tool_calls' }] },
190
+ ])
191
+ : sse([
192
+ { choices: [{ delta: { content: 'remote 调用完成' } }] },
193
+ { choices: [{ delta: {}, finish_reason: 'stop' }] },
194
+ ]))
195
+ })
196
+ })
197
+ await new Promise<void>(r => llmServer.listen(0, '127.0.0.1', r))
198
+ const llmPort = (llmServer.address() as { port: number }).port
199
+ // OTLP 捕获端点
200
+ const otlpBodies: any[] = []
201
+ const otlp: Server = createServer((req, res) => {
202
+ let raw = ''
203
+ req.on('data', c => { raw += c })
204
+ req.on('end', () => { if (req.url === '/v1/traces') otlpBodies.push(JSON.parse(raw)); res.writeHead(200).end() })
205
+ })
206
+ await new Promise<void>(r => otlp.listen(0, '127.0.0.1', r))
207
+ const otlpPort = (otlp.address() as { port: number }).port
208
+
209
+ writeFileSync(join(dir, 'empty.ts'), `export const manifest = { name: 'empty', version: '1.0.0' }\nexport default function apply() {}\n`)
210
+ writeFileSync(join(dir, 'car.config.json'), JSON.stringify({
211
+ llm: { baseUrl: `http://127.0.0.1:${llmPort}/v1`, model: 'm' },
212
+ mcp: { servers: { remote: { url: mcpUrl } } },
213
+ }))
214
+ const child = spawn(process.execPath, ['--experimental-transform-types', CLI, 'run', 'empty.ts',
215
+ '--prompt', 'call the remote echo tool with t=hi', '--mcp', 'remote'], {
216
+ cwd: dir,
217
+ env: { ...process.env, OPENAI_API_KEY: TEST_TOKEN, CAR_ALLOW_ENV_CREDENTIALS: '1', CAR_OTEL_ENDPOINT: `http://127.0.0.1:${otlpPort}` },
218
+ stdio: ['ignore', 'pipe', 'pipe'],
219
+ })
220
+ let out = ''
221
+ let err = ''
222
+ child.stdout.on('data', d => { out += d })
223
+ child.stderr.on('data', d => { err += d })
224
+ const exitDone = new Promise<number>(resolve => child.on('exit', c => resolve(c ?? -1)))
225
+ const code = await Promise.race([exitDone, new Promise<number>(r => setTimeout(() => r(-999), 60_000))])
226
+ mcpServer.close()
227
+ llmServer.close()
228
+ otlp.close()
229
+
230
+ assert.equal(code, 0, `exit=${code}\nSTDOUT:\n${out}\nSTDERR:\n${err}`)
231
+ assert.match(out, /turnEnd=completed/)
232
+ // 远程 server 侧:请求序 + 头/体双载体同值(生产调用点实查)
233
+ assert.deepEqual(seen.map(r => r.method), ['initialize', 'notifications/initialized', 'tools/list', 'tools/call'])
234
+ const call = seen[3]!
235
+ const headerTp = call.headers['traceparent']
236
+ assert.ok(typeof headerTp === 'string' && parseTraceparent(headerTp), `tools/call 请求携带合法 traceparent 头(${headerTp})`)
237
+ assert.equal((call.body.params as any)._meta?.traceparent, headerTp, 'body _meta 与头双载体同值(1.8 字节面维持 + 1.9 头通道加法)')
238
+ // 工具结果含回显 traceparent(假 server 捕获头)
239
+ const toolMsgs = llmBodies.flatMap(b => b.messages).filter((m: any) => m.role === 'tool')
240
+ const echoed: string[] = []
241
+ for (const m of toolMsgs) {
242
+ for (const g of JSON.stringify(m.content).matchAll(/tp:(00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2})/g)) echoed.push(g[1]!)
243
+ }
244
+ assert.equal(echoed.length, 1, '远程 MCP 路径头通道回显')
245
+ assert.equal(echoed[0], headerTp, '回显 == 出站头(头通道端到端一致)')
246
+
247
+ // OTLP 出站对拍(三点闭合:回显 traceparent ↔ 出站 car.mcp.tool spanId ↔ 同 traceId)
248
+ await new Promise(r => setTimeout(r, 300))
249
+ const spans = otlpBodies.flatMap(b => b.scopeSpans[0].spans)
250
+ const turnSpan = spans.find((s: any) => s.name === 'car.turn')
251
+ assert.ok(turnSpan, 'turn span 出站')
252
+ const mcpSpans = spans.filter((s: any) => s.name === 'car.mcp.tool')
253
+ assert.equal(mcpSpans.length, 1, '一次远程 MCP 外呼一个 client span')
254
+ const parsed = parseTraceparent(headerTp!)!
255
+ assert.equal(parsed.traceId, turnSpan.traceId, '回显 traceId == 出站 turn traceId(端到端同 trace)')
256
+ assert.ok(mcpSpans.some((s: any) => s.spanId === parsed.parentSpanId), '回显 spanId == 出站 car.mcp.tool spanId(client span 对拍闭合)')
257
+ // 零内容口径:出站体无会话内容
258
+ const all = JSON.stringify(otlpBodies)
259
+ assert.equal(/call the remote echo tool|远程回声|remote-ok/.test(all), false, 'span 出站体零内容')
260
+ await close()
261
+ })
262
+ })