@lqc123qwe/car-runtime 1.7.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.
@@ -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
+ })