@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,262 +0,0 @@
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
- })
package/test/s5.spec.ts DELETED
@@ -1,95 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { validatePeers, parseManifest } from '../src/load/loader.ts'
4
- import { declareEvent, EventBus } from '../src/kernel/events.ts'
5
- import { parseManifest as pm } from '../src/load/loader.ts'
6
-
7
- // ==================== F10 peer 版本约束激活(严格模式默认+显式豁免,经中间确认⑥) ====================
8
-
9
- const manifest = (peers: any[], override?: any) => ({ name: 'consumer', version: '1.0.0', peers, ...(override ? { peerPolicyOverride: override } : {}) }) as any
10
-
11
- test('F10 AC: peer 冲突 → 加载期硬报错 + 冲突链(严格模式默认)', () => {
12
- const m = manifest([{ peer: 'lib-a', range: '^1.0.0' }])
13
- const installed = new Map([['lib-a', '2.0.0']])
14
- assert.throws(() => validatePeers(m, installed), /CAR-E-PEER.*lib-a@\^1\.0\.0 required but 2\.0\.0 installed/)
15
- })
16
-
17
- test('F10 AC: 缺失必选 peer → 硬报错(installed=null)', () => {
18
- const m = manifest([{ peer: 'lib-b', range: '^1.0.0' }])
19
- assert.throws(() => validatePeers(m, new Map()), /<missing>/)
20
- })
21
-
22
- test('F10 AC: 显式豁免 → 放宽生效(exempted=true,调用方留痕)', () => {
23
- const m = manifest([{ peer: 'lib-a', range: '^1.0.0' }], { relaxed: true, reason: 'internal fork compatibility' })
24
- const installed = new Map([['lib-a', '2.0.0']])
25
- const r = validatePeers(m, installed)
26
- assert.equal(r.exempted, true)
27
- assert.equal(r.violations.length, 1)
28
- })
29
-
30
- test('F10 AC: optional peer 缺失不构成硬冲突', () => {
31
- const m = manifest([{ peer: 'lib-opt', range: '^1.0.0', optional: true }])
32
- const r = validatePeers(m, new Map())
33
- assert.equal(r.exempted, false)
34
- assert.equal(r.violations.filter(v => !v.optional).length, 0)
35
- })
36
-
37
- test('F10: satisfiesRange 语义——^0.x 锁 minor;^1.x 允许 minor/patch', () => {
38
- const m0 = manifest([{ peer: 'z', range: '^0.2.1' }])
39
- assert.throws(() => validatePeers(m0, new Map([['z', '0.3.0']]))) // ^0.2 不接受 0.3
40
- const m1 = manifest([{ peer: 'z', range: '^1.2.3' }])
41
- assert.doesNotThrow(() => validatePeers(m1, new Map([['z', '1.9.0']])))
42
- assert.throws(() => validatePeers(m1, new Map([['z', '2.0.0']])))
43
- })
44
-
45
- test('F10: 豁免缺 reason 在 parseManifest 即拒绝(审计留痕前置)', () => {
46
- assert.throws(() => pm({ name: 'p', version: '1.0.0', peerPolicyOverride: { relaxed: true } }), /reason.*required/)
47
- })
48
-
49
- // ==================== F11 优先级字段(处理器级契约) ====================
50
-
51
- test('F11 AC: 同事件多处理器按 priority 升序执行(默认 0)', async () => {
52
- declareEvent('prio/demo', 'serial')
53
- const bus = new EventBus()
54
- const order: string[] = []
55
- bus.on('prio/demo', (_p: unknown, next: () => string) => { order.push('late'); return next() }, { priority: 10 })
56
- bus.on('prio/demo', (_p: unknown, next: () => string) => { order.push('early'); return next() }, { priority: -5 })
57
- bus.on('prio/demo', (_p: unknown, next: () => string) => { order.push('mid'); return next() }, { priority: 0 })
58
- await bus.dispatch<string>('prio/demo', 'x', () => 'base')
59
- assert.deepEqual(order, ['early', 'mid', 'late'])
60
- })
61
-
62
- test('F11 AC: 同 priority 保持注册序(稳定排序,M1 行为不回退)', async () => {
63
- declareEvent('prio/stable', 'serial')
64
- const bus = new EventBus()
65
- const order: string[] = []
66
- bus.on('prio/stable', () => { order.push('first'); return 'x' })
67
- bus.on('prio/stable', () => { order.push('second'); return 'x' })
68
- bus.on('prio/stable', () => { order.push('third'); return 'x' })
69
- await bus.dispatch('prio/stable', {})
70
- assert.deepEqual(order, ['first', 'second', 'third'])
71
- })
72
-
73
- test('F11: 向后兼容——M1 字符串 label 形态照常工作', async () => {
74
- declareEvent('prio/compat', 'bail')
75
- const bus = new EventBus()
76
- bus.on('prio/compat', () => 'hit', 'my-label')
77
- assert.equal(await bus.dispatch('prio/compat', {}), 'hit')
78
- assert.deepEqual(bus.handlerChain('prio/compat'), [{ label: 'my-label', priority: 0 }])
79
- })
80
-
81
- test('F11: handlerChain 治理视图返回实际执行序(F14 配置树/产消图数据源)', async () => {
82
- declareEvent('prio/gov', 'emit')
83
- const bus = new EventBus()
84
- bus.on('prio/gov', () => {}, { label: 'b', priority: 2 })
85
- bus.on('prio/gov', () => {}, { label: 'a', priority: 1 })
86
- assert.deepEqual(bus.handlerChain('prio/gov').map(h => h.label), ['a', 'b'])
87
- })
88
-
89
- // ==================== 签名方案定稿(ADR-003)口径自检 ====================
90
-
91
- test('ADR-003: manifest 解析对签名预留字段不拒绝(S6 验签器接入点)', () => {
92
- // S6 将扩展 manifest: signature: { bundle?: string; minisig?: string }
93
- const m = pm({ name: 'signed', version: '1.0.0' })
94
- assert.equal(m.name, 'signed') // M2 S5 口径:解析层不感知签名字段(warn 语义 S6 落地)
95
- })
package/test/s6.spec.ts DELETED
@@ -1,169 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { generateKeyPairSync, sign, createHash } from 'node:crypto'
4
- import { enforceSignature, verifyMinisig, type SignatureBundle } from '../src/load/verifier.ts'
5
- import { GoalDriver } from '../src/loop/goal.ts'
6
- import { recoverInterrupted } from '../src/loop/recover.ts'
7
- import { runTurn } from '../src/loop/stop.ts'
8
- import { SessionLog } from '../src/session/log.ts'
9
-
10
- // ==================== 验签器(ADR-003 双轨 / D-2 enforce 分级) ====================
11
-
12
- function makeTrustRoot() {
13
- const { publicKey, privateKey } = generateKeyPairSync('ed25519')
14
- const pubB64 = publicKey.export({ type: 'spki', format: 'der' }).toString('base64')
15
- const signHash = (hash: string) => sign(null, Buffer.from(hash), privateKey).toString('base64')
16
- return { pubB64, signHash }
17
- }
18
-
19
- const HASH = createHash('sha256').update('manifest-body').digest('hex')
20
-
21
- test('S6: minisig 验证——正确签名放行,篡改硬拒绝(不可配置放行)', () => {
22
- const { pubB64, signHash } = makeTrustRoot()
23
- const sig: SignatureBundle = { minisig: signHash(HASH) }
24
- const good = enforceSignature(HASH, sig, { trustRootPublicKey: pubB64, mode: 'warn' })
25
- assert.equal(good.allowed, true)
26
- assert.equal(good.warning, undefined)
27
- // 篡改哈希后验签失败——即使 warn 模式也硬拒绝
28
- const bad = enforceSignature(HASH + 'x', sig, { trustRootPublicKey: pubB64, mode: 'warn' })
29
- assert.equal(bad.allowed, false)
30
- assert.match(bad.error!, /verification FAILED.*不可配置放行/)
31
- })
32
-
33
- test('S6 D-2: 签名缺失——warn 告警放行(指纹兜底),enforce 硬拒绝', () => {
34
- const { pubB64 } = makeTrustRoot()
35
- const warn = enforceSignature(HASH, undefined, { trustRootPublicKey: pubB64, mode: 'warn' })
36
- assert.equal(warn.allowed, true)
37
- assert.match(warn.warning!, /CAR-W-SIG.*enforce 切换见 D-2 终局条件/)
38
- const enforce = enforceSignature(HASH, undefined, { trustRootPublicKey: pubB64, mode: 'enforce' })
39
- assert.equal(enforce.allowed, false)
40
- assert.match(enforce.error!, /signature missing/)
41
- })
42
-
43
- test('S6 ADR-003: Sigstore 不可达 → 回退静态轨(双轨回退语义)', () => {
44
- const { pubB64, signHash } = makeTrustRoot()
45
- const sig: SignatureBundle = { minisig: signHash(HASH), sigstoreBundle: '<bundled>' }
46
- const r = enforceSignature(HASH, sig, {
47
- trustRootPublicKey: pubB64, mode: 'enforce',
48
- sigstoreAvailable: () => true, // 主轨探针可用但骨架期 bundle 校验不可达 → 回退静态轨
49
- })
50
- assert.equal(r.allowed, true) // 静态轨兜底成功
51
- })
52
-
53
- test('S6: verifyMinisig 对非法 base64 不崩溃(fail-closed)', () => {
54
- const { pubB64 } = makeTrustRoot()
55
- const r = verifyMinisig(HASH, 'not-base64!!!', pubB64)
56
- assert.equal(r.ok, false)
57
- assert.ok(r.error)
58
- })
59
-
60
- // ==================== F13 Goal 层 ====================
61
-
62
- test('F13: Goal phase 持久化(goalUpdate 事件)+ 恢复层重放(activation 不恢复=disarmed)', () => {
63
- const log = new SessionLog()
64
- const goal = new GoalDriver(log)
65
- goal.arm()
66
- assert.equal(goal.activation, 'armed')
67
- goal.setPhase('paused', 'waiting for external event')
68
- goal.setPhase('complete', 'done')
69
- const events = log.events as any[]
70
- assert.equal(events.filter(e => e.kind === 'goalUpdate').length, 2)
71
- const replay = GoalDriver.replay(log.events)
72
- assert.equal(replay.phase, 'complete') // phase 从日志重放
73
- assert.equal(replay.activation, 'disarmed') // activation 永不持久化(AC-4.5)
74
- })
75
-
76
- test('F13: 新实例默认 disarmed(重启不会把历史任务叫醒,AC-4.5)', () => {
77
- const goal = new GoalDriver(new SessionLog())
78
- assert.equal(goal.activation, 'disarmed')
79
- assert.equal(goal.phase, 'active')
80
- })
81
-
82
- test('F13: 非 completed 收口自动 disarm(D5 §9:撞限/错误都会 disarm)', () => {
83
- const goal = new GoalDriver(new SessionLog())
84
- goal.arm()
85
- goal.autoDisarmOnTurnEnd('max-tokens')
86
- assert.equal(goal.activation, 'disarmed')
87
- goal.arm()
88
- goal.autoDisarmOnTurnEnd('completed')
89
- assert.equal(goal.activation, 'armed') // completed 不 disarm
90
- })
91
-
92
- // ==================== F13 aggregate 可配置 + blockOnDeny ====================
93
-
94
- test('F13: aggregate.terminate=any——单个 terminate 即 aborted(对照默认 AND)', async () => {
95
- const log = new SessionLog()
96
- const tools = new Map([
97
- ['killer', { declaredSideEffect: 'readonly', terminate: true, run: async () => 'k' } as any],
98
- ['probe', { declaredSideEffect: 'readonly', run: async () => 'p' } as any],
99
- ])
100
- const r = await runTurn({
101
- log, turnId: 'T', tools,
102
- preset: { aggregate: { terminate: 'any' } },
103
- model: async (): Promise<any> => ({ stopReason: 'toolUse', toolCalls: [
104
- { id: 'c1', tool: 'probe', args: {} }, { id: 'c2', tool: 'killer', args: {} },
105
- ] }),
106
- })
107
- assert.equal(r.reason, 'aborted') // any:killer 投票即整批终止(对照 M1 默认 AND 需全部)
108
- })
109
-
110
- test('F13: aggregate.concludesTurn=all——单工具收口不再立即 completed(对照默认 OR)', async () => {
111
- const log = new SessionLog()
112
- let n = 0
113
- const tools = new Map([
114
- ['probe', { declaredSideEffect: 'readonly', run: async () => 'p' } as any],
115
- ['commit', { declaredSideEffect: 'write', concludesTurn: true, run: async () => 'c' } as any],
116
- ])
117
- const r = await runTurn({
118
- log, turnId: 'T', tools, preset: { mode: 'full', aggregate: { concludesTurn: 'all' } },
119
- model: async (): Promise<any> => n++ === 0
120
- ? { stopReason: 'toolUse', toolCalls: [
121
- { id: 'c1', tool: 'probe', args: {} }, { id: 'c2', tool: 'commit', args: {} },
122
- ] }
123
- : { stopReason: 'stop', text: 'final' },
124
- })
125
- // all 模式:批次 2 个工具仅 1 个 conclude 投票 → concludesAll 不满足 → 模型继续 → stop 收口
126
- // (对照默认 OR:同批次会立即 completed——见 s2/s4 用例)
127
- assert.equal(r.reason, 'completed')
128
- assert.equal(r.steps, 2)
129
- })
130
-
131
- test('F13: blockOnDeny——授权拒绝即收口 blocked(产出点新增,枚举不增)', async () => {
132
- const log = new SessionLog()
133
- const tools = new Map([['danger', { declaredSideEffect: 'write', run: async () => 'x' } as any]])
134
- const r = await runTurn({
135
- log, turnId: 'T', tools,
136
- preset: { mode: 'confirm', blockOnDeny: true, authorize: async () => false },
137
- model: async (): Promise<any> => ({ stopReason: 'toolUse', toolCalls: [{ id: 'c1', tool: 'danger', args: {} }] }),
138
- })
139
- assert.equal(r.reason, 'blocked')
140
- const end = log.events.find(e => e.kind === 'turnEnd') as any
141
- assert.equal(end.meta.reason, 'blocked')
142
- })
143
-
144
- // ==================== 恢复层(interrupted) ====================
145
-
146
- test('F13: recoverInterrupted——未配对 toolCall 补合成结果 + 未闭合 turn 补记 interrupted', () => {
147
- const log = new SessionLog()
148
- log.append('user', 'user', 'T1', 'go')
149
- log.append('model', 'toolCall', 'T1', { id: 'c1', tool: 't', args: {} })
150
- // 模拟进程消失:c1 无 toolResult、T1 无 turnEnd
151
- const report = recoverInterrupted(log)
152
- assert.equal(report.pairedCalls, 1)
153
- assert.equal(report.turnsClosed, 1)
154
- const synthetic = log.events.find(e => e.kind === 'toolResult' && (e.payload as any).id === 'c1') as any
155
- assert.equal(synthetic.payload.recovered, true) // 副作用状态未知显式声明
156
- const end = log.events.find(e => e.kind === 'turnEnd' && e.turnId === 'T1') as any
157
- assert.equal(end.meta.reason, 'interrupted') // 与 aborted 不混用
158
- })
159
-
160
- test('F13: 已配对/已闭合日志幂等(重复恢复零补记)', () => {
161
- const log = new SessionLog()
162
- log.append('user', 'user', 'T1', 'go')
163
- log.append('model', 'toolCall', 'T1', { id: 'c1', tool: 't', args: {} })
164
- log.append('plugin', 'toolResult', 'T1', { id: 'c1', result: 'ok' })
165
- log.append('runtime', 'turnEnd', 'T1', null, { reason: 'completed' })
166
- const report = recoverInterrupted(log)
167
- assert.equal(report.pairedCalls, 0)
168
- assert.equal(report.turnsClosed, 0)
169
- })
package/test/s7.spec.ts DELETED
@@ -1,136 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { scanSecrets, redactSecrets, shannonEntropy, PATTERN_LIBRARY } from '../src/security/secrets.ts'
4
- import { buildSeatbeltProfile } from '../src/sandbox/sandbox.ts'
5
- import { execFileSync } from 'node:child_process'
6
-
7
- // ==================== F12 secrets 检测(T-6 + D-3) ====================
8
-
9
- test('F12: API Key 前缀族命中(OpenAI/GitHub/AWS)', () => {
10
- const t = 'const key = "sk-proj-abcdefghij0123456789abcd" // OPENAI_API_KEY'
11
- const { hits } = scanSecrets(t)
12
- assert.ok(hits.some(h => h.name === 'OpenAI'), 'OpenAI sk- 命中')
13
- const aws = scanSecrets('aws_secret_access_key = "AKIAIOSFODNN7EXAMPLE"').hits
14
- assert.ok(aws.some(h => h.name === 'AWS Access Key'), 'AWS AccessKey 命中')
15
- })
16
-
17
- test('F12: L2 熵过滤——低熵假 token 不命中(误报控制)', () => {
18
- // sk- 前缀但内容是重复字符(低熵 <3.5)→ 不应命中
19
- const t = 'sk-aaaaaaaaaaaaaaaaaaaaaaaa // 测试占位符,非真实 key'
20
- const { hits, candidates } = scanSecrets(t, { returnCandidates: true })
21
- assert.equal(hits.length, 0, '低熵候选不进命中')
22
- assert.ok(candidates.length >= 0) // 观察面存在
23
- })
24
-
25
- test('F12: L3 上下文打分——裸 sk- 串(无键名/赋值语境)分数不足则降级候选', () => {
26
- const t = 'random text sk-Xy3kP9mQ2vB7nR4tW8zC for demo' // 有熵但无键名共现、非赋值语境
27
- const { hits, candidates } = scanSecrets(t, { returnCandidates: true })
28
- assert.ok(!hits.some(h => h.name === 'OpenAI') || hits.length >= 0) // 依打分而定,但机制须工作
29
- assert.ok(hits.length + candidates.length >= 1, 'L1 前缀层必须先捕获候选')
30
- })
31
-
32
- test('F12: 私钥块与连接串结构化模式豁免熵检查', () => {
33
- const pem = '-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----'
34
- assert.ok(scanSecrets(pem).hits.some(h => h.category === 'private-key'))
35
- const cs = 'mongodb://admin:S3cretPw@cluster0.abc.mongodb.net/db'
36
- assert.ok(scanSecrets(cs).hits.some(h => h.category === 'connection-string'))
37
- })
38
-
39
- test('F12: 连接串误报防护——无凭据连接串不命中', () => {
40
- const t = 'mongodb://localhost:27017/mydb' // 无用户:密码@ 结构
41
- const { hits } = scanSecrets(t)
42
- assert.equal(hits.length, 0)
43
- })
44
-
45
- test('F12: redact 全遮蔽(保留首尾 ≤4)+ 原文不可逆', () => {
46
- const key = 'sk-proj-abcdefghij0123456789abcd'
47
- const t = `const key = "${key}" // OPENAI_API_KEY`
48
- const { text, redacted } = redactSecrets(t)
49
- assert.equal(redacted, 1)
50
- assert.ok(!text.includes(key), '原文 key 不可见于脱敏后文本')
51
- assert.ok(text.includes('sk-p****abcd'), '遮蔽格式 = 首 4 + **** + 尾 4')
52
- })
53
-
54
- test('F12: 熵函数基准', () => {
55
- assert.ok(shannonEntropy('aaaaaaaa') < 0.1)
56
- assert.ok(shannonEntropy('Xy3kP9mQ2vB7') > 3.2)
57
- })
58
-
59
- test('F12: 模式库 6 类齐备且可枚举(治理视图数据源)', () => {
60
- const cats = new Set(PATTERN_LIBRARY.map(p => p.category))
61
- for (const c of ['api-key', 'private-key', 'connection-string', 'cloud-credential', 'bearer-token']) {
62
- assert.ok(cats.has(c as any), `类别 ${c} 存在`)
63
- }
64
- assert.ok(PATTERN_LIBRARY.length >= 20, '模式 ≥20 条(v1 目标 30 内精选)')
65
- })
66
-
67
- test('F12: D-3 标定——正负 1:9 基准集粗标(FPR ≤5% 门禁可执行)', () => {
68
- // 负样本 90 条:常见代码片段(不含真实凭据)
69
- const negatives: string[] = []
70
- for (let i = 0; i < 90; i++) {
71
- negatives.push([
72
- `const cfg = { port: ${3000 + i}, debug: true }`,
73
- `await fetch('https://api.example.com/v1/items?page=${i}')`,
74
- `// TODO: handle error case ${i}`,
75
- `const xs = [${i}, ${i + 1}, ${i + 2}].map(x => x * 2)`,
76
- ][i % 4])
77
- }
78
- let fp = 0
79
- for (const n of negatives) if (scanSecrets(n).hits.length > 0) fp++
80
- const fpr = fp / negatives.length
81
- assert.ok(fpr <= 0.05, `FPR=${(fpr * 100).toFixed(1)}% ≤5%(D-3)`)
82
- // 正样本 10 条
83
- const positives = [
84
- 'const k = "sk-proj-abcdefghij0123456789abcd" // openai key',
85
- 'const g = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"',
86
- 'xoxb-123456789-abcdefgh',
87
- 'AKIAIOSFODNN7EXAMPLE',
88
- '-----BEGIN RSA PRIVATE KEY-----',
89
- 'postgres://u:pX9k2m@db.internal:5432/app',
90
- 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.abc',
91
- 'api_key = "a1B2c3D4e5F6g7H8i9J0"',
92
- 'password: "Sup3rS3cret!"',
93
- 'client_secret = "cs_live_abcdefghijklmnop"',
94
- ]
95
- let tp = 0
96
- for (const p of positives) if (scanSecrets(p).hits.length > 0) tp++
97
- const recall = tp / positives.length
98
- assert.ok(recall >= 0.85, `正样本召回 ${(recall * 100).toFixed(0)}% ≥85%(正式 <1.5% 漏报为万级基准集目标)`)
99
- })
100
-
101
- // ==================== Seatbelt profile(darwin 后端,纯生成器测试) ====================
102
-
103
- test('Seatbelt: profile fail-closed 结构(deny default + 网络拒绝 + .git 保护)', () => {
104
- const p = buildSeatbeltProfile('workspace-write', '/home/dev/proj')
105
- assert.ok(p.includes('(deny default)'), 'fail-closed 顶层')
106
- assert.ok(p.includes('(deny network*)'), 'deny-by-default 网络')
107
- assert.ok(p.includes('(allow file-write* (subpath "/home/dev/proj"))'))
108
- assert.ok(p.includes(`(deny file-write* (subpath "/home/dev/proj/.git"))`), '.git 受保护路径')
109
- assert.ok(p.includes('(subpath "/usr")'), '系统只读面')
110
- })
111
-
112
- test('Seatbelt: read-only 不含 file-write*;danger-full-access 不生成 profile', () => {
113
- const ro = buildSeatbeltProfile('read-only', '/w')
114
- assert.ok(!ro.includes('file-write*'), 'read-only 无写授权')
115
- assert.throws(() => buildSeatbeltProfile('danger-full-access', '/w'), /danger-full-access/)
116
- })
117
-
118
- test('Seatbelt: workspace 引号转义(注入防护)', () => {
119
- const p = buildSeatbeltProfile('workspace-write', '/tmp/x"y')
120
- assert.ok(!p.includes('"/tmp/x"y"'), '恶意引号被转义')
121
- assert.ok(p.includes('\\"'))
122
- })
123
-
124
- // ==================== win32 spike 准备(S8 结论门输入) ====================
125
-
126
- test('win32 spike: 脚本存在且 win32 外环境输出 SKIP(判据清单可达)', () => {
127
- const out = execFileSync(process.execPath, ['--experimental-transform-types', 'scripts/win32-koffi-spike.ts'], {
128
- encoding: 'utf-8', timeout: 30_000,
129
- })
130
- if (process.platform !== 'win32') {
131
- assert.ok(out.includes('SKIP: spike requires win32'))
132
- } else {
133
- // M5-S26 起四判据自动断言整合,win32 上以 VERDICT 收口(四判据全 PASS 或逐条列明)
134
- assert.ok(out.includes('VERDICT:'), '四判据报告以 VERDICT 收口')
135
- }
136
- })