@lqc123qwe/car-runtime 1.0.0-rc.1
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.
- package/.github/workflows/ci.yml +12 -0
- package/.github/workflows/release.yml +21 -0
- package/README.md +122 -0
- package/adr/ADR-001.md +4 -0
- package/adr/ADR-002.md +5 -0
- package/adr/ADR-003.md +35 -0
- package/adr/host-onboarding.md +68 -0
- package/adr/mlps-audit-checklist.md +33 -0
- package/adr/win32-spike.md +45 -0
- package/package.json +25 -0
- package/scripts/ptc-baseline/run-baseline.ts +118 -0
- package/scripts/release-pipeline.ts +189 -0
- package/scripts/win32-koffi-spike.ts +34 -0
- package/src/authz/authz.ts +96 -0
- package/src/cli.ts +122 -0
- package/src/governance/dump.ts +41 -0
- package/src/host/facade.ts +99 -0
- package/src/host/hostGateway.ts +98 -0
- package/src/host/mappings.ts +74 -0
- package/src/host/stdio.ts +74 -0
- package/src/kernel/context.ts +208 -0
- package/src/kernel/events.ts +99 -0
- package/src/load/loader.ts +165 -0
- package/src/load/registry.ts +88 -0
- package/src/load/verifier.ts +67 -0
- package/src/loop/goal.ts +53 -0
- package/src/loop/recover.ts +47 -0
- package/src/loop/stop.ts +200 -0
- package/src/mcp/gateway.ts +118 -0
- package/src/ptc/budget.ts +37 -0
- package/src/ptc/erasable.ts +28 -0
- package/src/ptc/runCode.ts +127 -0
- package/src/ptc/sdk.ts +28 -0
- package/src/ptc/worker-entry.ts +67 -0
- package/src/sandbox/sandbox.ts +199 -0
- package/src/security/secrets.ts +111 -0
- package/src/session/export.ts +110 -0
- package/src/session/log.ts +120 -0
- package/src/telemetry/metrics.ts +38 -0
- package/test/kernel.spec.ts +136 -0
- package/test/s11.spec.ts +129 -0
- package/test/s12.spec.ts +135 -0
- package/test/s13.spec.ts +117 -0
- package/test/s14.spec.ts +117 -0
- package/test/s15.spec.ts +80 -0
- package/test/s17.spec.ts +102 -0
- package/test/s18.spec.ts +72 -0
- package/test/s2.spec.ts +245 -0
- package/test/s3.spec.ts +179 -0
- package/test/s4.spec.ts +169 -0
- package/test/s5.spec.ts +95 -0
- package/test/s6.spec.ts +169 -0
- package/test/s7.spec.ts +135 -0
- package/test/s9.spec.ts +89 -0
package/test/s5.spec.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
assert.ok(out.includes('SKIP: koffi not installed') || out.includes('READY'), '判据清单或 READY')
|
|
134
|
+
}
|
|
135
|
+
})
|
package/test/s9.spec.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { Context } from '../src/kernel/context.ts'
|
|
4
|
+
import { declareEvent, EventBus } from '../src/kernel/events.ts'
|
|
5
|
+
import { dumpAssembly, dumpEventGraph } from '../src/governance/dump.ts'
|
|
6
|
+
import { exportForensicsBundle, verifyBundle, complianceStatement, FIELD_STANDARD_MAP, AUDIT_FIELDS } from '../src/session/export.ts'
|
|
7
|
+
import { SessionLog } from '../src/session/log.ts'
|
|
8
|
+
|
|
9
|
+
// ==================== F14 治理视图 ====================
|
|
10
|
+
|
|
11
|
+
test('F14: 配置树 dump——插件/依赖/服务/副作用计数完整', () => {
|
|
12
|
+
const ctx = new Context()
|
|
13
|
+
ctx.plugin({ name: 'db', apply: (c) => { c.provide('conn', { url: 'x' }); c.effect(() => () => {}, 'timer') } })
|
|
14
|
+
ctx.plugin({ name: 'api', inject: ['conn'], apply: () => {} })
|
|
15
|
+
const tree = dumpAssembly(ctx)
|
|
16
|
+
const db = tree.plugins.find(p => p.name === 'db')!
|
|
17
|
+
assert.equal(db.state, 'ACTIVE')
|
|
18
|
+
assert.deepEqual(db.provides, ['conn'])
|
|
19
|
+
assert.equal(db.effects, 1)
|
|
20
|
+
assert.deepEqual(db.effectLabels, ['timer'])
|
|
21
|
+
const api = tree.plugins.find(p => p.name === 'api')!
|
|
22
|
+
assert.deepEqual(api.inject, ['conn'])
|
|
23
|
+
assert.ok(tree.services.some(s => s.name === 'conn' && s.provider === 'db'))
|
|
24
|
+
assert.equal(tree.pending.length, 0)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('F14: 产消图——目录 × handlerChain,priority 执行序', () => {
|
|
28
|
+
declareEvent('gov/e', 'serial')
|
|
29
|
+
const bus = new EventBus()
|
|
30
|
+
bus.on('gov/e', () => {}, { label: 'low', priority: 10 })
|
|
31
|
+
bus.on('gov/e', () => {}, { label: 'high', priority: -1 })
|
|
32
|
+
const { events, edges } = dumpEventGraph(bus, new Map([['gov/e', { mode: 'serial' }]]))
|
|
33
|
+
assert.equal(events[0].mode, 'serial')
|
|
34
|
+
assert.deepEqual(events[0].consumers.map(c => c.label), ['high', 'low']) // 实际执行序
|
|
35
|
+
assert.equal(edges.filter(e => e.direction === 'producer').length, 1)
|
|
36
|
+
assert.equal(edges.filter(e => e.direction === 'consumer').length, 2)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// ==================== F15 合规导出增强 ====================
|
|
40
|
+
|
|
41
|
+
function sampleLog(): SessionLog {
|
|
42
|
+
const log = new SessionLog()
|
|
43
|
+
log.append('user', 'user', 'T1', { text: 'hi' })
|
|
44
|
+
log.append('model', 'toolCall', 'T1', { id: 'c1', tool: 't' })
|
|
45
|
+
log.append('model', 'toolResult', 'T1', { id: 'c1', result: 'ok' })
|
|
46
|
+
log.append('runtime', 'turnEnd', 'T1', null, { reason: 'completed' })
|
|
47
|
+
return log
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test('F15: 取证包——逐文件 SHA-256 + 链锚点 + 审计元数据', () => {
|
|
51
|
+
const log = sampleLog()
|
|
52
|
+
const bundle = exportForensicsBundle(log, {
|
|
53
|
+
sessionId: 'S1', exportedBy: 'alice', exportedAt: '2026-09-04T14:00:00Z',
|
|
54
|
+
runtimeVersion: '0.1.0', sessionRange: { fromSeq: 0, toSeq: 3 },
|
|
55
|
+
})
|
|
56
|
+
assert.equal(bundle.manifest.format, 'car-forensics/1')
|
|
57
|
+
assert.equal(bundle.manifest.verifyChainAtExport, 'PASS')
|
|
58
|
+
assert.equal(bundle.manifest.eventCount, 4)
|
|
59
|
+
assert.ok(bundle.manifest.chainTail)
|
|
60
|
+
assert.equal(bundle.files.length, 2)
|
|
61
|
+
assert.ok(bundle.files.some(f => f.name === 'manifest.json'))
|
|
62
|
+
assert.equal(verifyBundle(bundle).ok, true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('F15: 篡改文件 → verifyBundle 拒绝;断链 → 导出中止', () => {
|
|
66
|
+
const log = sampleLog()
|
|
67
|
+
const bundle = exportForensicsBundle(log, { sessionId: 'S', exportedBy: 'a', exportedAt: 'x', runtimeVersion: 'v', sessionRange: { fromSeq: 0, toSeq: 3 } })
|
|
68
|
+
bundle.files[0].content = bundle.files[0].content.replace('"hi"', '"HACKED"')
|
|
69
|
+
const v = verifyBundle(bundle)
|
|
70
|
+
assert.equal(v.ok, false)
|
|
71
|
+
assert.equal(v.brokenFile, bundle.files[0].name)
|
|
72
|
+
// 断链导出中止
|
|
73
|
+
const log2 = sampleLog()
|
|
74
|
+
;(log2.events as any)[1].payload = { tampered: true }
|
|
75
|
+
assert.throws(() => exportForensicsBundle(log2, { sessionId: 'S', exportedBy: 'a', exportedAt: 'x', runtimeVersion: 'v', sessionRange: { fromSeq: 0, toSeq: 3 } }), /CAR-E-EXPORT/)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('T-7: 三标分层——字段映射与核心字段不可裁剪', () => {
|
|
79
|
+
assert.equal(AUDIT_FIELDS.length, 13)
|
|
80
|
+
for (const std of ['soc2', 'gdpr', 'mlps'] as const) {
|
|
81
|
+
const s = complianceStatement(std)
|
|
82
|
+
assert.ok(s.fields.length >= 6, `${std} 字段集非空`)
|
|
83
|
+
assert.equal(s.coreMissing.length, 0, `${std} 不可裁剪核心字段必须全含(9 核心字段三标并集设计保证)`)
|
|
84
|
+
assert.ok(s.statement.length > 40)
|
|
85
|
+
}
|
|
86
|
+
// 等保 b) 条四要素:日期时间/用户/事件类型/是否成功 均在 mlps 字段集
|
|
87
|
+
const mlps = complianceStatement('mlps').fields
|
|
88
|
+
for (const f of ['ts', 'actor', 'eventType', 'result/status']) assert.ok(mlps.includes(f), `等保 b) 要素 ${f}`)
|
|
89
|
+
})
|