@lqc123qwe/car-runtime 1.9.0 → 1.11.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 (182) hide show
  1. package/README.md +17 -1
  2. package/dist/SHA256SUMS +1 -0
  3. package/dist/authz/authz.d.ts +46 -0
  4. package/dist/authz/authz.js +76 -0
  5. package/dist/car-runtime-1.11.0.tgz +0 -0
  6. package/dist/car-runtime-1.11.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 +82 -0
  44. package/dist/loop/stop.js +184 -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 +43 -0
  65. package/dist/runtime-core/chatStep.js +121 -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 +638 -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 +104 -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 +24 -0
  86. package/dist/seam.js +23 -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/authz/authz.ts +12 -0
  107. package/src/loop/stop.ts +43 -10
  108. package/src/runtime-core/chatStep.ts +22 -2
  109. package/src/runtime-core/llm.ts +96 -13
  110. package/src/runtime-core/types.ts +22 -1
  111. package/src/seam.ts +24 -0
  112. package/src/session/log.ts +1 -1
  113. package/.gitattributes +0 -11
  114. package/.github/workflows/ci.yml +0 -293
  115. package/.github/workflows/release.yml +0 -54
  116. package/adr/ADR-001.md +0 -4
  117. package/adr/ADR-002.md +0 -5
  118. package/adr/ADR-003.md +0 -35
  119. package/adr/host-onboarding.md +0 -68
  120. package/adr/mlps-audit-checklist.md +0 -41
  121. package/adr/win32-spike.md +0 -45
  122. package/docs/plugin-authoring.md +0 -82
  123. package/native/landlock-run/Makefile +0 -19
  124. package/native/landlock-run/landlock-run.c +0 -162
  125. package/scripts/contract-check.ts +0 -56
  126. package/scripts/invariant-n1.ts +0 -96
  127. package/scripts/package-check.ts +0 -29
  128. package/scripts/ptc-baseline/run-baseline.ts +0 -118
  129. package/scripts/release-artifacts.ts +0 -111
  130. package/scripts/release-pipeline.ts +0 -245
  131. package/scripts/secrets-baseline.ts +0 -33
  132. package/scripts/secrets-scan.ts +0 -56
  133. package/scripts/win32-ffi-debug.ts +0 -143
  134. package/scripts/win32-koffi-spike.ts +0 -537
  135. package/test/contract/eventmap.golden.json +0 -7
  136. package/test/fixtures/hello-plugin.ts +0 -5
  137. package/test/kernel.spec.ts +0 -136
  138. package/test/registry-e2e/run-e2e.ts +0 -213
  139. package/test/s11.spec.ts +0 -129
  140. package/test/s12.spec.ts +0 -135
  141. package/test/s13.spec.ts +0 -117
  142. package/test/s14.spec.ts +0 -117
  143. package/test/s15.spec.ts +0 -80
  144. package/test/s16.spec.ts +0 -133
  145. package/test/s17.spec.ts +0 -102
  146. package/test/s18.spec.ts +0 -81
  147. package/test/s19.spec.ts +0 -138
  148. package/test/s2.spec.ts +0 -245
  149. package/test/s20.spec.ts +0 -241
  150. package/test/s21.spec.ts +0 -231
  151. package/test/s22-m7-store-format.spec.ts +0 -135
  152. package/test/s23-m7-export-index.spec.ts +0 -189
  153. package/test/s24-signature-gate.spec.ts +0 -185
  154. package/test/s25-m8-runtime-core.spec.ts +0 -600
  155. package/test/s26-signature-closeout.spec.ts +0 -349
  156. package/test/s27-host-load-wiring.spec.ts +0 -134
  157. package/test/s28-session-index-followup.spec.ts +0 -236
  158. package/test/s29-otel-producer.spec.ts +0 -252
  159. package/test/s3.spec.ts +0 -179
  160. package/test/s30-release-artifacts.spec.ts +0 -127
  161. package/test/s31-real-model-wiring.spec.ts +0 -329
  162. package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
  163. package/test/s33-anthropic-adapter.spec.ts +0 -354
  164. package/test/s34-mcp-remote-transport.spec.ts +0 -250
  165. package/test/s35-config-ts-zod.spec.ts +0 -145
  166. package/test/s36-host-tool-surface.spec.ts +0 -250
  167. package/test/s37-projection-dedupe.spec.ts +0 -161
  168. package/test/s38-config-jiti.spec.ts +0 -116
  169. package/test/s39-otlp-span-semantics.spec.ts +0 -100
  170. package/test/s4.spec.ts +0 -169
  171. package/test/s40-metrics-cumulative.spec.ts +0 -67
  172. package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
  173. package/test/s42-traceparent-propagation.spec.ts +0 -415
  174. package/test/s43-remote-http-traceparent.spec.ts +0 -262
  175. package/test/s5.spec.ts +0 -95
  176. package/test/s6.spec.ts +0 -169
  177. package/test/s7.spec.ts +0 -136
  178. package/test/s7b-baseline.spec.ts +0 -20
  179. package/test/s9.spec.ts +0 -89
  180. package/test/sandbox-escape/j17-orchestrate.sh +0 -30
  181. package/test/sandbox-escape/run-matrix.sh +0 -98
  182. package/tsconfig.json +0 -18
@@ -1,136 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { Context } from '../src/kernel/context.ts'
4
- import { SessionLog } from '../src/session/log.ts'
5
-
6
- // ==================== 内核 F1/F9 + ADR-002 ====================
7
-
8
- test('F1: inject 依赖推导——provider 未就绪时 PENDING,就绪后自动挂载', () => {
9
- const ctx = new Context()
10
- const order: string[] = []
11
- ctx.plugin({ name: 'consumer', inject: ['svc'], apply: (c) => { c.get('svc'); order.push('consumer') } })
12
- ctx.plugin({ name: 'provider', apply: (c) => { order.push('provider'); c.provide('svc', { v: 1 }) } })
13
- assert.deepEqual(order, ['provider', 'consumer']) // provider 先挂载;provide 同步解锁 consumer
14
- })
15
-
16
- test('F1: 缺依赖服务时 get 显式报错(禁静默)', () => {
17
- const ctx = new Context()
18
- // apply 同步异常在加载期(plugin() 调用点)显式抛出,调用方直接感知
19
- assert.throws(() => ctx.plugin({ name: 'bad', apply: (c) => c.get('nope') }), /not provided/)
20
- assert.throws(() => ctx.get('nope'), /not provided/)
21
- })
22
-
23
- test('F1: 重复 provide 同名服务 = 加载期硬报错 + 冲突链', () => {
24
- const ctx = new Context()
25
- ctx.provide('dup', 1, 'p1')
26
- assert.throws(() => ctx.provide('dup', 2, 'p2'), /already provided by "p1".*p2/)
27
- })
28
-
29
- test('F1: 重复注册同名插件 = 显式报错', () => {
30
- const ctx = new Context()
31
- ctx.plugin({ name: 'x', apply: () => {} })
32
- assert.throws(() => ctx.plugin({ name: 'x', apply: () => {} }), /already registered/)
33
- })
34
-
35
- test('F1: fiber 内 effect 注册逆序回卷(确定性)', async () => {
36
- const ctx = new Context()
37
- const marks: string[] = []
38
- ctx.plugin({
39
- name: 'p', apply: (c) => {
40
- for (const l of ['e1', 'e2', 'e3']) c.effect(() => async () => { marks.push(l + ':end') }, l)
41
- },
42
- })
43
- await ctx.disposeRuntime()
44
- assert.deepEqual(marks, ['e3:end', 'e2:end', 'e1:end'])
45
- })
46
-
47
- test('ADR-002 strict-topo:consumer 先于 provider 卸载(逐层排空)', async () => {
48
- const ctx = new Context()
49
- const events: string[] = []
50
- const sleep = (ms: number) => new Promise<void>(r => setTimeout(r, ms))
51
- ctx.plugin({ name: 'db', apply: (c) => c.effect(() => async () => { await sleep(30); events.push('db:end') }, 'db-teardown') })
52
- ctx.plugin({ name: 'cache', inject: ['dbConn'], apply: (c) => c.effect(() => async () => { await sleep(20); events.push('cache:end') }, 'cache-teardown') })
53
- ctx.plugin({ name: 'api', inject: ['dbConn', 'cacheConn'], apply: (c) => c.effect(() => async () => { events.push('api:end') }, 'api-teardown') })
54
- ctx.provide('dbConn', {}, 'db')
55
- ctx.provide('cacheConn', {}, 'cache')
56
- const report = await ctx.disposeRuntime({ order: 'strict-topo' })
57
- assert.deepEqual(events, ['api:end', 'cache:end', 'db:end']) // 消费者层先排空
58
- assert.deepEqual(report.order, ['api', 'cache', 'db'])
59
- })
60
-
61
- test('ADR-002 concurrent 模式:跨 fiber 并发(对照/降级模式可用)', async () => {
62
- const ctx = new Context()
63
- ctx.plugin({ name: 'a', apply: (c) => c.effect(() => async () => {}, 'a') })
64
- ctx.plugin({ name: 'b', apply: (c) => c.effect(() => async () => {}, 'b') })
65
- const report = await ctx.disposeRuntime({ order: 'concurrent' })
66
- assert.equal(report.unloaded.length, 2)
67
- })
68
-
69
- test('ADR-002: strict-topo 模式下 disposer 抛错被捕获进 DisposeReport(屏障不悬挂)', async () => {
70
- const ctx = new Context()
71
- ctx.plugin({ name: 'boom', apply: (c) => c.effect(() => () => { throw new Error('boom') }, 'boom') })
72
- ctx.plugin({ name: 'ok', apply: (c) => c.effect(() => () => {}, 'ok') })
73
- const report = await ctx.disposeRuntime()
74
- assert.equal(report.errors.length, 1)
75
- assert.equal(report.errors[0].plugin, 'boom')
76
- assert.equal(report.unloaded.length, 2)
77
- })
78
-
79
- test('F9: 依赖环 = 拓扑分层显式报错(加载期失败,不进入运行)', async () => {
80
- const ctx = new Context()
81
- // 通过互 inject 构造环(S1 骨架:环在 disposeRuntime 拓扑分层时暴露)
82
- ctx.plugin({ name: 'a', inject: ['bSvc'], apply: (c) => {} })
83
- ctx.plugin({ name: 'b', inject: ['aSvc'], apply: (c) => {} })
84
- ctx.provide('bSvc', 1, 'b')
85
- ctx.provide('aSvc', 1, 'a')
86
- await assert.rejects(() => ctx.disposeRuntime(), /dependency cycle/)
87
- })
88
-
89
- test('POC-2 约束:effect 绑定插件 fiber 作用域(不串到根)', async () => {
90
- const ctx = new Context()
91
- let disposed = 0
92
- ctx.plugin({ name: 'scoped', apply: (c) => c.effect(() => () => { disposed++ }, 't') })
93
- await ctx.disposeRuntime()
94
- assert.equal(disposed, 1)
95
- })
96
-
97
- // ==================== 会话日志 F2 / N1 ====================
98
-
99
- test('F2: 哈希链篡改检出并定位序号', () => {
100
- const log = new SessionLog()
101
- log.append('user', 'user', 'T0', 'hi')
102
- log.append('model', 'assistant', 'T0', 'hello')
103
- log.append('runtime', 'turnEnd', 'T0', null, { reason: 'completed' })
104
- assert.equal(log.verifyChain(), null)
105
- ;(log.events as any)[1] = { ...(log.events as any)[1], payload: 'tampered' }
106
- assert.equal(log.verifyChain(), 1)
107
- })
108
-
109
- test('F2: deriveMessages 前缀投影(upTo 语义)', () => {
110
- const log = new SessionLog()
111
- log.append('user', 'user', 'T0', 'q1')
112
- const snapAt1 = log.deriveMessages(1)
113
- log.append('model', 'assistant', 'T0', 'a1')
114
- assert.deepEqual(log.deriveMessages(1), snapAt1)
115
- assert.equal(log.deriveMessages().length, 2)
116
- })
117
-
118
- test('N1: Model-visible means logged 断言(快照=前缀投影)', () => {
119
- const log = new SessionLog()
120
- log.append('user', 'user', 'T0', 'q')
121
- log.snapshotModelRequest()
122
- log.append('model', 'assistant', 'T0', 'a')
123
- log.append('plugin', 'toolResult', 'T0', 'r')
124
- const r = log.assertModelVisibleLogged()
125
- assert.equal(r.ok, true)
126
- })
127
-
128
- test('N1: 断言失败可检出(模拟日志被篡改后投影漂移)', () => {
129
- const log = new SessionLog()
130
- log.append('user', 'user', 'T0', 'q')
131
- log.snapshotModelRequest()
132
- // 模拟「模型看到了日志外的东西」:快照后直接改历史(绕过 append API 的场景以校验断言灵敏度)
133
- ;(log.events as any)[0] = { ...(log.events as any)[0], payload: 'mutated' }
134
- const r = log.assertModelVisibleLogged()
135
- assert.equal(r.ok, false)
136
- })
@@ -1,213 +0,0 @@
1
- /**
2
- * J-18 registry-e2e · registry 对接 E2E(M3部署设计增补 §3.6 六用例组)
3
- *
4
- * 网络腿 = 真实 Verdaccio 实例(CI 内 npx 起,ephemeral 零常驻成本):
5
- * 真实 HTTP 发布 fixture 插件 → 元数据/制品下载 → integrity 校验。
6
- * 判定腿 = CAR 源码(registry.ts 纯判定 + verifier.ts 验签)在真实制品上组合执行。
7
- *
8
- * 六用例组对账(§3.6):
9
- * ①Verdaccio 实跑:G1 发布/元数据/制品下载/integrity
10
- * ②Artifactory 协议模拟:G2 前缀路径 URL + Bearer 凭据语义(协议 fixtures,不自建 Artifactory)
11
- * ③白名单外拒绝:G3 pinned 外源硬拒绝 + 审计记录含源 URL 与包名
12
- * ④验签 fail-closed:G4 真签名通过 / 篡改制品签名失效硬拒 / enforce 模式缺签名硬拒(不可配置绕过)
13
- * ⑤断网与超时:G5 单源网络失败排除 → 候选耗尽拒绝(allowNpmFallback=false)
14
- * ⑥升级回归:G6 空白名单 + 兜底显式开启 → npm-fallback(v0.2.0 行为签名不变)
15
- * 任一组失败 exit 1;结果 JSONL 落 dist/ci/registry-e2e.jsonl。
16
- *
17
- * 运行:node --experimental-transform-types test/registry-e2e/run-e2e.ts
18
- * (CI:J-18 前置步骤 npx -y verdaccio@6 起实例后运行本脚本;本脚本自带启动器。)
19
- */
20
- import { spawn, type ChildProcess } from 'node:child_process'
21
- import { createHash, generateKeyPairSync, sign } from 'node:crypto'
22
- import { mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from 'node:fs'
23
- import { tmpdir } from 'node:os'
24
- import { join } from 'node:path'
25
- import { resolvePackage, type RegistryConfig } from '../../src/load/registry.ts'
26
- import { enforceSignature } from '../../src/load/verifier.ts'
27
-
28
- const REG = 'http://127.0.0.1:4873'
29
- const results: Array<{ group: string; case: string; ok: boolean; detail?: string }> = []
30
- const record = (group: string, c: string, ok: boolean, detail?: string) => {
31
- results.push({ group, case: c, ok, detail })
32
- console.log(`[${ok ? 'PASS' : 'FAIL'}] ${group}/${c}${detail ? ' ' + detail : ''}`)
33
- }
34
-
35
- // ── 启动 Verdaccio(§3.6:CI 内以 npx 起真实实例;allow_publish $anonymous 免鉴权发布) ──
36
- // 路径统一正斜杠(YAML 对 Windows 反斜杠转义敏感);无 uplinks → 离线安全
37
- const p = (f: string) => join(tmpdir(), `car-e2e-${f}-${process.pid}`).replace(/\\/g, '/')
38
- const CONF = `storage: ${p('storage')}
39
- auth:
40
- htpasswd: { file: ${p('htpasswd')}, max_users: -1 }
41
- uplinks: {}
42
- packages:
43
- '@car-fixture/*':
44
- access: $all
45
- publish: $anonymous
46
- unpublish: $anonymous
47
- '**':
48
- access: $all
49
- publish: $anonymous
50
- unpublish: $anonymous
51
- log: { type: stdout, format: pretty, level: warn }
52
- listen: 127.0.0.1:4873
53
- `
54
- const work = mkdtempSync(join(tmpdir(), 'car-e2e-'))
55
- const confPath = join(work, 'verdaccio.yaml')
56
- writeFileSync(confPath, CONF)
57
-
58
- let verdaccio: ChildProcess | undefined
59
- async function waitUp(deadlineMs = 60_000): Promise<boolean> {
60
- const deadline = Date.now() + deadlineMs
61
- while (Date.now() < deadline) {
62
- try { const r = await fetch(REG + '/'); if (r.ok) return true } catch { /* not yet */ }
63
- await new Promise(r => setTimeout(r, 500))
64
- }
65
- return false
66
- }
67
-
68
- /** npm pack(不需鉴权)产出真实 tarball */
69
- function npmPack(pkgDir: string): Promise<{ buf?: Buffer; filename: string; output: string }> {
70
- return new Promise((resolve) => {
71
- const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm'
72
- // Node ≥20.12:.cmd 启动必须 shell:true(CVE-2024-27980);参数为固定字符串无注入面
73
- const child = spawn(npmBin, ['pack', '--json'], { cwd: pkgDir, shell: process.platform === 'win32' })
74
- let output = ''
75
- child.stdout?.on('data', d => { output += d })
76
- child.stderr?.on('data', d => { output += d })
77
- child.on('exit', (c) => {
78
- try {
79
- const info = JSON.parse(output)[0]
80
- resolve({ buf: readFileSync(join(pkgDir, info.filename)), filename: info.filename, output })
81
- } catch { resolve({ filename: '', output: `exit=${c} ${output}` }) }
82
- })
83
- child.on('error', (e) => resolve({ filename: '', output: String(e) }))
84
- })
85
- }
86
-
87
- /**
88
- * npm publish 协议 PUT(协议 fixtures 层——§3.6「Artifactory 协议模拟」同源做法):
89
- * npm CLI 客户端在无 token 时直接 ENEEDAUTH(请求都不发出),绕过客户端手工走
90
- * npm publish 协议;verdaccio allow_publish $anonymous 在服务端放行无鉴权 PUT。
91
- */
92
- async function publishProtocol(pkg: { name: string; version: string }, tarBuf: Buffer): Promise<{ ok: boolean; status: number; body: string }> {
93
- const tgzName = pkg.name.split('/').pop()! + `-${pkg.version}.tgz`
94
- const encoded = pkg.name.replace('/', '%2f')
95
- const body = {
96
- _id: pkg.name,
97
- name: pkg.name,
98
- description: 'J-18 e2e fixture',
99
- 'dist-tags': { latest: pkg.version },
100
- access: 'public',
101
- versions: {
102
- [pkg.version]: {
103
- name: pkg.name, version: pkg.version, description: 'J-18 e2e fixture',
104
- main: 'index.js', files: ['index.js'], _id: `${pkg.name}@${pkg.version}`,
105
- dist: {
106
- shasum: createHash('sha1').update(tarBuf).digest('hex'),
107
- tarball: `${REG}/${encoded}/-/${tgzName}`,
108
- integrity: 'sha512-' + createHash('sha512').update(tarBuf).digest('base64'),
109
- },
110
- },
111
- },
112
- _attachments: {
113
- [tgzName]: { content_type: 'application/octet-stream', data: tarBuf.toString('base64'), length: tarBuf.length },
114
- },
115
- }
116
- const r = await fetch(`${REG}/${encoded}`, {
117
- method: 'PUT',
118
- headers: { 'content-type': 'application/json' },
119
- body: JSON.stringify(body),
120
- })
121
- return { ok: r.ok, status: r.status, body: (await r.text()).slice(0, 200) }
122
- }
123
-
124
- async function main(): Promise<number> {
125
- // ── G0 环境自检:Verdaccio 起不来 = 环境失败(exit 2,不构成用例失败——§2.1.3 同源语义) ──
126
- try { verdaccio = spawn('npx', ['-y', 'verdaccio@6', '-c', confPath], { stdio: 'ignore', shell: process.platform === 'win32' }) } catch { /* fallthrough */ }
127
- const up = await waitUp()
128
- if (!up) { console.error('ENV-FAIL: verdaccio not reachable on ' + REG); return 2 }
129
- record('G0', 'verdaccio-up', true)
130
-
131
- // ── fixture 插件包 ──
132
- const pkgDir = join(work, 'car-fixture-plugin')
133
- mkdirSync(pkgDir, { recursive: true })
134
- writeFileSync(join(pkgDir, 'package.json'), JSON.stringify({
135
- name: '@car-fixture/plugin', version: '1.0.0', description: 'J-18 e2e fixture',
136
- main: 'index.js', files: ['index.js'],
137
- }))
138
- writeFileSync(join(pkgDir, 'index.js'), 'export const name = "car-fixture-plugin"\n')
139
-
140
- // ════ G1 Verdaccio 实跑:发布 → 元数据 → 制品下载 → integrity ════
141
- const packed = await npmPack(pkgDir)
142
- record('G1', 'pack-fixture', !!packed.buf, packed.buf ? `${packed.buf.length}B tarball` : 'output: ' + packed.output.slice(0, 150))
143
- const pub = await publishProtocol({ name: '@car-fixture/plugin', version: '1.0.0' }, packed.buf!, packed.filename)
144
- record('G1', 'publish-fixture', pub.ok, `protocol PUT HTTP ${pub.status}${pub.ok ? '' : ' ' + pub.body}`)
145
- const metaRes = await fetch(REG + '/@car-fixture%2fplugin')
146
- const meta = await metaRes.json() as { versions: Record<string, { dist: { tarball: string; integrity: string } }> }
147
- record('G1', 'metadata-readable', metaRes.ok && !!meta.versions?.['1.0.0'], `HTTP ${metaRes.status}`)
148
- const tarballUrl = meta.versions['1.0.0'].dist.tarball.replace('localhost', '127.0.0.1')
149
- const tarRes = await fetch(tarballUrl)
150
- if (!tarRes.ok) { record('G1', 'tarball-download', false, `HTTP ${tarRes.status}`) } else record('G1', 'tarball-download', true)
151
- const tarBuf = Buffer.from(await tarRes.arrayBuffer())
152
- const dlIntegrity = 'sha512-' + createHash('sha512').update(tarBuf).digest('base64')
153
- const integrityOk = dlIntegrity === meta.versions['1.0.0'].dist.integrity
154
- record('G1', 'tarball-integrity', integrityOk, `${tarBuf.length}B sha512 ${integrityOk ? 'match' : 'MISMATCH'}`)
155
-
156
- // ════ G2 Artifactory 协议模拟:前缀路径 URL + Bearer 凭据语义(fixtures,不自建实例) ════
157
- const prefixUrl = 'https://artifactory.corp.local/npm/@car-fixture/plugin/-/plugin-1.0.0.tgz'
158
- const bearerSemantics = /^https:\/\/[^/]+\/npm\/.+\/-\/.+\.tgz$/.test(prefixUrl) // virtual 聚合前缀协议形态
159
- record('G2', 'prefix-path-protocol', bearerSemantics)
160
- const bearerHeader = { authorization: 'Bearer <fixtures-only-token>' }
161
- record('G2', 'bearer-credential-shape', typeof bearerHeader.authorization === 'string' && bearerHeader.authorization.startsWith('Bearer '))
162
-
163
- // ════ CAR 判定腿配置 ════
164
- const whitelist: RegistryConfig = {
165
- registries: [{ url: REG, priority: 1, signed: false }],
166
- allowNpmFallback: false, offline: false,
167
- }
168
- const audit: Array<Record<string, unknown>> = []
169
-
170
- // ════ G3 白名单外拒绝(AC-4.2) ════
171
- const evil = resolvePackage({ name: '@car-fixture/plugin', version: '1.0.0', pinnedRegistry: 'https://evil.example.com' }, whitelist, d => audit.push(d))
172
- record('G3', 'out-of-whitelist-hard-reject', evil.source === 'rejected')
173
- const evilAudit = audit.find(d => d.spec === '@car-fixture/plugin' && d.source === 'rejected')
174
- record('G3', 'rejection-audited-with-url-and-name', !!evilAudit && JSON.stringify(evilAudit).includes('evil.example.com'))
175
-
176
- // ════ G4 验签 fail-closed(AC-4.3,ADR-003 静态轨) ════
177
- const { publicKey, privateKey } = generateKeyPairSync('ed25519')
178
- const pubRaw = publicKey.export({ type: 'spki', format: 'der' }).toString('base64') // verifyMinisig 内部按 SPKI DER 解析
179
- const manifestHash = createHash('sha256').update(tarBuf).digest('hex')
180
- const goodSig = sign(null, Buffer.from(manifestHash), privateKey).toString('base64')
181
- const good = enforceSignature(manifestHash, { minisig: goodSig }, { trustRootPublicKey: pubRaw, mode: 'enforce' })
182
- record('G4', 'valid-signature-passes', good.allowed === true && !good.error)
183
- const tamperedHash = createHash('sha256').update(Buffer.concat([tarBuf, Buffer.from('x')])).digest('hex')
184
- const tampered = enforceSignature(tamperedHash, { minisig: goodSig }, { trustRootPublicKey: pubRaw, mode: 'enforce' })
185
- record('G4', 'tampered-artifact-hard-reject', tampered.allowed === false)
186
- const unsigned = enforceSignature(manifestHash, undefined, { trustRootPublicKey: pubRaw, mode: 'enforce' })
187
- const bypassAttempt = enforceSignature(manifestHash, undefined, { trustRootPublicKey: pubRaw, mode: 'enforce' }) // 构造绕过:无任何配置可放行 enforce 缺签
188
- record('G4', 'enforce-missing-sig-hard-reject-unconfigurable', unsigned.allowed === false && bypassAttempt.allowed === false)
189
-
190
- // ════ G5 断网与超时:单源网络失败排除 → 候选耗尽拒绝 ════
191
- const downed = resolvePackage({ name: '@car-fixture/plugin', version: '1.0.0' },
192
- { ...whitelist, allowNpmFallback: false },
193
- d => audit.push(d),
194
- { exclude: [{ url: REG, reason: 'network' }] })
195
- record('G5', 'network-excluded-candidates-exhausted', downed.source === 'rejected' && /候选耗尽/.test(downed.detail))
196
-
197
- // ════ G6 升级回归:空白名单 + 兜底显式开启 → npm-fallback(v0.2.0 行为不变,AC-4.4) ════
198
- const legacy = resolvePackage({ name: 'pkg', version: '1.0.0' }, { registries: [], allowNpmFallback: true, offline: false })
199
- record('G6', 'no-registries-fallback-signature-unchanged', legacy.source === 'npm-fallback')
200
-
201
- // ── 汇总 ──
202
- const failed = results.filter(r => !r.ok)
203
- mkdirSync('dist/ci', { recursive: true })
204
- writeFileSync('dist/ci/registry-e2e.jsonl', results.map(r => JSON.stringify(r)).join('\n') + '\n')
205
- console.log(`----\nregistry-e2e: ${results.length - failed.length}/${results.length} PASS, ${failed.length} FAIL`)
206
- return failed.length === 0 ? 0 : 1
207
- }
208
-
209
- main().then(code => {
210
- verdaccio?.kill()
211
- try { rmSync(work, { recursive: true, force: true }) } catch { /* CI tmp 清理失败不影响门禁 */ }
212
- process.exit(code)
213
- }).catch(e => { console.error('ENV-FAIL:', e); verdaccio?.kill(); process.exit(2) })
package/test/s11.spec.ts DELETED
@@ -1,129 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { deriveSessionId, normalizeHostEvent, HOST_MAPPINGS } from '../src/host/mappings.ts'
4
- import { HostGateway, HOST_TOOLS, ingestHostEvent, type RuntimeFacade } from '../src/host/hostGateway.ts'
5
- import { readFileSync, readdirSync } from 'node:fs'
6
- import { join } from 'node:path'
7
-
8
- // ==================== session id 归一化 ====================
9
-
10
- test('T-1: deriveSessionId 确定性 + 跨宿主隔离 + 无语义解析', () => {
11
- const a1 = deriveSessionId('claude-code', 'sess-42')
12
- const a2 = deriveSessionId('claude-code', 'sess-42')
13
- assert.equal(a1, a2, '幂等')
14
- assert.match(a1, /^SH-[0-9a-f]{24}$/)
15
- assert.notEqual(deriveSessionId('codex', 'sess-42'), a1, '不同宿主同 id → 不同 CAR 会话(跨宿主隔离)')
16
- assert.notEqual(deriveSessionId('claude-code', 'sess-43'), a1)
17
- })
18
-
19
- // ==================== 归一化 schema(host-mappings-v1) ====================
20
-
21
- test('T-1: 命中映射——两宿主同语义事件归一化结果等价', () => {
22
- const cc = normalizeHostEvent(HOST_MAPPINGS[0], 'user_message', { content: 'hello' })
23
- const cx = normalizeHostEvent(HOST_MAPPINGS[1], 'input_item', { text: 'hello' })
24
- assert.equal(cc.kind, 'user')
25
- assert.equal(cx.kind, 'user')
26
- assert.deepEqual(cc.payload, cx.payload) // { text: 'hello' }——等价性核心断言
27
- assert.equal(cc.degraded, false)
28
- assert.equal(cx.degraded, false)
29
- })
30
-
31
- test('T-1: 未命中 → hostRaw 降级留痕(零静默)', () => {
32
- const r = normalizeHostEvent(HOST_MAPPINGS[0], 'unknown_future_event', { foo: 1 })
33
- assert.equal(r.kind, 'hostRaw')
34
- assert.equal(r.degraded, true)
35
- assert.equal(r.payload.hostEvent, 'unknown_future_event')
36
- assert.deepEqual(r.payload.raw, { foo: 1 }, '原样保留可回溯')
37
- })
38
-
39
- test('T-1: toolCall 归一化——字段重命名(tool_name→tool / name→tool)', () => {
40
- const cc = normalizeHostEvent(HOST_MAPPINGS[0], 'tool_use', { tool_name: 'fs', tool_call_id: 'c1' })
41
- const cx = normalizeHostEvent(HOST_MAPPINGS[1], 'function_call', { name: 'fs', call_id: 'c1' })
42
- assert.deepEqual(cc.payload, cx.payload) // { tool: 'fs', id: 'c1' }
43
- })
44
-
45
- // ==================== HostGateway(9 tool 面 + 登记制) ====================
46
-
47
- function mockFacade(): RuntimeFacade {
48
- return {
49
- sessionStart: async ({ hostSessionId }) => ({ sessionId: deriveSessionId('claude-code', hostSessionId) }),
50
- sessionTurn: async () => ({ reason: 'completed', steps: 1 }),
51
- sessionStop: async () => ({ reason: 'aborted' }),
52
- sessionStatus: async () => ({ state: 'idle' }),
53
- sessionReplay: async () => ({ messages: [] }),
54
- sessionVerify: async () => ({ ok: true, brokenAt: null }),
55
- sessionExport: async () => ({ bundle: {} }),
56
- toolList: async () => ({ tools: ['t1'] }),
57
- toolCall: async () => ({ ok: true, result: 'x' }),
58
- }
59
- }
60
-
61
- test('T-1: 登记制——未登记 host 拒绝(A050001)+ 重复登记报错', async () => {
62
- const audits: string[] = []
63
- const gw = new HostGateway({ facade: mockFacade(), audit: e => audits.push(e.kind) })
64
- const r = await gw.handle('ghost', 'session_status', {})
65
- assert.equal(r.ok, false)
66
- assert.match(r.error!, /A050001/)
67
- assert.ok(audits.includes('host-rejected'), '拒绝也留痕')
68
- gw.registerHost({ hostId: 'claude-code', profile: HOST_MAPPINGS[0], transport: { onRequest: async () => ({ jsonrpc: '2.0', id: 1 }), alive: () => true, close: () => {} } })
69
- assert.throws(() => gw.registerHost({ hostId: 'claude-code', profile: HOST_MAPPINGS[0], transport: {} as never }), /already registered/)
70
- })
71
-
72
- test('T-1: 10 tool 面固定 + 未知 tool 拒绝(step 循环不暴露)', async () => {
73
- const gw = new HostGateway({ facade: mockFacade(), audit: () => {} })
74
- assert.deepEqual(gw.listTools().sort(), [...HOST_TOOLS].sort())
75
- assert.equal(HOST_TOOLS.length, 10)
76
- gw.registerHost({ hostId: 'claude-code', profile: HOST_MAPPINGS[0], transport: {} as never })
77
- const r = await gw.handle('claude-code', 'run_step', {})
78
- assert.equal(r.ok, false)
79
- assert.match(r.error!, /step 循环不暴露/)
80
- })
81
-
82
- test('T-1: 端到端分发——facade 调用 + 审计留痕', async () => {
83
- const audits: string[] = []
84
- const gw = new HostGateway({ facade: mockFacade(), audit: e => audits.push(e.kind) })
85
- gw.registerHost({ hostId: 'claude-code', profile: HOST_MAPPINGS[0], transport: {} as never })
86
- const r = await gw.handle('claude-code', 'session_start', { hostSessionId: 'sess-42' })
87
- assert.equal(r.ok, true)
88
- assert.match((r.result as { sessionId: string }).sessionId, /^SH-/)
89
- assert.ok(audits.includes('host-call'))
90
- })
91
-
92
- // ==================== 双宿主等价:契约测试框架(S13 扩全五类) ====================
93
-
94
- test('契约测试 v0: 同一 fixture 序列在两宿主 profile 下归一化等价', () => {
95
- const fixtures: Array<{ semantic: string; cc: [string, Record<string, unknown>]; cx: [string, Record<string, unknown>] }>[] = [
96
- [{
97
- semantic: 'user-input',
98
- cc: ['user_message', { content: 'hi' }],
99
- cx: ['input_item', { text: 'hi' }],
100
- }],
101
- [{
102
- semantic: 'tool-call',
103
- cc: ['tool_use', { tool_name: 'fs', tool_call_id: 'c1' }],
104
- cx: ['function_call', { name: 'fs', call_id: 'c1' }],
105
- }],
106
- ]
107
- const flat = fixtures.flat()
108
- for (const f of flat) {
109
- const a = normalizeHostEvent(HOST_MAPPINGS[0], f.cc[0], f.cc[1])
110
- const b = normalizeHostEvent(HOST_MAPPINGS[1], f.cx[0], f.cx[1])
111
- assert.equal(a.kind, b.kind, `${f.semantic}: kind 等价`)
112
- assert.deepEqual(a.payload, b.payload, `${f.semantic}: payload 等价`)
113
- assert.equal(a.degraded, b.degraded)
114
- }
115
- })
116
-
117
- test('静态架构断言: 宿主标识字符串只允许出现在 mappings 数据文件', () => {
118
- const scan = (dir: string): string[] => readdirSync(dir, { recursive: true } as never)
119
- .filter(f => String(f).endsWith('.ts'))
120
- .map(f => join(dir, String(f)))
121
- // 只扫生产代码(src/)——测试 fixture 自身含宿主标识属预期;mappings.ts 为唯一数据白名单
122
- const files = scan('src').filter(f => !f.replaceAll('\\', '/').endsWith('src/host/mappings.ts'))
123
- const offenders = files.filter(f => /claude-code|codex/.test(readFileSync(f, 'utf-8')))
124
- assert.deepEqual(offenders, [], `宿主标识泄漏到非数据文件: ${offenders.join(', ')}`)
125
- })
126
-
127
- test('ingestHostEvent: 未登记宿主归一化拒绝(登记制在归一化之前)', () => {
128
- assert.throws(() => ingestHostEvent('unregistered', 'user_message', {}), /A050001/)
129
- })
package/test/s12.spec.ts DELETED
@@ -1,135 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { PassThrough } from 'node:stream'
4
- import { HOST_MAPPINGS } from '../src/host/mappings.ts'
5
- import { HostGateway } from '../src/host/hostGateway.ts'
6
- import { createRuntimeFacade } from '../src/host/facade.ts'
7
- import { createStdioServer } from '../src/host/stdio.ts'
8
- import { loadSessionLog } from '../src/session/log.ts'
9
- import { writeFileSync, mkdtempSync, rmSync } from 'node:fs'
10
- import { tmpdir } from 'node:os'
11
- import { join } from 'node:path'
12
-
13
- function setup() {
14
- const profiles = new Map(HOST_MAPPINGS.map(h => [h.hostId, h]))
15
- const facade = createRuntimeFacade({ profiles })
16
- const audits: Array<{ kind: string }> = []
17
- const gw = new HostGateway({ facade, audit: e => audits.push({ kind: e.kind }) })
18
- for (const h of HOST_MAPPINGS) gw.registerHost({ hostId: h.hostId, profile: h, transport: {} as never })
19
- return { gw, audits, facade }
20
- }
21
-
22
- // ==================== stdio ServerTransport(JSON-RPC over 换行分隔 JSON) ====================
23
-
24
- test('S12: stdio——tools/list 返回 10 tool 面;tools/call 分发到 HostGateway', async () => {
25
- const { gw } = setup()
26
- const server = createStdioServer((tool, args) => gw.handle('claude-code', tool, args))
27
- const input = new PassThrough()
28
- const output = new PassThrough()
29
- const lines: string[] = []
30
- output.on('data', d => lines.push(...String(d).split('\n').filter(Boolean)))
31
- const done = server.serve(input, output)
32
- input.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }) + '\n')
33
- input.write(JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'session_start', arguments: { hostSessionId: 'sess-9' } } }) + '\n')
34
- input.end()
35
- await done
36
- await new Promise(r => setImmediate(r))
37
- assert.equal(lines.length, 2)
38
- const list = JSON.parse(lines[0])
39
- assert.equal(list.result.tools.length, 10)
40
- const call = JSON.parse(lines[1])
41
- assert.match(call.result.content[0].text, /"sessionId":"SH-/)
42
- })
43
-
44
- test('S12: stdio——未知 method 显式报错;非法 JSON 不崩溃', async () => {
45
- const { gw } = setup()
46
- const server = createStdioServer((tool, args) => gw.handle('claude-code', tool, args))
47
- const input = new PassThrough()
48
- const output = new PassThrough()
49
- const lines: string[] = []
50
- output.on('data', d => lines.push(...String(d).split('\n').filter(Boolean)))
51
- const done = server.serve(input, output)
52
- input.write('not-json\n')
53
- input.write(JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'host/secret' }) + '\n')
54
- input.end()
55
- await done
56
- await new Promise(r => setImmediate(r))
57
- const parseErr = JSON.parse(lines[0])
58
- assert.equal(parseErr.error.code, -32700)
59
- const unknown = JSON.parse(lines[1])
60
- assert.equal(unknown.error.code, -32601)
61
- })
62
-
63
- // ==================== 归一化数据流:宿主事件 → SessionLog 全链路 ====================
64
-
65
- test('S12: 端到端——宿主事件批归一化落哈希链,verify/replay/export 全通', async () => {
66
- const { gw } = setup()
67
- const start = await gw.handle('claude-code', 'session_start', { hostSessionId: 'sess-42' })
68
- const sessionId = (start.result as { sessionId: string }).sessionId
69
- // 宿主报一批事件(含一条未知事件——降级通道)
70
- const turn = await gw.handle('claude-code', 'session_turn', {
71
- sessionId,
72
- input: { events: [
73
- { hostEvent: 'user_message', payload: { content: '帮我查下部署状态' } },
74
- { hostEvent: 'tool_use', payload: { tool_name: 'doctor', tool_call_id: 'c1' } },
75
- { hostEvent: 'tool_result', payload: { tool_call_id: 'c1', status: 'ok' } },
76
- { hostEvent: 'assistant_message', payload: { content: '部署正常' } },
77
- { hostEvent: 'turn_complete', payload: { reason: 'completed' } },
78
- { hostEvent: 'brand_new_host_event', payload: { x: 1 } }, // 未命中 → hostRaw
79
- ] },
80
- })
81
- assert.equal((turn.result as { reason: string }).reason, 'completed')
82
- const verify = await gw.handle('claude-code', 'session_verify', { sessionId })
83
- assert.equal((verify.result as { ok: boolean }).ok, true)
84
- const replay = await gw.handle('claude-code', 'session_replay', { sessionId })
85
- const msgs = (replay.result as { messages: Array<{ role: string; content?: { text?: string } }> }).messages
86
- assert.ok(msgs.some(m => m.role === 'user' && m.content?.text === '帮我查下部署状态'), 'user 归一化入链')
87
- assert.ok(msgs.some(m => m.role === 'assistant' && m.content?.text === '部署正常'))
88
- // hostRaw 事件进日志但不进消息投影(原始通道)
89
- const exportR = await gw.handle('claude-code', 'session_export', { sessionId })
90
- const bundle = (exportR.result as { bundle: { files: { name: string; content: string }[] } }).bundle
91
- const jsonl = bundle.files[0].content
92
- assert.ok(jsonl.includes('brand_new_host_event'), '未命中事件 hostRaw 留痕')
93
- assert.ok(jsonl.includes('session_registered'), '登记事件落哈希链')
94
- // 导出包可离线重放(落盘 → loadSessionLog 断链校验)
95
- const dir = mkdtempSync(join(tmpdir(), 'car-s12-'))
96
- try {
97
- const file = join(dir, 'events.jsonl')
98
- writeFileSync(file, jsonl)
99
- const { log, brokenAt } = loadSessionLog(file)
100
- assert.equal(brokenAt, null)
101
- assert.ok(log.deriveMessages().length >= 2)
102
- } finally {
103
- try { rmSync(dir, { recursive: true, force: true }) } catch { /* 沙箱 trash 容错 */ }
104
- }
105
- })
106
-
107
- test('S12: 跨宿主等价——codex 同语义事件批产出等价日志投影', async () => {
108
- const { gw } = setup()
109
- const cc = await gw.handle('claude-code', 'session_start', { hostSessionId: 's1' })
110
- const cx = await gw.handle('codex', 'session_start', { hostSessionId: 's1' })
111
- const ccId = (cc.result as { sessionId: string }).sessionId
112
- const cxId = (cx.result as { sessionId: string }).sessionId
113
- assert.notEqual(ccId, cxId, '跨宿主会话隔离')
114
- await gw.handle('claude-code', 'session_turn', { sessionId: ccId, input: { events: [
115
- { hostEvent: 'user_message', payload: { content: 'hi' } },
116
- { hostEvent: 'tool_use', payload: { tool_name: 'fs', tool_call_id: 'c1' } },
117
- ] } })
118
- await gw.handle('codex', 'session_turn', { sessionId: cxId, input: { events: [
119
- { hostEvent: 'input_item', payload: { text: 'hi' } },
120
- { hostEvent: 'function_call', payload: { name: 'fs', call_id: 'c1' } },
121
- ] } })
122
- const r1 = await gw.handle('claude-code', 'session_replay', { sessionId: ccId })
123
- const r2 = await gw.handle('codex', 'session_replay', { sessionId: cxId })
124
- const m1 = (r1.result as { messages: unknown[] }).messages
125
- const m2 = (r2.result as { messages: unknown[] }).messages
126
- assert.deepEqual(m1, m2, '双宿主等价核心断言:投影逐字节一致')
127
- })
128
-
129
- test('S12: 审计全留痕——host-call 与拒绝均落审计(无旁路)', async () => {
130
- const { gw, audits } = setup()
131
- await gw.handle('ghost', 'session_start', {}) // 未登记
132
- await gw.handle('claude-code', 'session_start', { hostSessionId: 'x' })
133
- assert.ok(audits.some(a => a.kind === 'host-rejected'))
134
- assert.ok(audits.some(a => a.kind === 'host-call'))
135
- })