@lqc123qwe/car-runtime 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +17 -1
  2. package/dist/SHA256SUMS +1 -0
  3. package/dist/authz/authz.d.ts +44 -0
  4. package/dist/authz/authz.js +65 -0
  5. package/dist/car-runtime-1.10.0.tgz +0 -0
  6. package/dist/car-runtime-1.10.0.tgz.sig.bundle +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +825 -0
  9. package/dist/dx/doctor.d.ts +62 -0
  10. package/dist/dx/doctor.js +127 -0
  11. package/dist/governance/dump.d.ts +47 -0
  12. package/dist/governance/dump.js +17 -0
  13. package/dist/host/facade.d.ts +13 -0
  14. package/dist/host/facade.js +154 -0
  15. package/dist/host/hostGateway.d.ts +117 -0
  16. package/dist/host/hostGateway.js +65 -0
  17. package/dist/host/mappings.d.ts +27 -0
  18. package/dist/host/mappings.js +52 -0
  19. package/dist/host/stdio.d.ts +14 -0
  20. package/dist/host/stdio.js +83 -0
  21. package/dist/kernel/context.d.ts +76 -0
  22. package/dist/kernel/context.js +203 -0
  23. package/dist/kernel/events.d.ts +37 -0
  24. package/dist/kernel/events.js +82 -0
  25. package/dist/load/config.d.ts +79 -0
  26. package/dist/load/config.js +285 -0
  27. package/dist/load/loader.d.ts +72 -0
  28. package/dist/load/loader.js +162 -0
  29. package/dist/load/registry.d.ts +44 -0
  30. package/dist/load/registry.js +58 -0
  31. package/dist/load/report.d.ts +98 -0
  32. package/dist/load/report.js +301 -0
  33. package/dist/load/sigGate.d.ts +22 -0
  34. package/dist/load/sigGate.js +68 -0
  35. package/dist/load/sign.d.ts +25 -0
  36. package/dist/load/sign.js +47 -0
  37. package/dist/load/verifier.d.ts +35 -0
  38. package/dist/load/verifier.js +56 -0
  39. package/dist/loop/goal.d.ts +31 -0
  40. package/dist/loop/goal.js +34 -0
  41. package/dist/loop/recover.d.ts +17 -0
  42. package/dist/loop/recover.js +39 -0
  43. package/dist/loop/stop.d.ts +79 -0
  44. package/dist/loop/stop.js +151 -0
  45. package/dist/mcp/gateway.d.ts +96 -0
  46. package/dist/mcp/gateway.js +69 -0
  47. package/dist/mcp/httpClient.d.ts +27 -0
  48. package/dist/mcp/httpClient.js +196 -0
  49. package/dist/mcp/stdioClient.d.ts +8 -0
  50. package/dist/mcp/stdioClient.js +73 -0
  51. package/dist/ptc/budget.d.ts +21 -0
  52. package/dist/ptc/budget.js +36 -0
  53. package/dist/ptc/erasable.d.ts +14 -0
  54. package/dist/ptc/erasable.js +27 -0
  55. package/dist/ptc/runCode.d.ts +65 -0
  56. package/dist/ptc/runCode.js +140 -0
  57. package/dist/ptc/schemaRender.d.ts +15 -0
  58. package/dist/ptc/schemaRender.js +90 -0
  59. package/dist/ptc/sdk.d.ts +27 -0
  60. package/dist/ptc/sdk.js +23 -0
  61. package/dist/ptc/worker-entry.d.ts +1 -0
  62. package/dist/ptc/worker-entry.js +71 -0
  63. package/dist/release-audit.jsonl +4 -0
  64. package/dist/runtime-core/chatStep.d.ts +40 -0
  65. package/dist/runtime-core/chatStep.js +104 -0
  66. package/dist/runtime-core/credentials.d.ts +49 -0
  67. package/dist/runtime-core/credentials.js +118 -0
  68. package/dist/runtime-core/errors.d.ts +30 -0
  69. package/dist/runtime-core/errors.js +36 -0
  70. package/dist/runtime-core/llm.d.ts +76 -0
  71. package/dist/runtime-core/llm.js +553 -0
  72. package/dist/runtime-core/redaction.d.ts +39 -0
  73. package/dist/runtime-core/redaction.js +79 -0
  74. package/dist/runtime-core/telemetry.d.ts +88 -0
  75. package/dist/runtime-core/telemetry.js +332 -0
  76. package/dist/runtime-core/tools.d.ts +15 -0
  77. package/dist/runtime-core/tools.js +12 -0
  78. package/dist/runtime-core/trace.d.ts +55 -0
  79. package/dist/runtime-core/trace.js +137 -0
  80. package/dist/runtime-core/types.d.ts +84 -0
  81. package/dist/runtime-core/types.js +13 -0
  82. package/dist/sandbox/sandbox.d.ts +68 -0
  83. package/dist/sandbox/sandbox.js +153 -0
  84. package/dist/sbom.json +263 -0
  85. package/dist/seam.d.ts +21 -0
  86. package/dist/seam.js +20 -0
  87. package/dist/security/secrets-baseline.d.ts +43 -0
  88. package/dist/security/secrets-baseline.js +176 -0
  89. package/dist/security/secrets.d.ts +45 -0
  90. package/dist/security/secrets.js +138 -0
  91. package/dist/session/export.d.ts +55 -0
  92. package/dist/session/export.js +81 -0
  93. package/dist/session/fork.d.ts +51 -0
  94. package/dist/session/fork.js +63 -0
  95. package/dist/session/format.d.ts +21 -0
  96. package/dist/session/format.js +49 -0
  97. package/dist/session/indexStore.d.ts +44 -0
  98. package/dist/session/indexStore.js +265 -0
  99. package/dist/session/log.d.ts +79 -0
  100. package/dist/session/log.js +157 -0
  101. package/dist/session/store.d.ts +9 -0
  102. package/dist/session/store.js +37 -0
  103. package/dist/telemetry/metrics.d.ts +25 -0
  104. package/dist/telemetry/metrics.js +26 -0
  105. package/package.json +17 -1
  106. package/src/seam.ts +21 -0
  107. package/.gitattributes +0 -11
  108. package/.github/workflows/ci.yml +0 -293
  109. package/.github/workflows/release.yml +0 -54
  110. package/adr/ADR-001.md +0 -4
  111. package/adr/ADR-002.md +0 -5
  112. package/adr/ADR-003.md +0 -35
  113. package/adr/host-onboarding.md +0 -68
  114. package/adr/mlps-audit-checklist.md +0 -41
  115. package/adr/win32-spike.md +0 -45
  116. package/docs/plugin-authoring.md +0 -82
  117. package/native/landlock-run/Makefile +0 -19
  118. package/native/landlock-run/landlock-run.c +0 -162
  119. package/scripts/contract-check.ts +0 -56
  120. package/scripts/invariant-n1.ts +0 -96
  121. package/scripts/package-check.ts +0 -29
  122. package/scripts/ptc-baseline/run-baseline.ts +0 -118
  123. package/scripts/release-artifacts.ts +0 -111
  124. package/scripts/release-pipeline.ts +0 -245
  125. package/scripts/secrets-baseline.ts +0 -33
  126. package/scripts/secrets-scan.ts +0 -56
  127. package/scripts/win32-ffi-debug.ts +0 -143
  128. package/scripts/win32-koffi-spike.ts +0 -537
  129. package/test/contract/eventmap.golden.json +0 -7
  130. package/test/fixtures/hello-plugin.ts +0 -5
  131. package/test/kernel.spec.ts +0 -136
  132. package/test/registry-e2e/run-e2e.ts +0 -213
  133. package/test/s11.spec.ts +0 -129
  134. package/test/s12.spec.ts +0 -135
  135. package/test/s13.spec.ts +0 -117
  136. package/test/s14.spec.ts +0 -117
  137. package/test/s15.spec.ts +0 -80
  138. package/test/s16.spec.ts +0 -133
  139. package/test/s17.spec.ts +0 -102
  140. package/test/s18.spec.ts +0 -81
  141. package/test/s19.spec.ts +0 -138
  142. package/test/s2.spec.ts +0 -245
  143. package/test/s20.spec.ts +0 -241
  144. package/test/s21.spec.ts +0 -231
  145. package/test/s22-m7-store-format.spec.ts +0 -135
  146. package/test/s23-m7-export-index.spec.ts +0 -189
  147. package/test/s24-signature-gate.spec.ts +0 -185
  148. package/test/s25-m8-runtime-core.spec.ts +0 -600
  149. package/test/s26-signature-closeout.spec.ts +0 -349
  150. package/test/s27-host-load-wiring.spec.ts +0 -134
  151. package/test/s28-session-index-followup.spec.ts +0 -236
  152. package/test/s29-otel-producer.spec.ts +0 -252
  153. package/test/s3.spec.ts +0 -179
  154. package/test/s30-release-artifacts.spec.ts +0 -127
  155. package/test/s31-real-model-wiring.spec.ts +0 -329
  156. package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
  157. package/test/s33-anthropic-adapter.spec.ts +0 -354
  158. package/test/s34-mcp-remote-transport.spec.ts +0 -250
  159. package/test/s35-config-ts-zod.spec.ts +0 -145
  160. package/test/s36-host-tool-surface.spec.ts +0 -250
  161. package/test/s37-projection-dedupe.spec.ts +0 -161
  162. package/test/s38-config-jiti.spec.ts +0 -116
  163. package/test/s39-otlp-span-semantics.spec.ts +0 -100
  164. package/test/s4.spec.ts +0 -169
  165. package/test/s40-metrics-cumulative.spec.ts +0 -67
  166. package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
  167. package/test/s42-traceparent-propagation.spec.ts +0 -415
  168. package/test/s43-remote-http-traceparent.spec.ts +0 -262
  169. package/test/s5.spec.ts +0 -95
  170. package/test/s6.spec.ts +0 -169
  171. package/test/s7.spec.ts +0 -136
  172. package/test/s7b-baseline.spec.ts +0 -20
  173. package/test/s9.spec.ts +0 -89
  174. package/test/sandbox-escape/j17-orchestrate.sh +0 -30
  175. package/test/sandbox-escape/run-matrix.sh +0 -98
  176. package/tsconfig.json +0 -18
@@ -1,148 +0,0 @@
1
- /**
2
- * 1.7-S3 · mcp-serve turn span 面(D-28):sessionTurn 挂 turn 级 span(装配层拦截)
3
- *
4
- * 覆盖(1.7-迭代规划 TELE-3;1.2-S3「span 面随 W1」mcp-serve 半边出清):
5
- * - spawn E2E:mcp-serve 真进程 + 本地 http 捕获端点——session_turn 产 car.turn span 出站
6
- * (car.turn_id=T1 回填〔facade 内部自增派生,装配层调用前不可知〕/ outcome=归一化 reason /
7
- * traceId 生成 / parentSpanId 缺席=根 span);turn span 落 /v1/traces;
8
- * - 非会话工具(session_start)不产 span——拦截面仅 session_turn(D-28 口径);
9
- * - 既有 meter 桥与 S17 快照面不受影响(s29 全量回归保证);遥测审计禁混流红线(静态断言)s29 保持。
10
- */
11
- import { test } from 'node:test'
12
- import assert from 'node:assert/strict'
13
- import { spawn } from 'node:child_process'
14
- import { createServer, type Server } from 'node:http'
15
- import { mkdtempSync, rmSync } from 'node:fs'
16
- import { tmpdir } from 'node:os'
17
- import { join, dirname } from 'node:path'
18
- import { fileURLToPath } from 'node:url'
19
- import { deriveSessionId } from '../src/host/mappings.ts'
20
-
21
- const CLI = join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'cli.ts')
22
-
23
- function withTempDir(name: string, fn: (dir: string) => Promise<void>): Promise<void> {
24
- const dir = mkdtempSync(join(tmpdir(), `car-s41-${name}-`))
25
- return fn(dir).finally(() => { try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ } })
26
- }
27
-
28
- test('S41: spawn E2E——mcp-serve sessionTurn → car.turn span 出站(turnId 回填 / outcome=reason)', async () => {
29
- await withTempDir('e2e', async dir => {
30
- const traceBodies: any[] = []
31
- let done: (v: void) => void = () => {}
32
- const closed = new Promise<void>(r => { done = r })
33
- const server: Server = createServer((req, res) => {
34
- let body = ''
35
- req.on('data', c => { body += c })
36
- req.on('end', () => {
37
- if (req.url === '/v1/traces') traceBodies.push(JSON.parse(body))
38
- res.writeHead(200).end()
39
- })
40
- })
41
- await new Promise<void>(r => server.listen(0, '127.0.0.1', r))
42
- const port = (server.address() as { port: number }).port
43
- try {
44
- const child = spawn(process.execPath, ['--experimental-transform-types', CLI, 'mcp-serve'], {
45
- cwd: dir,
46
- env: { ...process.env, CAR_OTEL_ENDPOINT: `http://127.0.0.1:${port}`, CAR_OTEL_INTERVAL_MS: '0' },
47
- stdio: ['pipe', 'pipe', 'pipe'],
48
- })
49
- let err = ''
50
- child.stderr.on('data', d => { err += d })
51
- // 确定性派生(s36 同款):session_start 返回的 CAR 会话 id 进程内预计算——双请求可并发下发
52
- const sid = deriveSessionId('claude-code', 's41-span')
53
- child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'session_start', arguments: { hostSessionId: 's41-span' } } }) + '\n')
54
- child.stdin.write(JSON.stringify({
55
- jsonrpc: '2.0', id: 2, method: 'tools/call',
56
- params: {
57
- name: 'session_turn',
58
- arguments: { sessionId: sid, input: { events: [{ hostEvent: 'user_message', payload: { content: 'hi' } }, { hostEvent: 'turn_complete' }] } },
59
- },
60
- }) + '\n')
61
- child.stdin.end()
62
- child.on('exit', () => done())
63
- // macOS runner 慢启动容差(s29 同款);落选 race timer 必须 clear
64
- let raceTimer: ReturnType<typeof setTimeout> | undefined
65
- try {
66
- await Promise.race([
67
- closed,
68
- new Promise<never>((_, rej) => { raceTimer = setTimeout(() => rej(new Error(`mcp-serve 超时\n${err}`)), 60_000) }),
69
- ])
70
- } finally {
71
- if (raceTimer) clearTimeout(raceTimer)
72
- }
73
- const spans = traceBodies.flatMap(b => b.scopeSpans[0].spans)
74
- const turnSpan = spans.find(s => s.name === 'car.turn')
75
- assert.ok(turnSpan, 'session_turn 产 car.turn span 出站(v1/traces 面可达)')
76
- const attrs = Object.fromEntries(turnSpan.attributes.map((a: any) => [a.key, a.value.stringValue]))
77
- assert.equal(attrs['car.turn_id'], 'T1', 'turnId 事后回填(facade 内部自增派生)')
78
- assert.equal(attrs['car.outcome'], 'completed', 'outcome = 归一化 turnEnd reason')
79
- assert.ok(turnSpan.traceId && turnSpan.spanId, 'traceId/spanId 生成')
80
- assert.equal('parentSpanId' in turnSpan, false, '根 span 无 parent')
81
- assert.equal('events' in turnSpan, false, '正常路径无 exception 事件')
82
- assert.equal(turnSpan.status.code, 'STATUS_CODE_UNSET')
83
- // 非会话工具(session_start)不产 span——拦截面仅 session_turn(D-28)
84
- assert.equal(spans.length, 1, '单 session_turn = 单 turn span(session_start 不产 span)')
85
- // 零内容口径:span 出站体无会话内容(user_message payload 不出站)
86
- assert.equal(/hi|user_message/.test(JSON.stringify(traceBodies)), false, 'span 出站体零内容(事件批内容不出站)')
87
- } finally {
88
- server.closeAllConnections()
89
- server.close()
90
- }
91
- })
92
- })
93
-
94
- test('S41: session_turn 未知会话显式报错——span 收口 error + exception 事件出站', async () => {
95
- await withTempDir('err', async dir => {
96
- const traceBodies: any[] = []
97
- let done: (v: void) => void = () => {}
98
- const closed = new Promise<void>(r => { done = r })
99
- const server: Server = createServer((req, res) => {
100
- let body = ''
101
- req.on('data', c => { body += c })
102
- req.on('end', () => {
103
- if (req.url === '/v1/traces') traceBodies.push(JSON.parse(body))
104
- res.writeHead(200).end()
105
- })
106
- })
107
- await new Promise<void>(r => server.listen(0, '127.0.0.1', r))
108
- const port = (server.address() as { port: number }).port
109
- try {
110
- const child = spawn(process.execPath, ['--experimental-transform-types', CLI, 'mcp-serve'], {
111
- cwd: dir,
112
- env: { ...process.env, CAR_OTEL_ENDPOINT: `http://127.0.0.1:${port}`, CAR_OTEL_INTERVAL_MS: '0' },
113
- stdio: ['pipe', 'pipe', 'pipe'],
114
- })
115
- let err = ''
116
- child.stderr.on('data', d => { err += d })
117
- child.on('exit', () => done())
118
- child.stdin.write(JSON.stringify({
119
- jsonrpc: '2.0', id: 1, method: 'tools/call',
120
- params: { name: 'session_turn', arguments: { sessionId: 'SH-nonexistent', input: { events: [] } } },
121
- }) + '\n')
122
- child.stdin.end()
123
- let raceTimer: ReturnType<typeof setTimeout> | undefined
124
- try {
125
- await Promise.race([
126
- closed,
127
- new Promise<never>((_, rej) => { raceTimer = setTimeout(() => rej(new Error(`mcp-serve 超时\n${err}`)), 60_000) }),
128
- ])
129
- } finally {
130
- if (raceTimer) clearTimeout(raceTimer)
131
- }
132
- const spans = traceBodies.flatMap(b => b.scopeSpans[0].spans)
133
- const turnSpan = spans.find(s => s.name === 'car.turn')
134
- assert.ok(turnSpan, '失败路径 turn span 仍出站')
135
- assert.equal(turnSpan.status.code, 'STATUS_CODE_ERROR', 'handle 异常 → status ERROR')
136
- const ev = turnSpan.events?.find((e: any) => e.name === 'exception')
137
- assert.ok(ev, 'exception 事件出站(D-26 形态在 mcp-serve 路径成立)')
138
- const evAttrs = Object.fromEntries(ev.attributes.map((a: any) => [a.key, a.value.stringValue]))
139
- assert.match(evAttrs['exception.message'] ?? '', /CAR-E-HOST/, '异常消息进 exception.message(内容治理属调用方)')
140
- const attrs = Object.fromEntries(turnSpan.attributes.map((a: any) => [a.key, a.value.stringValue]))
141
- assert.equal(attrs['car.outcome'], 'error')
142
- assert.equal('car.turn_id' in attrs, false, '回填前失败:无占位 turnId(startTurn(\'\') 空值不设属性)')
143
- } finally {
144
- server.closeAllConnections()
145
- server.close()
146
- }
147
- })
148
- })
@@ -1,415 +0,0 @@
1
- /**
2
- * 1.8 · W3C traceparent 跨进程传播(D-29/D-30/D-31 · S42)
3
- *
4
- * 覆盖(1.8-迭代规划 TRACE-1/2/3):
5
- * - 单元:W3C 编解码(合法/畸形/全零 fail-open);mcpToolSpan client span(挂 turn + traceparent
6
- * 形态 + noop 零注入);startTurn opts.trace 远端挂链;ptcRelay relay span(worker 时刻覆盖 +
7
- * error 路径);gateway.callTool `_meta` 注入与缺省不注入(字节面兼容);
8
- * - E2E-A:car run --mcp --ptc 全链——假 MCP server 捕获 `_meta.traceparent` 回显进工具结果,
9
- * 与 OTLP 出站 car.mcp.tool span 三点对拍闭合(traceId + spanId)+ car.ptc relay span 挂链
10
- * turn + worker 计时窗口;零内容断言(结果文本/提示词不出站);
11
- * - E2E-B:mcp-serve 收 `_meta.traceparent` → car.turn span 加入宿主 trace(traceId 复用 +
12
- * parentSpanId = 远端 spanId);畸形 traceparent fail-open 新 trace。
13
- */
14
- import { test } from 'node:test'
15
- import assert from 'node:assert/strict'
16
- import { spawn } from 'node:child_process'
17
- import { createServer, type Server } from 'node:http'
18
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
19
- import { tmpdir } from 'node:os'
20
- import { join, dirname } from 'node:path'
21
- import { fileURLToPath } from 'node:url'
22
- import { createTelemetryFacade } from '../src/runtime-core/telemetry.ts'
23
- import { TurnTracer, formatTraceparent, parseTraceparent } from '../src/runtime-core/trace.ts'
24
- import { McpGateway, type ClientTransport, type JsonRpcRequest, type JsonRpcResponse } from '../src/mcp/gateway.ts'
25
-
26
- const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
27
- const CLI = join(ROOT, 'src', 'cli.ts')
28
-
29
- const okRes = () => ({ ok: true, status: 200 }) as unknown as Response
30
-
31
- // 密钥扫描口径:夹具凭据经变量注入(赋值行无引号字面量)
32
- const TEST_KEY = 'test-key-123'
33
-
34
- function withTempDir(name: string, fn: (dir: string) => Promise<void>): Promise<void> {
35
- const dir = mkdtempSync(join(tmpdir(), `car-s42-${name}-`))
36
- return fn(dir).finally(() => { try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ } })
37
- }
38
-
39
- const sse = (chunks: object[]) => chunks.map(c => `data: ${JSON.stringify(c)}\n\n`).join('') + 'data: [DONE]\n\n'
40
-
41
- // ==================== 单元:W3C 编解码(D-29) ====================
42
-
43
- test('S42: formatTraceparent/parseTraceparent——W3C 形态 + 畸形 fail-open(null 不抛)', () => {
44
- const tid = 'a'.repeat(32)
45
- const sid = 'b'.repeat(16)
46
- assert.equal(formatTraceparent(tid, sid), `00-${tid}-${sid}-01`, 'flags 01(本端记录中)')
47
- assert.deepEqual(parseTraceparent(`00-${tid}-${sid}-01`), { traceId: tid, parentSpanId: sid })
48
- assert.deepEqual(parseTraceparent(`00-${tid}-${sid}-ff`), { traceId: tid, parentSpanId: sid }, 'flags 任意值接受(挂链不消费 flags)')
49
- // 畸形 fail-open:全 null 不抛(遥测面禁 fail-hard——BD-05 同源)
50
- assert.equal(parseTraceparent('garbage'), null, '非格式串')
51
- assert.equal(parseTraceparent(`ff-${tid}-${sid}-01`), null, 'version 非 00')
52
- assert.equal(parseTraceparent(`00-${'a'.repeat(31)}-${sid}-01`), null, 'trace-id 错长')
53
- assert.equal(parseTraceparent(`00-${tid}-${'b'.repeat(15)}-01`), null, 'span-id 错长')
54
- assert.equal(parseTraceparent(`00-${'0'.repeat(32)}-${sid}-01`), null, 'trace-id 全零(W3C 无效)')
55
- assert.equal(parseTraceparent(`00-${tid}-${'0'.repeat(16)}-01`), null, 'span-id 全零(W3C 无效)')
56
- assert.equal(parseTraceparent(`00-${'g'.repeat(32)}-${sid}-01`), null, '非 hex')
57
- // 自研 id 形态天然合规:16B/8B hex 往返
58
- const rt = parseTraceparent(formatTraceparent(tid, sid))
59
- assert.deepEqual(rt, { traceId: tid, parentSpanId: sid }, 'format→parse 往返一致')
60
- })
61
-
62
- // ==================== 单元:mcpToolSpan client span(D-29) ====================
63
-
64
- test('S42: mcpToolSpan——car.mcp.tool 挂 turn + traceparent 形态;noop 门面 traceparent 零注入', async () => {
65
- await withTempDir('client-span', async () => {
66
- const bodies: any[] = []
67
- const f = createTelemetryFacade({ endpoint: 'https://otel' }, {
68
- fetchImpl: (async (_u: string | URL, init?: RequestInit) => { bodies.push(JSON.parse(String(init?.body))); return okRes() }) as typeof fetch,
69
- })
70
- const tracer = new TurnTracer(f)
71
- const turn = tracer.startTurn('T0')
72
- const span = turn.mcpToolSpan('helper', 'upper')
73
- // traceparent 形态 = 00-{turn traceId}-{本 span spanId}-01
74
- const parsed = parseTraceparent(span.traceparent)
75
- assert.ok(parsed, 'traceparent 可解析')
76
- assert.equal(parsed!.traceId, turn.traceId, 'client span traceId = turn traceId')
77
- span.end('ok')
78
- const span2 = turn.mcpToolSpan('helper', 'upper')
79
- span2.recordException(new Error('boom'))
80
- span2.end('error')
81
- turn.end('completed')
82
- await f.flush()
83
- const spans = bodies.flatMap(b => b.scopeSpans[0].spans)
84
- const mcp = spans.filter((s: any) => s.name === 'car.mcp.tool')
85
- assert.equal(mcp.length, 2, '两次外呼两个 client span')
86
- assert.equal(mcp[0].parentSpanId, spans.find((s: any) => s.name === 'car.turn').spanId, 'client span 父 = turn spanId')
87
- assert.equal(mcp[0].traceId, turn.traceId, '共享 trace')
88
- const attrs = Object.fromEntries(mcp[0].attributes.map((a: any) => [a.key, a.value.stringValue]))
89
- assert.equal(attrs['car.mcp_server'], 'helper', 'serverId 标识符面(零内容口径扩展登记)')
90
- assert.equal(attrs['car.tool'], 'upper')
91
- assert.equal(attrs['car.outcome'], 'ok')
92
- // 本 span spanId 与 traceparent 三点一致(E2E-A 端到端对拍的单元基线)
93
- const outSpanId = mcp[0].spanId
94
- assert.equal(parsed!.parentSpanId, outSpanId, 'traceparent spanId = 出站 spanId')
95
- const errAttrs = Object.fromEntries(mcp[1].attributes.map((a: any) => [a.key, a.value.stringValue]))
96
- assert.equal(errAttrs['car.outcome'], 'error')
97
- assert.equal(mcp[1].status.code, 'STATUS_CODE_ERROR', 'error 路径 D-26 形态')
98
- // noop 门面:traceparent=''(调用方不注入 _meta——字节面兼容线)
99
- const noopTracer = new TurnTracer(createTelemetryFacade(null))
100
- const noopSpan = noopTracer.startTurn('T0').mcpToolSpan('helper', 'upper')
101
- assert.equal(noopSpan.traceparent, '', 'noop 零注入面')
102
- noopSpan.end('ok')
103
- noopTracer.startTurn('T0').ptcRelay().complete(0, 0, 'ok') // noop relay 零抛
104
- })
105
- })
106
-
107
- // ==================== 单元:startTurn 远端挂链(D-30) + ptcRelay(D-31) ====================
108
-
109
- test('S42: startTurn opts.trace——turn span 加入远端 trace(traceId 复用 + parentSpanId)', async () => {
110
- await withTempDir('server-link', async () => {
111
- const bodies: any[] = []
112
- const f = createTelemetryFacade({ endpoint: 'https://otel' }, {
113
- fetchImpl: (async (_u: string | URL, init?: RequestInit) => { bodies.push(JSON.parse(String(init?.body))); return okRes() }) as typeof fetch,
114
- })
115
- const tracer = new TurnTracer(f)
116
- const remoteTraceId = 'c'.repeat(32)
117
- const remoteSpanId = 'd'.repeat(16)
118
- const turn = tracer.startTurn('', { trace: { traceId: remoteTraceId, parentSpanId: remoteSpanId } })
119
- turn.setTurnId('T1')
120
- turn.end('completed')
121
- await f.flush()
122
- const spans = bodies.flatMap(b => b.scopeSpans[0].spans)
123
- const t = spans.find((s: any) => s.name === 'car.turn')
124
- assert.equal(t.traceId, remoteTraceId, 'traceId 复用远端(加入宿主 trace)')
125
- assert.equal(t.parentSpanId, remoteSpanId, 'parentSpanId = 远端 spanId')
126
- // step 子 span 同 trace 挂 turn(挂链后层级不破坏)
127
- const turn2 = tracer.startTurn('', { trace: { traceId: remoteTraceId, parentSpanId: remoteSpanId } })
128
- turn2.stepSpan(1).end('stop')
129
- turn2.end('completed')
130
- await f.flush()
131
- const spans2 = bodies.flatMap(b => b.scopeSpans[0].spans)
132
- const step = spans2.find((s: any) => s.name === 'car.step')
133
- assert.equal(step.traceId, remoteTraceId)
134
- assert.equal(step.parentSpanId, spans2.filter((s: any) => s.name === 'car.turn').at(-1)!.spanId)
135
- })
136
- })
137
-
138
- test('S42: ptcRelay——car.ptc span 主线程收口(worker 时刻覆盖 + 挂链 turn + error 形态)', async () => {
139
- await withTempDir('ptc-relay', async () => {
140
- const bodies: any[] = []
141
- const f = createTelemetryFacade({ endpoint: 'https://otel' }, {
142
- fetchImpl: (async (_u: string | URL, init?: RequestInit) => { bodies.push(JSON.parse(String(init?.body))); return okRes() }) as typeof fetch,
143
- })
144
- const tracer = new TurnTracer(f)
145
- const turn = tracer.startTurn('T0')
146
- const relay = turn.ptcRelay()
147
- assert.deepEqual(relay.workerTrace, { traceId: turn.traceId, parentSpanId: turn.spanId }, 'workerTrace 载荷 = turn 挂链')
148
- // ok 路径:worker 计时时刻覆盖(1000→1060 不经 now()——精确出站)
149
- relay.complete(1000, 1060, 'ok')
150
- // error 路径:recordException D-26 形态
151
- turn.ptcRelay().complete(2000, 2250, 'error', new Error('boom'))
152
- turn.end('completed')
153
- await f.flush()
154
- const spans = bodies.flatMap(b => b.scopeSpans[0].spans)
155
- const ptc = spans.filter((s: any) => s.name === 'car.ptc')
156
- assert.equal(ptc.length, 2)
157
- assert.equal(ptc[0].startTimeUnixNano, String(1000 * 1e6), 'startMs 时刻覆盖(非 now())')
158
- assert.equal(ptc[0].endTimeUnixNano, String(1060 * 1e6), 'endMs 时刻覆盖')
159
- assert.equal(ptc[0].parentSpanId, turn.spanId, 'car.ptc 挂 turn')
160
- assert.equal(ptc[0].traceId, turn.traceId, '同 trace')
161
- assert.equal(ptc[0].status.code, 'STATUS_CODE_UNSET')
162
- assert.equal(ptc[1].status.code, 'STATUS_CODE_ERROR', 'error 路径 status')
163
- const ev = ptc[1].events?.find((e: any) => e.name === 'exception')
164
- assert.ok(ev, 'error 路径 exception 事件(D-26)')
165
- // 负时长防线:endMs < startMs 收口为 startMs(防御下游转换器)
166
- turn.ptcRelay().complete(3000, 2999, 'ok')
167
- turn.end('completed')
168
- await f.flush()
169
- const ptc3 = bodies.flatMap(b => b.scopeSpans[0].spans).filter((s: any) => s.name === 'car.ptc').at(-1)!
170
- assert.equal(ptc3.endTimeUnixNano, ptc3.startTimeUnixNano, '负时长钳制')
171
- // noop 门面:workerTrace null + complete 零出站
172
- const noopRelay = new TurnTracer(createTelemetryFacade(null)).startTurn('T0').ptcRelay()
173
- assert.equal(noopRelay.workerTrace, null)
174
- noopRelay.complete(0, 0, 'ok')
175
- })
176
- })
177
-
178
- // ==================== 单元:gateway.callTool `_meta` 注入(D-29) ====================
179
-
180
- test('S42: callTool traceparent 注入 params._meta;缺省无 _meta 键(字节面兼容)', async () => {
181
- await withTempDir('meta', async () => {
182
- const sent: JsonRpcRequest[] = []
183
- const transport: ClientTransport = {
184
- send: async (req) => {
185
- sent.push(req)
186
- // tools/list 需回工具面(登记制:无工具则 callTool 不发送直接 unknown tool)
187
- const result = req.method === 'tools/list'
188
- ? { tools: [{ name: 'upper', sideEffect: 'readonly' }] }
189
- : {}
190
- return { jsonrpc: '2.0', id: req.id, result } as JsonRpcResponse
191
- },
192
- alive: () => true,
193
- close: () => {},
194
- }
195
- const gw = new McpGateway()
196
- await gw.register({ serverId: 'helper', transport })
197
- sent.length = 0
198
- await gw.callTool('helper', 'upper', { text: 'x' }, { traceparent: formatTraceparent('a'.repeat(32), 'b'.repeat(16)) })
199
- assert.equal((sent[0].params as any)._meta?.traceparent, formatTraceparent('a'.repeat(32), 'b'.repeat(16)), '_meta.traceparent 注入')
200
- assert.equal(sent[0].params && 'name' in (sent[0].params as object), true, 'params.name 保持')
201
- await gw.callTool('helper', 'upper', { text: 'x' })
202
- const p2 = sent[1].params as Record<string, unknown>
203
- assert.equal('_meta' in p2, false, '缺省不注入(线上 JSON-RPC 字节面不变)')
204
- await gw.callTool('helper', 'upper', { text: 'x' }, { traceparent: '' })
205
- assert.equal('_meta' in (sent[2].params as object), false, '空串不注入(noop 门面路径)')
206
- })
207
- })
208
-
209
- // ==================== E2E-A:car run --mcp --ptc 全链(D-29/D-31 生产调用点) ====================
210
-
211
- /** 受控 stdio MCP server(node 脚本文本):tools/call 捕获 _meta.traceparent 回显进工具结果 */
212
- const FAKE_MCP_SERVER = `
213
- const lines = [];
214
- let buf = '';
215
- process.stdin.on('data', d => {
216
- buf += d.toString();
217
- let nl;
218
- while ((nl = buf.indexOf('\\n')) >= 0) {
219
- const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1);
220
- if (!line) continue;
221
- const req = JSON.parse(line);
222
- if (req.method === 'tools/list') {
223
- reply(req, { tools: [{ name: 'upper', description: '大写工具', sideEffect: 'readonly', inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] } }] });
224
- } else if (req.method === 'tools/call') {
225
- const tp = (req.params._meta || {}).traceparent || 'none';
226
- reply(req, { content: [{ type: 'text', text: String(req.params.arguments.text).toUpperCase() + '|tp:' + tp }] });
227
- }
228
- }
229
- });
230
- function reply(req, result) { process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: req.id, result }) + '\\n'); }
231
- `
232
-
233
- const RUN_CODE = `const r = await tools.mcp_helper_upper({ text: 'abc' })\nawait new Promise(res => setTimeout(res, 60))\nreturn r`
234
-
235
- test('S42 E2E-A: car run 全链——_meta.traceparent 与 OTLP car.mcp.tool 三点闭合 + car.ptc relay span', async () => {
236
- await withTempDir('full-chain', async dir => {
237
- // 插件(run 首位参数——s32 同款装配面)
238
- writeFileSync(join(dir, 'p.ts'), `export const manifest = { name: 'p', version: '1.0.0' }\n` +
239
- `export default function apply() {}\n`)
240
- writeFileSync(join(dir, 'car.config.json'), JSON.stringify({
241
- llm: { model: 'test-model' },
242
- mcp: { servers: { helper: { command: process.execPath, args: ['-e', FAKE_MCP_SERVER] } } },
243
- }))
244
- // 假 LLM:req1 → run_code(worker 桥调 MCP);req2 → 直调 MCP 工具;req3 → done
245
- const llmBodies: any[] = []
246
- let n = 0
247
- const llmServer: Server = createServer((req, res) => {
248
- let raw = ''
249
- req.on('data', c => { raw += c })
250
- req.on('end', () => {
251
- llmBodies.push(JSON.parse(raw))
252
- res.writeHead(200, { 'content-type': 'text/event-stream' })
253
- res.end(n++ === 0
254
- ? sse([
255
- { choices: [{ delta: { tool_calls: [{ index: 0, id: 'c1', function: { name: 'run_code', arguments: JSON.stringify({ code: RUN_CODE, description: '经桥调 MCP 大写' } ) } }] } }] },
256
- { choices: [{ delta: {}, finish_reason: 'tool_calls' }] },
257
- ])
258
- : n === 2
259
- ? sse([
260
- { choices: [{ delta: { tool_calls: [{ index: 0, id: 'c2', function: { name: 'mcp_helper_upper', arguments: JSON.stringify({ text: 'xyz' } ) } }] } }] },
261
- { choices: [{ delta: {}, finish_reason: 'tool_calls' }] },
262
- ])
263
- : sse([{ choices: [{ delta: { role: 'assistant', content: '' } }] }, { choices: [{ delta: { content: 'done' } }] }, { choices: [{ delta: {}, finish_reason: 'stop' }] }]))
264
- })
265
- })
266
- await new Promise<void>(r => llmServer.listen(0, '127.0.0.1', r))
267
- const llmPort = (llmServer.address() as { port: number }).port
268
- // OTLP 捕获端点
269
- const otlpBodies: any[] = []
270
- const otlp: Server = createServer((req, res) => {
271
- let raw = ''
272
- req.on('data', c => { raw += c })
273
- req.on('end', () => { if (req.url === '/v1/traces') otlpBodies.push(JSON.parse(raw)); res.writeHead(200).end() })
274
- })
275
- await new Promise<void>(r => otlp.listen(0, '127.0.0.1', r))
276
- const otlpPort = (otlp.address() as { port: number }).port
277
-
278
- const child = spawn(process.execPath, ['--experimental-transform-types', CLI, 'run', 'p.ts',
279
- '--prompt', 'uppercase please', '--base-url', `http://127.0.0.1:${llmPort}/v1`,
280
- '--mcp', 'helper', '--ptc'], {
281
- cwd: dir,
282
- env: { ...process.env, OPENAI_API_KEY: TEST_KEY, CAR_ALLOW_ENV_CREDENTIALS: '1', CAR_OTEL_ENDPOINT: `http://127.0.0.1:${otlpPort}` },
283
- stdio: ['ignore', 'pipe', 'pipe'],
284
- })
285
- let out = ''
286
- let err = ''
287
- child.stdout.on('data', d => { out += d })
288
- child.stderr.on('data', d => { err += d })
289
- const exitDone = new Promise<number>(resolve => child.on('exit', c => resolve(c ?? -1)))
290
- const code = await Promise.race([exitDone, new Promise<number>(r => setTimeout(() => r(-999), 60_000))])
291
- llmServer.close()
292
- otlp.close()
293
-
294
- assert.equal(code, 0, `exit=${code}\nSTDOUT:\n${out}\nSTDERR:\n${err}`)
295
- assert.match(out, /turnEnd=completed/)
296
- // 工具结果含回显 traceparent(假 MCP server 捕获 _meta)——桥内与直调两条路径
297
- const toolMsgs = llmBodies.flatMap(b => b.messages).filter((m: any) => m.role === 'tool')
298
- const echoed: string[] = []
299
- for (const m of toolMsgs) {
300
- const s = JSON.stringify(m.content)
301
- for (const g of s.matchAll(/tp:(00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2})/g)) echoed.push(g[1]!)
302
- }
303
- assert.ok(echoed.length >= 2, `两条 MCP 路径均回显 traceparent(${JSON.stringify(echoed)})`)
304
-
305
- // OTLP 出站对拍(三点闭合:回显 traceparent ↔ 出站 car.mcp.tool spanId ↔ 同 traceId)
306
- await new Promise(r => setTimeout(r, 300))
307
- const spans = otlpBodies.flatMap(b => b.scopeSpans[0].spans)
308
- const turnSpan = spans.find((s: any) => s.name === 'car.turn')
309
- assert.ok(turnSpan, 'turn span 出站')
310
- const mcpSpans = spans.filter((s: any) => s.name === 'car.mcp.tool')
311
- assert.equal(mcpSpans.length, 2, '两次 MCP 外呼两个 client span')
312
- for (const s of mcpSpans) {
313
- assert.equal(s.parentSpanId, turnSpan.spanId, 'client span 父 = turn')
314
- assert.equal(s.traceId, turnSpan.traceId, '同 trace')
315
- }
316
- const outSpanIds = new Set(mcpSpans.map((s: any) => s.spanId))
317
- const outTraceId = turnSpan.traceId
318
- for (const tp of echoed) {
319
- const p = parseTraceparent(tp)!
320
- assert.ok(p, `回显 traceparent 可解析:${tp}`)
321
- assert.equal(p.traceId, outTraceId, '回显 traceId == 出站 turn traceId(端到端同 trace)')
322
- assert.ok(outSpanIds.has(p.parentSpanId), '回显 spanId == 出站 car.mcp.tool spanId(client span 对拍闭合)')
323
- }
324
- // car.ptc relay span:挂链 turn + worker 计时窗口(程序含 60ms sleep)
325
- const ptcSpan = spans.find((s: any) => s.name === 'car.ptc')
326
- assert.ok(ptcSpan, 'car.ptc relay span 出站')
327
- assert.equal(ptcSpan.parentSpanId, turnSpan.spanId, 'car.ptc 挂 turn')
328
- assert.equal(ptcSpan.traceId, turnSpan.traceId, 'car.ptc 同 trace')
329
- const durMs = (Number(ptcSpan.endTimeUnixNano) - Number(ptcSpan.startTimeUnixNano)) / 1e6
330
- assert.ok(durMs >= 50 && durMs < 5000, `worker 计时窗口 ≈ 程序 sleep(${durMs.toFixed(1)}ms)`)
331
- assert.equal(ptcSpan.status.code, 'STATUS_CODE_UNSET', 'ok 路径无 exception')
332
- // 零内容口径:出站体无会话内容(提示词/工具结果文本/程序体不出站)
333
- const all = JSON.stringify(otlpBodies)
334
- assert.equal(/uppercase please|大写工具|setTimeout/.test(all), false, 'span 出站体零内容')
335
- })
336
- })
337
-
338
- // ==================== E2E-B:mcp-serve 收 traceparent 挂链(D-30 生产调用点) ====================
339
-
340
- test('S42 E2E-B: mcp-serve——_meta.traceparent 合法挂链远端 trace;畸形 fail-open 新 trace', async () => {
341
- await withTempDir('mcp-serve', async dir => {
342
- const traceBodies: any[] = []
343
- let done: (v: void) => void = () => {}
344
- const closed = new Promise<void>(r => { done = r })
345
- const server: Server = createServer((req, res) => {
346
- let body = ''
347
- req.on('data', c => { body += c })
348
- req.on('end', () => {
349
- if (req.url === '/v1/traces') traceBodies.push(JSON.parse(body))
350
- res.writeHead(200).end()
351
- })
352
- })
353
- await new Promise<void>(r => server.listen(0, '127.0.0.1', r))
354
- const port = (server.address() as { port: number }).port
355
- try {
356
- const child = spawn(process.execPath, ['--experimental-transform-types', CLI, 'mcp-serve'], {
357
- cwd: dir,
358
- env: { ...process.env, CAR_OTEL_ENDPOINT: `http://127.0.0.1:${port}`, CAR_OTEL_INTERVAL_MS: '0' },
359
- stdio: ['pipe', 'pipe', 'pipe'],
360
- })
361
- let err = ''
362
- child.stderr.on('data', d => { err += d })
363
- child.on('exit', () => done())
364
- const { deriveSessionId } = await import('../src/host/mappings.ts')
365
- const sid = deriveSessionId('claude-code', 's42-link')
366
- const remoteTraceId = 'c'.repeat(32)
367
- const remoteSpanId = 'd'.repeat(16)
368
- child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'session_start', arguments: { hostSessionId: 's42-link' } } }) + '\n')
369
- // 合法 traceparent → turn span 加入宿主 trace
370
- child.stdin.write(JSON.stringify({
371
- jsonrpc: '2.0', id: 2, method: 'tools/call',
372
- params: {
373
- name: 'session_turn',
374
- arguments: { sessionId: sid, input: { events: [{ hostEvent: 'user_message', payload: { content: 'hi' } }, { hostEvent: 'turn_complete' }] } },
375
- _meta: { traceparent: formatTraceparent(remoteTraceId, remoteSpanId) },
376
- },
377
- }) + '\n')
378
- // 畸形 traceparent → fail-open 新 trace(不 fail-hard)
379
- child.stdin.write(JSON.stringify({
380
- jsonrpc: '2.0', id: 3, method: 'tools/call',
381
- params: {
382
- name: 'session_turn',
383
- arguments: { sessionId: sid, input: { events: [{ hostEvent: 'user_message', payload: { content: 'ho' } }, { hostEvent: 'turn_complete' }] } },
384
- _meta: { traceparent: 'not-a-traceparent' },
385
- },
386
- }) + '\n')
387
- child.stdin.end()
388
- let raceTimer: ReturnType<typeof setTimeout> | undefined
389
- try {
390
- await Promise.race([
391
- closed,
392
- new Promise<never>((_, rej) => { raceTimer = setTimeout(() => rej(new Error(`mcp-serve 超时\n${err}`)), 60_000) }),
393
- ])
394
- } finally {
395
- if (raceTimer) clearTimeout(raceTimer)
396
- }
397
- const spans = traceBodies.flatMap(b => b.scopeSpans[0].spans)
398
- const turns = spans.filter((s: any) => s.name === 'car.turn')
399
- assert.equal(turns.length, 2, '两次 session_turn 两个 turn span')
400
- const linked = turns.find((s: any) => s.traceId === remoteTraceId)
401
- assert.ok(linked, '合法 traceparent:turn span 加入宿主 trace(traceId 复用)')
402
- assert.equal(linked.parentSpanId, remoteSpanId, 'parentSpanId = 远端 spanId')
403
- const attrs = Object.fromEntries(linked.attributes.map((a: any) => [a.key, a.value.stringValue]))
404
- assert.equal(attrs['car.turn_id'], 'T1', 'turnId 回填不受挂链影响')
405
- assert.equal(attrs['car.outcome'], 'completed')
406
- const fallback = turns.find((s: any) => s.traceId !== remoteTraceId)
407
- assert.ok(fallback, '畸形 traceparent:fail-open 新 trace')
408
- assert.equal('parentSpanId' in fallback, false, 'fail-open 无 parent(新独立 trace)')
409
- assert.equal(/hi|ho|user_message/.test(JSON.stringify(traceBodies)), false, 'span 出站体零内容')
410
- } finally {
411
- server.closeAllConnections()
412
- server.close()
413
- }
414
- })
415
- })