@lqc123qwe/car-runtime 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,349 @@
1
+ /**
2
+ * 1.1 · 签名收尾最小集(S26):plugin-sign CLI / doctor 签名行 + keychain 专测 / 配置文件通道
3
+ *
4
+ * 覆盖(1.1-迭代规划 W2-1/W2-2/W2-3/W2-5):
5
+ * - plugin-sign keygen:密钥对可完成签验闭环(与 verifier/sigGate 签验同源)
6
+ * - plugin-sign sign:与指南 §2 node -e 手工口径字节一致(同私钥同文件 → 同 sidecar)
7
+ * - plugin-sign CLI 真实子进程:keygen/sign/verify 全链 + 退出码家规(0/1/2)+ 密钥只落运行期临时目录
8
+ * - doctor 签名检查行 + doctorKeychain/CLI keychain 行专测(M8 §4.6「已接线未专测」补齐)
9
+ * - car.config.json 配置文件通道:发现/校验/优先级(flag > env > 配置 > 缺省)/fail-visible
10
+ */
11
+ import { test } from 'node:test'
12
+ import assert from 'node:assert/strict'
13
+ import { spawn } from 'node:child_process'
14
+ import { createHash, generateKeyPairSync, sign as cryptoSign } from 'node:crypto'
15
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
16
+ import { tmpdir } from 'node:os'
17
+ import { join, dirname } from 'node:path'
18
+ import { fileURLToPath } from 'node:url'
19
+ import { generateSigningKeypair, signPluginFile } from '../src/load/sign.ts'
20
+ import { verifyPluginFile } from '../src/load/sigGate.ts'
21
+ import { loadCarConfig, mergeSignatureGate, CONFIG_FILENAME, type CarConfig } from '../src/load/config.ts'
22
+ import { doctorKeychain, doctorSignature } from '../src/dx/doctor.ts'
23
+
24
+ const CLI = join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'cli.ts')
25
+
26
+ // ==================== fixture(s24 口径:密钥与签名夹具只落运行期临时目录——R-1) ====================
27
+
28
+ function makePlugin(dir: string, file: string, tool: string, body = ''): string {
29
+ const p = join(dir, file)
30
+ writeFileSync(p, `export const manifest = { name: 'p', version: '1.0.0' }\nexport default function apply(api) {\n api.registerTool({ name: '${tool}', run: async () => 'ok' })\n}\n${body}`)
31
+ return p
32
+ }
33
+
34
+ function withTempDir(name: string, fn: (dir: string) => Promise<void> | void): Promise<void> {
35
+ const dir = mkdtempSync(join(tmpdir(), `car-s26-${name}-`))
36
+ // 同步回调同步执行(失败以正常断言失败呈现,不落入微任务 unhandledRejection);异步回调经 Promise 收尾后清理
37
+ try {
38
+ const r = fn(dir)
39
+ const cleanup = () => { try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ } }
40
+ if (r instanceof Promise) return r.finally(cleanup)
41
+ cleanup()
42
+ return Promise.resolve()
43
+ } catch (e) {
44
+ try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ }
45
+ throw e
46
+ }
47
+ }
48
+
49
+ /** 真实子进程跑 CLI(E-6 同款 spawn 口径;cwd 注入使密钥/配置文件只落临时目录) */
50
+ function car(args: string[], opts: { cwd?: string; env?: NodeJS.ProcessEnv } = {}): Promise<{ code: number; stdout: string; stderr: string }> {
51
+ return new Promise((resolve, reject) => {
52
+ const child = spawn(process.execPath, ['--experimental-transform-types', CLI, ...args], {
53
+ cwd: opts.cwd,
54
+ env: opts.env ? { ...process.env, ...opts.env } : process.env,
55
+ stdio: ['ignore', 'pipe', 'pipe'],
56
+ })
57
+ let out = ''
58
+ let err = ''
59
+ child.stdout.on('data', d => { out += d })
60
+ child.stderr.on('data', d => { err += d })
61
+ child.on('error', reject)
62
+ child.on('exit', code => resolve({ code: code ?? -1, stdout: out, stderr: err }))
63
+ })
64
+ }
65
+
66
+ // ==================== plugin-sign 原语(W2-1) ====================
67
+
68
+ test('S26: keygen 原语——密钥对完成签验闭环(generateSigningKeypair + signPluginFile 与 verifier/sigGate 同源互验)', () => {
69
+ withTempDir('keygen-primitive', dir => {
70
+ const kp = generateSigningKeypair()
71
+ const f = makePlugin(dir, 'p.ts', 't')
72
+ const r = signPluginFile(f, kp.privateKeyDer)
73
+ assert.match(r.manifestHash, /^[0-9a-f]{64}$/)
74
+ assert.ok(existsSync(r.sidecar), 'sidecar <file>.minisig 在位')
75
+ // 与 s24 signSidecar 同口径验签:enforce + 信任根 → 静默放行
76
+ const g = verifyPluginFile(f, { mode: 'enforce', trustRootPublicKey: kp.publicKeyBase64 })
77
+ assert.equal(g.allowed, true)
78
+ assert.equal(g.warning, undefined)
79
+ // 篡改后同信任根 → 硬拒绝
80
+ writeFileSync(f, readFileSync(f, 'utf-8') + '// tampered\n')
81
+ const g2 = verifyPluginFile(f, { mode: 'enforce', trustRootPublicKey: kp.publicKeyBase64 })
82
+ assert.equal(g2.allowed, false)
83
+ assert.match(g2.error!, /signature verification FAILED/)
84
+ })
85
+ })
86
+
87
+ test('S26: sign 产物与指南 node -e 手工口径字节一致——同私钥同文件产出同 sidecar(签名确定性)', () => {
88
+ withTempDir('sign-equivalence', dir => {
89
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519')
90
+ const privDer = privateKey.export({ type: 'pkcs8', format: 'der' }) as Buffer
91
+ const pubB64 = publicKey.export({ type: 'spki', format: 'der' }).toString('base64')
92
+ const f = makePlugin(dir, 'p.ts', 't')
93
+ // 指南 §2 第 2 步原样逻辑(node -e 等价实现)
94
+ const manifestHash = createHash('sha256').update(readFileSync(f)).digest('hex')
95
+ const manual = cryptoSign(null, Buffer.from(manifestHash), { key: privDer, format: 'der', type: 'pkcs8' }).toString('base64')
96
+ const r = signPluginFile(f, privDer)
97
+ assert.equal(readFileSync(r.sidecar, 'utf-8'), manual, 'signPluginFile 与手工口径产出逐字节一致')
98
+ assert.equal(verifyPluginFile(f, { mode: 'warn', trustRootPublicKey: pubB64 }).allowed, true)
99
+ })
100
+ })
101
+
102
+ // ==================== plugin-sign CLI(真实子进程) ====================
103
+
104
+ test('S26: CLI keygen——临时 cwd 产出 .priv/.pub + stdout 信任根;重复生成拒绝、--force 覆盖', async () => {
105
+ await withTempDir('cli-keygen', async dir => {
106
+ const r1 = await car(['plugin-sign', 'keygen'], { cwd: dir })
107
+ assert.equal(r1.code, 0, r1.stderr)
108
+ assert.ok(existsSync(join(dir, 'car-release.priv')), '缺省前缀私钥在位')
109
+ assert.ok(existsSync(join(dir, 'car-release.pub')), '缺省前缀公钥在位')
110
+ assert.match(r1.stdout, /CAR_TRUST_ROOT/)
111
+ const pub = readFileSync(join(dir, 'car-release.pub'), 'utf-8').trim()
112
+ assert.match(pub, /^[A-Za-z0-9+/=]+$/, '公钥为单段 base64(sigGate 读取口径)')
113
+ assert.match(r1.stdout, new RegExp(pub.slice(0, 16)), 'stdout 打印信任根值')
114
+
115
+ const r2 = await car(['plugin-sign', 'keygen'], { cwd: dir })
116
+ assert.equal(r2.code, 2, '已存在拒绝覆盖')
117
+ assert.match(r2.stderr, /拒绝覆盖/)
118
+ const r3 = await car(['plugin-sign', 'keygen', '--force'], { cwd: dir })
119
+ assert.equal(r3.code, 0, '--force 显式覆盖')
120
+
121
+ const r4 = await car(['plugin-sign', 'keygen', '--out', 'alt'], { cwd: dir })
122
+ assert.equal(r4.code, 0, '--out 自定义前缀')
123
+ assert.ok(existsSync(join(dir, 'alt.priv')) && existsSync(join(dir, 'alt.pub')))
124
+
125
+ const r5 = await car(['plugin-sign', 'keygen', '--nope'], { cwd: dir })
126
+ assert.equal(r5.code, 2, '未知选项 exit 2')
127
+ })
128
+ })
129
+
130
+ test('S26: CLI sign + verify 全链——keygen→sign→verify PASS;篡改/缺签 FAIL exit 1;无信任根 exit 2', async () => {
131
+ await withTempDir('cli-sign-verify', async dir => {
132
+ assert.equal((await car(['plugin-sign', 'keygen'], { cwd: dir })).code, 0)
133
+ const f = makePlugin(dir, 'plugin.ts', 't')
134
+
135
+ const rs = await car(['plugin-sign', 'sign', 'plugin.ts'], { cwd: dir })
136
+ assert.equal(rs.code, 0, rs.stderr)
137
+ assert.match(rs.stdout, /signed plugin\.ts fp:[0-9a-f]{12}/)
138
+ assert.ok(existsSync(`${f}.minisig`), 'sidecar 相对 cwd 命中同文件')
139
+
140
+ const rv = await car(['plugin-sign', 'verify', 'plugin.ts'], { cwd: dir })
141
+ assert.equal(rv.code, 0, rv.stderr)
142
+ assert.match(rv.stdout, /verify: PASS plugin\.ts fp:[0-9a-f]{12}/)
143
+ assert.match(rv.stdout, /信任根来源 pubfile/, '信任根回落到 ./car-release.pub')
144
+
145
+ // 篡改 → FAIL exit 1(数据校验失败)
146
+ writeFileSync(f, readFileSync(f, 'utf-8') + '// tampered\n')
147
+ const rv2 = await car(['plugin-sign', 'verify', 'plugin.ts'], { cwd: dir })
148
+ assert.equal(rv2.code, 1)
149
+ assert.match(rv2.stderr, /verify: FAIL plugin\.ts/)
150
+ assert.match(rv2.stderr, /signature verification FAILED/)
151
+
152
+ // 重签恢复 → PASS(改文件必须重签口径)
153
+ assert.equal((await car(['plugin-sign', 'sign', 'plugin.ts'], { cwd: dir })).code, 0)
154
+ assert.equal((await car(['plugin-sign', 'verify', 'plugin.ts'], { cwd: dir })).code, 0)
155
+
156
+ // 缺签文件 → FAIL exit 1(verify 固定 enforce:缺签即 FAIL)
157
+ makePlugin(dir, 'unsigned.ts', 't2')
158
+ const rv3 = await car(['plugin-sign', 'verify', 'unsigned.ts'], { cwd: dir })
159
+ assert.equal(rv3.code, 1)
160
+ assert.match(rv3.stderr, /signature missing/)
161
+
162
+ // 无信任根(无 flag/env/pub 文件——换无 car-release.pub 的子目录 cwd)→ exit 2
163
+ const noTrustDir = join(dir, 'no-trust')
164
+ mkdirSync(noTrustDir)
165
+ const rv4 = await car(['plugin-sign', 'verify', f], { cwd: noTrustDir, env: { CAR_TRUST_ROOT: '' } })
166
+ assert.equal(rv4.code, 2, 'cwd 无 car-release.pub 时显式报无信任根')
167
+ assert.match(rv4.stderr, /无信任根/)
168
+
169
+ // --trust-root flag 显式提供 → PASS(flag > env > pubfile 优先级序)
170
+ const pub = readFileSync(join(dir, 'car-release.pub'), 'utf-8').trim()
171
+ const rv5 = await car(['plugin-sign', 'verify', 'plugin.ts', '--trust-root', pub], { cwd: dir })
172
+ assert.equal(rv5.code, 0, rv5.stderr)
173
+ assert.match(rv5.stdout, /信任根来源 flag/)
174
+ })
175
+ })
176
+
177
+ test('S26: CLI sign 输入面——缺私钥文件 exit 2、缺文件参数 exit 2、不可读插件 exit 1、--key 显式路径', async () => {
178
+ await withTempDir('cli-sign-input', async dir => {
179
+ const f = makePlugin(dir, 'p.ts', 't')
180
+ const r1 = await car(['plugin-sign', 'sign', 'p.ts'], { cwd: dir })
181
+ assert.equal(r1.code, 2, '无 car-release.priv → 用法/输入不可用')
182
+ assert.match(r1.stderr, /私钥不可读/)
183
+
184
+ const r2 = await car(['plugin-sign', 'sign'], { cwd: dir })
185
+ assert.equal(r2.code, 2, '缺文件参数')
186
+
187
+ const kp = generateSigningKeypair()
188
+ const keyPath = join(dir, 'my.priv')
189
+ writeFileSync(keyPath, kp.privateKeyDer)
190
+ const r3 = await car(['plugin-sign', 'sign', 'p.ts', '--key', 'my.priv'], { cwd: dir })
191
+ assert.equal(r3.code, 0, r3.stderr)
192
+ const g = verifyPluginFile(f, { mode: 'enforce', trustRootPublicKey: kp.publicKeyBase64 })
193
+ assert.equal(g.allowed, true, '--key 私钥签名可被对应公钥验证')
194
+
195
+ const r4 = await car(['plugin-sign', 'sign', 'missing.ts', '--key', 'my.priv'], { cwd: dir })
196
+ assert.equal(r4.code, 1, '插件文件不可读 = sign 失败(数据面)')
197
+ assert.match(r4.stderr, /sign FAIL missing\.ts/)
198
+ })
199
+ })
200
+
201
+ // ==================== 配置文件通道(W2-3,1.1-S3) ====================
202
+
203
+ test('S26: 配置发现——cwd 无 car.config.json = 正常态空配置;显式路径必须存在(缺 = CAR-E-CONFIG)', () => {
204
+ withTempDir('config-discovery', dir => {
205
+ const r1 = loadCarConfig({ cwd: dir })
206
+ assert.equal(r1.error, undefined)
207
+ assert.equal(r1.path, undefined, '未发现 = path 缺省(正常态非错误)')
208
+ assert.deepEqual(r1.config, {})
209
+ const r2 = loadCarConfig({ explicitPath: join(dir, 'missing.json'), cwd: dir })
210
+ assert.match(r2.error!, /CAR-E-CONFIG: 配置文件不存在/)
211
+ })
212
+ })
213
+
214
+ test('S26: 配置校验 fail-visible——坏 JSON / 未知顶层键 / 未登记 sandbox 键 / 类型错全部显式拒绝', () => {
215
+ withTempDir('config-fail-visible', dir => {
216
+ const bad = (name: string, content: string) => {
217
+ const p = join(dir, name)
218
+ writeFileSync(p, content)
219
+ const r = loadCarConfig({ explicitPath: p, cwd: dir })
220
+ assert.match(r.error!, /CAR-E-CONFIG/, `${name} 应显式拒绝`)
221
+ return r
222
+ }
223
+ bad('broken.json', '{ sandbox: {')
224
+ bad('toplevel.json', JSON.stringify({ plugins: [] }))
225
+ bad('unknown-sandbox.json', JSON.stringify({ sandbox: { unsigned: { allow: true }, nope: 1 } }))
226
+ bad('unknown-unsigned.json', JSON.stringify({ sandbox: { unsigned: { allow: true, extra: 1 } } }))
227
+ bad('unknown-sig.json', JSON.stringify({ sandbox: { sig: { enforce: true, secret: 'x' } } }))
228
+ bad('type-allow.json', JSON.stringify({ sandbox: { unsigned: { allow: 'yes' } } }))
229
+ bad('type-enforce.json', JSON.stringify({ sandbox: { sig: { enforce: 1 } } }))
230
+ bad('type-trustroot.json', JSON.stringify({ sandbox: { sig: { trustRoot: 42 } } }))
231
+ bad('top-array.json', '[]')
232
+ // 有效配置:冻结键 + 新键全通过
233
+ const pub = generateSigningKeypair().publicKeyBase64
234
+ const good = join(dir, 'good.json')
235
+ writeFileSync(good, JSON.stringify({ sandbox: { unsigned: { allow: true }, sig: { enforce: true, trustRoot: pub } } }))
236
+ const ok = loadCarConfig({ explicitPath: good, cwd: dir })
237
+ assert.equal(ok.error, undefined)
238
+ assert.deepEqual(ok.config, { sandbox: { unsignedAllow: true, sigEnforce: true, sigTrustRoot: pub } })
239
+ })
240
+ })
241
+
242
+ test('S26: 优先级合并 flag > env > 配置 > 缺省——env 显式非 1 压过配置 enforce;env 未定义落配置层', () => {
243
+ const pub = generateSigningKeypair().publicKeyBase64
244
+ // 形状 = 校验器产出(扁平);类型标注让形状漂移在 tsc 期显式报错(本次接线曾在此处被嵌套手误掩盖)
245
+ const cfg: CarConfig = { sandbox: { sigEnforce: true, sigTrustRoot: pub, unsignedAllow: true } }
246
+ // 缺省(无 env 无配置)= 1.0 行为不变:warn / 无信任根 / 不豁免
247
+ const d = mergeSignatureGate({}, {})
248
+ assert.deepEqual(d, { mode: 'warn', trustRootPublicKey: undefined, unsignedAllow: false })
249
+ // 配置层生效(env 未定义)
250
+ const c = mergeSignatureGate({}, cfg)
251
+ assert.equal(c.mode, 'enforce')
252
+ assert.equal(c.trustRootPublicKey, pub)
253
+ assert.equal(c.unsignedAllow, true)
254
+ // env 显式意见压过配置:CAR_SIG_ENFORCE=0 = 显式 warn
255
+ const e0 = mergeSignatureGate({ CAR_SIG_ENFORCE: '0' }, cfg)
256
+ assert.equal(e0.mode, 'warn')
257
+ // env =1 → enforce;CAR_TRUST_ROOT 压过配置公钥;CAR_UNSIGNED_ALLOW=0 显式关豁免
258
+ const e1 = mergeSignatureGate({ CAR_SIG_ENFORCE: '1', CAR_TRUST_ROOT: 'OTHER', CAR_UNSIGNED_ALLOW: '0' }, cfg)
259
+ assert.equal(e1.mode, 'enforce')
260
+ assert.equal(e1.trustRootPublicKey, 'OTHER')
261
+ assert.equal(e1.unsignedAllow, false)
262
+ // flag 最高:env=0 也压不住 --sig-enforce
263
+ const f = mergeSignatureGate({ CAR_SIG_ENFORCE: '0' }, cfg, true)
264
+ assert.equal(f.mode, 'enforce')
265
+ })
266
+
267
+ test('S26: CLI 配置通道真实消费——car run 经 car.config.json 走 enforce 缺签拒绝(exit 1)+ 坏配置中止(exit 2)', async () => {
268
+ await withTempDir('config-cli-e2e', async dir => {
269
+ makePlugin(dir, 'plugin.ts', 't')
270
+ // car run 直载门消费配置层:enforce + 无签名 → 拒绝 exit 1(生产行为,非仅函数级)
271
+ writeFileSync(join(dir, CONFIG_FILENAME), JSON.stringify({ sandbox: { sig: { enforce: true } } }))
272
+ const r1 = await car(['run', 'plugin.ts'], { cwd: dir })
273
+ assert.equal(r1.code, 1)
274
+ assert.match(r1.stderr, /签名门禁拒绝.*signature missing/)
275
+ // env 显式 0 压过配置 → warn 放行到装配段
276
+ const r2 = await car(['run', 'plugin.ts'], { cwd: dir, env: { CAR_SIG_ENFORCE: '0' } })
277
+ assert.equal(r2.code, 0, r2.stderr)
278
+ assert.match(r2.stdout, /\[1\/5 装配\] OK/)
279
+ // 坏配置 fail-visible 中止(exit 2,不进入装载)
280
+ writeFileSync(join(dir, CONFIG_FILENAME), JSON.stringify({ sandbox: { unknown: 1 } }))
281
+ const r3 = await car(['run', 'plugin.ts'], { cwd: dir })
282
+ assert.equal(r3.code, 2)
283
+ assert.match(r3.stderr, /CAR-E-CONFIG/)
284
+ // car reload 同源消费:坏显式路径 exit 2
285
+ const r4 = await car(['reload', 'plugin.ts', '--config', 'missing.json'], { cwd: dir })
286
+ assert.equal(r4.code, 2)
287
+ assert.match(r4.stderr, /CAR-E-CONFIG: 配置文件不存在/)
288
+ })
289
+ })
290
+
291
+ // ==================== doctor 签名行 + keychain 专测(W2-2/W2-5,1.1-S2) ====================
292
+
293
+ test('S26: doctorSignature 三通道——缺省 warn/无信任根/无配置;enforce+信任根可解析;坏信任根显式 invalid', () => {
294
+ withTempDir('doctor-sig', dir => {
295
+ const d1 = doctorSignature({ env: {}, cwd: dir })
296
+ assert.equal(d1.mode, 'warn')
297
+ assert.equal(d1.trustRoot, 'absent')
298
+ assert.equal(d1.configFile, 'absent')
299
+ assert.match(d1.detail, /mode=warn/)
300
+ assert.match(d1.detail, /信任根未配置/)
301
+ const pub = generateSigningKeypair().publicKeyBase64
302
+ const d2 = doctorSignature({ env: { CAR_SIG_ENFORCE: '1', CAR_TRUST_ROOT: pub }, cwd: dir })
303
+ assert.equal(d2.mode, 'enforce')
304
+ assert.equal(d2.trustRoot, 'valid')
305
+ assert.match(d2.detail, /可解析/)
306
+ const d3 = doctorSignature({ env: { CAR_TRUST_ROOT: 'not-a-valid-key' }, cwd: dir })
307
+ assert.equal(d3.trustRoot, 'invalid')
308
+ assert.match(d3.detail, /不可解析.*拒签风险/)
309
+ })
310
+ })
311
+
312
+ test('S26: doctorSignature 配置文件通道——found / invalid 显式呈现;信任根可来自配置层(env 缺省)', () => {
313
+ withTempDir('doctor-sig-config', dir => {
314
+ const pub = generateSigningKeypair().publicKeyBase64
315
+ writeFileSync(join(dir, CONFIG_FILENAME), JSON.stringify({ sandbox: { sig: { enforce: true, trustRoot: pub } } }))
316
+ const d1 = doctorSignature({ env: {}, cwd: dir })
317
+ assert.equal(d1.mode, 'enforce', '生效模式含配置层')
318
+ assert.equal(d1.trustRoot, 'valid', '配置层信任根参与检查')
319
+ assert.equal(d1.configFile, 'found')
320
+ assert.match(d1.detail, /car\.config\.json(校验通过)/)
321
+ writeFileSync(join(dir, CONFIG_FILENAME), '{"sandbox": { broken')
322
+ const d2 = doctorSignature({ env: {}, cwd: dir })
323
+ assert.equal(d2.configFile, 'invalid')
324
+ assert.equal(d2.mode, 'warn', '坏配置回落 env/缺省口径,invalid 显式呈现')
325
+ assert.match(d2.detail, /配置文件无效/)
326
+ })
327
+ })
328
+
329
+ test('S26: doctorKeychain 专测(M8 §4.6「已接线未专测」补齐)——平台通道三态,缺席显式降级不静默', () => {
330
+ // 能力探测口径(s25 同源):darwin/linux 读通道构造面在场(调用时才 spawn);win32 无零依赖读通道 = 显式缺席
331
+ const darwin = doctorKeychain({ platform: 'darwin' })
332
+ assert.equal(darwin.available, true)
333
+ assert.equal(darwin.note, 'darwin security')
334
+ const linux = doctorKeychain({ platform: 'linux' })
335
+ assert.equal(linux.available, true)
336
+ assert.equal(linux.note, 'linux secret-tool')
337
+ const win = doctorKeychain({ platform: 'win32' })
338
+ assert.equal(win.available, false)
339
+ assert.match(win.note, /缺席|显式降级/)
340
+ })
341
+
342
+ test('S26: CLI car doctor——签名行 + keychain 行输出格式(离线 CAR_OFFLINE=1 不失败 exit 0)', async () => {
343
+ const r = await car(['doctor'], { env: { CAR_OFFLINE: '1' } })
344
+ assert.equal(r.code, 0, r.stderr)
345
+ assert.match(r.stdout, /^node: /m)
346
+ assert.match(r.stdout, /^signature: mode=(warn|enforce)/m, '1.1 签名行在位')
347
+ assert.match(r.stdout, /^keychain: (就绪|缺席(显式降级))/m, 'keychain 行格式(M8 口径)')
348
+ assert.match(r.stdout, /^connectivity: SKIPPED/m, '离线显式 SKIPPED')
349
+ })
@@ -0,0 +1,134 @@
1
+ /**
2
+ * 1.1-S4 · mcp-serve 装载接线(宿主路径签名门 + car_load_total 全链采集)
3
+ *
4
+ * 覆盖(1.1-迭代规划 W2-4):
5
+ * - --plugin 装载经六阶段流水线(verify 门先于 import()):warn 缺签横幅放行 + 计数全链进 stderr 快照
6
+ * - enforce 缺签:verify FAIL → fail-closed 启动中止 exit 1(DEC-1 语义在宿主路径同权)
7
+ * - 装载失败不静默(warn 模式亦 fail-closed——加载期显式失败红线;CAR_PLUGINS env 通道)
8
+ * - 配置通道真实消费:cwd car.config.json 提供信任根 → 好签名静默装载、快照零签名计数
9
+ *
10
+ * 边界(防口径外推):本 spec 断言的是「装载接线 + 采集全链」——插件 factory 执行 + bindCore
11
+ * 冲刷注册项真实发生;sessionTurn 仍为事件批归一化(宿主会话执行插件工具属 W1 登记后续)。
12
+ */
13
+ import { test } from 'node:test'
14
+ import assert from 'node:assert/strict'
15
+ import { spawn } from 'node:child_process'
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 { generateSigningKeypair, signPluginFile } from '../src/load/sign.ts'
21
+
22
+ const CLI = join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'cli.ts')
23
+
24
+ function makePlugin(dir: string, file: string, tool: string, manifest = { name: 'p', version: '1.0.0' }): string {
25
+ const p = join(dir, file)
26
+ writeFileSync(p, `export const manifest = ${JSON.stringify(manifest)}\nexport default function apply(api) {\n api.registerTool({ name: '${tool}', run: async () => 'ok' })\n}\n`)
27
+ return p
28
+ }
29
+
30
+ function withTempDir(name: string, fn: (dir: string) => Promise<void> | void): Promise<void> {
31
+ const dir = mkdtempSync(join(tmpdir(), `car-s27-${name}-`))
32
+ try {
33
+ const r = fn(dir)
34
+ const cleanup = () => { try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ } }
35
+ if (r instanceof Promise) return r.finally(cleanup)
36
+ cleanup()
37
+ return Promise.resolve()
38
+ } catch (e) {
39
+ try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ }
40
+ throw e
41
+ }
42
+ }
43
+
44
+ /** spawn mcp-serve 真实子进程(s18 talk 同口径,cwd/env 可注入) */
45
+ function serve(args: string[], opts: { cwd?: string; env?: NodeJS.ProcessEnv; requests?: string[] } = {}): Promise<{ code: number; outs: any[]; stderr: string }> {
46
+ return new Promise((resolve, reject) => {
47
+ const child = spawn(process.execPath, ['--experimental-transform-types', CLI, 'mcp-serve', ...args], {
48
+ cwd: opts.cwd,
49
+ env: opts.env ? { ...process.env, ...opts.env } : process.env,
50
+ stdio: ['pipe', 'pipe', 'pipe'],
51
+ })
52
+ let out = ''
53
+ let err = ''
54
+ child.stdout.on('data', d => { out += d })
55
+ child.stderr.on('data', d => { err += d })
56
+ child.on('error', reject)
57
+ child.on('exit', code => {
58
+ try { resolve({ code: code ?? -1, outs: out.split('\n').filter(Boolean).map(l => JSON.parse(l)), stderr: err }) } catch (e) { reject(e) }
59
+ })
60
+ for (const r of opts.requests ?? [
61
+ JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }),
62
+ JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'session_start', arguments: { hostSessionId: 's27' } } }),
63
+ ]) child.stdin.write(r + '\n')
64
+ child.stdin.end()
65
+ })
66
+ }
67
+
68
+ function snapshotOf(stderr: string): Record<string, number> {
69
+ return JSON.parse(/snapshot=(\{.*?\}) zeroContent/.exec(stderr)![1])
70
+ }
71
+
72
+ test('S27: --plugin 装载接线——warn 缺签横幅 + 工厂执行/bindCore 冲刷真实发生 + 计数全链进快照', async () => {
73
+ await withTempDir('wired-warn', async dir => {
74
+ makePlugin(dir, 'p.ts', 't')
75
+ const r = await serve(['--plugin', dir], { cwd: dir })
76
+ assert.equal(r.code, 0, r.stderr)
77
+ // 协议面不变:宿主 10-tool 冻结(s11 口径)
78
+ assert.equal(r.outs[0].result.tools.length, 10)
79
+ // 装载真实发生:factory 执行 + bindCore 冲刷注册项(非「门过了但插件丢弃」的假接线)
80
+ assert.match(r.stderr, /plugins loaded: p@1\.0\.0/)
81
+ assert.match(r.stderr, /tools: t/)
82
+ // warn 缺签横幅显式可见
83
+ assert.match(r.stderr, /CAR-W-SIG/)
84
+ // 计数全链:loadPlugins 内生产 → counters → 会话收口 stderr 快照(宿主路径与 CLI 路径同权)
85
+ const snap = snapshotOf(r.stderr)
86
+ assert.equal(snap['car_load_total|result=ok'], 1)
87
+ assert.equal(snap['car_unsigned_confirmed|confirmed=no'], 1)
88
+ assert.match(r.stderr, /zeroContent=true/)
89
+ })
90
+ })
91
+
92
+ test('S27: enforce 缺签——verify FAIL fail-closed 启动中止 exit 1(DEC-1 宿主路径同权)', async () => {
93
+ await withTempDir('wired-enforce', async dir => {
94
+ makePlugin(dir, 'p.ts', 't')
95
+ const r = await serve(['--plugin', dir, '--sig-enforce'], { cwd: dir })
96
+ assert.equal(r.code, 1)
97
+ assert.match(r.stderr, /\[verify\s*\] FAIL/)
98
+ assert.match(r.stderr, /CAR-E-SIG: signature missing/)
99
+ assert.match(r.stderr, /fail-closed 启动中止/)
100
+ assert.equal(r.outs.length, 0, '协议通道零输出(未进入服务循环)')
101
+ })
102
+ })
103
+
104
+ test('S27: 装载失败不静默——warn 模式下 parse FAIL 亦 fail-closed exit 1(CAR_PLUGINS env 通道)', async () => {
105
+ await withTempDir('wired-parse-fail', async dir => {
106
+ makePlugin(dir, 'bad.ts', 't', { version: '1.0.0' }) // manifest 缺 name → parse FAIL
107
+ const r = await serve([], { cwd: dir, env: { CAR_PLUGINS: dir } })
108
+ assert.equal(r.code, 1)
109
+ assert.match(r.stderr, /\[parse\s*\] FAIL/)
110
+ assert.match(r.stderr, /CAR-E-MANIFEST/)
111
+ assert.match(r.stderr, /fail-closed 启动中止/)
112
+ })
113
+ })
114
+
115
+ test('S27: 配置通道真实消费——cwd car.config.json 信任根 + 好签名 → 静默装载、快照零签名计数', async () => {
116
+ await withTempDir('wired-config-trust', async dir => {
117
+ const kp = generateSigningKeypair()
118
+ const f = makePlugin(dir, 'p.ts', 't')
119
+ signPluginFile(f, kp.privateKeyDer)
120
+ // 信任根经配置文件通道(非 env)——cwd 发现序的真实消费点(1.1-GO-5)
121
+ writeFileSync(join(dir, 'car.config.json'), JSON.stringify({ sandbox: { sig: { trustRoot: kp.publicKeyBase64 } } }))
122
+ const r = await serve(['--plugin', dir], { cwd: dir })
123
+ assert.equal(r.code, 0, r.stderr)
124
+ assert.equal(r.outs[0].result.tools.length, 10)
125
+ assert.match(r.stderr, /plugins loaded: p@1\.0\.0/)
126
+ // 好签名:零签名横幅、零签名计数(S28 口径),load_total 正常
127
+ assert.equal(/CAR-W-SIG/.test(r.stderr), false)
128
+ const snap = snapshotOf(r.stderr)
129
+ assert.equal(snap['car_load_total|result=ok'], 1)
130
+ assert.equal('car_unsigned_confirmed|confirmed=no' in snap, false)
131
+ assert.equal('car_unsigned_confirmed|confirmed=yes' in snap, false)
132
+ assert.match(r.stderr, /zeroContent=true/)
133
+ })
134
+ })