@lqc123qwe/car-runtime 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -1
- package/dist/SHA256SUMS +1 -0
- package/dist/authz/authz.d.ts +44 -0
- package/dist/authz/authz.js +65 -0
- package/dist/car-runtime-1.10.0.tgz +0 -0
- package/dist/car-runtime-1.10.0.tgz.sig.bundle +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +825 -0
- package/dist/dx/doctor.d.ts +62 -0
- package/dist/dx/doctor.js +127 -0
- package/dist/governance/dump.d.ts +47 -0
- package/dist/governance/dump.js +17 -0
- package/dist/host/facade.d.ts +13 -0
- package/dist/host/facade.js +154 -0
- package/dist/host/hostGateway.d.ts +117 -0
- package/dist/host/hostGateway.js +65 -0
- package/dist/host/mappings.d.ts +27 -0
- package/dist/host/mappings.js +52 -0
- package/dist/host/stdio.d.ts +14 -0
- package/dist/host/stdio.js +83 -0
- package/dist/kernel/context.d.ts +76 -0
- package/dist/kernel/context.js +203 -0
- package/dist/kernel/events.d.ts +37 -0
- package/dist/kernel/events.js +82 -0
- package/dist/load/config.d.ts +79 -0
- package/dist/load/config.js +285 -0
- package/dist/load/loader.d.ts +72 -0
- package/dist/load/loader.js +162 -0
- package/dist/load/registry.d.ts +44 -0
- package/dist/load/registry.js +58 -0
- package/dist/load/report.d.ts +98 -0
- package/dist/load/report.js +301 -0
- package/dist/load/sigGate.d.ts +22 -0
- package/dist/load/sigGate.js +68 -0
- package/dist/load/sign.d.ts +25 -0
- package/dist/load/sign.js +47 -0
- package/dist/load/verifier.d.ts +35 -0
- package/dist/load/verifier.js +56 -0
- package/dist/loop/goal.d.ts +31 -0
- package/dist/loop/goal.js +34 -0
- package/dist/loop/recover.d.ts +17 -0
- package/dist/loop/recover.js +39 -0
- package/dist/loop/stop.d.ts +79 -0
- package/dist/loop/stop.js +151 -0
- package/dist/mcp/gateway.d.ts +96 -0
- package/dist/mcp/gateway.js +69 -0
- package/dist/mcp/httpClient.d.ts +27 -0
- package/dist/mcp/httpClient.js +196 -0
- package/dist/mcp/stdioClient.d.ts +8 -0
- package/dist/mcp/stdioClient.js +73 -0
- package/dist/ptc/budget.d.ts +21 -0
- package/dist/ptc/budget.js +36 -0
- package/dist/ptc/erasable.d.ts +14 -0
- package/dist/ptc/erasable.js +27 -0
- package/dist/ptc/runCode.d.ts +65 -0
- package/dist/ptc/runCode.js +140 -0
- package/dist/ptc/schemaRender.d.ts +15 -0
- package/dist/ptc/schemaRender.js +90 -0
- package/dist/ptc/sdk.d.ts +27 -0
- package/dist/ptc/sdk.js +23 -0
- package/dist/ptc/worker-entry.d.ts +1 -0
- package/dist/ptc/worker-entry.js +71 -0
- package/dist/release-audit.jsonl +4 -0
- package/dist/runtime-core/chatStep.d.ts +40 -0
- package/dist/runtime-core/chatStep.js +104 -0
- package/dist/runtime-core/credentials.d.ts +49 -0
- package/dist/runtime-core/credentials.js +118 -0
- package/dist/runtime-core/errors.d.ts +30 -0
- package/dist/runtime-core/errors.js +36 -0
- package/dist/runtime-core/llm.d.ts +76 -0
- package/dist/runtime-core/llm.js +553 -0
- package/dist/runtime-core/redaction.d.ts +39 -0
- package/dist/runtime-core/redaction.js +79 -0
- package/dist/runtime-core/telemetry.d.ts +88 -0
- package/dist/runtime-core/telemetry.js +332 -0
- package/dist/runtime-core/tools.d.ts +15 -0
- package/dist/runtime-core/tools.js +12 -0
- package/dist/runtime-core/trace.d.ts +55 -0
- package/dist/runtime-core/trace.js +137 -0
- package/dist/runtime-core/types.d.ts +84 -0
- package/dist/runtime-core/types.js +13 -0
- package/dist/sandbox/sandbox.d.ts +68 -0
- package/dist/sandbox/sandbox.js +153 -0
- package/dist/sbom.json +263 -0
- package/dist/seam.d.ts +21 -0
- package/dist/seam.js +20 -0
- package/dist/security/secrets-baseline.d.ts +43 -0
- package/dist/security/secrets-baseline.js +176 -0
- package/dist/security/secrets.d.ts +45 -0
- package/dist/security/secrets.js +138 -0
- package/dist/session/export.d.ts +55 -0
- package/dist/session/export.js +81 -0
- package/dist/session/fork.d.ts +51 -0
- package/dist/session/fork.js +63 -0
- package/dist/session/format.d.ts +21 -0
- package/dist/session/format.js +49 -0
- package/dist/session/indexStore.d.ts +44 -0
- package/dist/session/indexStore.js +265 -0
- package/dist/session/log.d.ts +79 -0
- package/dist/session/log.js +157 -0
- package/dist/session/store.d.ts +9 -0
- package/dist/session/store.js +37 -0
- package/dist/telemetry/metrics.d.ts +25 -0
- package/dist/telemetry/metrics.js +26 -0
- package/package.json +17 -1
- package/src/mcp/gateway.ts +10 -2
- package/src/mcp/httpClient.ts +23 -5
- package/src/runtime-core/trace.ts +5 -0
- package/src/seam.ts +21 -0
- package/.gitattributes +0 -11
- package/.github/workflows/ci.yml +0 -293
- package/.github/workflows/release.yml +0 -54
- package/adr/ADR-001.md +0 -4
- package/adr/ADR-002.md +0 -5
- package/adr/ADR-003.md +0 -35
- package/adr/host-onboarding.md +0 -68
- package/adr/mlps-audit-checklist.md +0 -41
- package/adr/win32-spike.md +0 -45
- package/docs/plugin-authoring.md +0 -82
- package/native/landlock-run/Makefile +0 -19
- package/native/landlock-run/landlock-run.c +0 -162
- package/scripts/contract-check.ts +0 -56
- package/scripts/invariant-n1.ts +0 -96
- package/scripts/package-check.ts +0 -29
- package/scripts/ptc-baseline/run-baseline.ts +0 -118
- package/scripts/release-artifacts.ts +0 -111
- package/scripts/release-pipeline.ts +0 -245
- package/scripts/secrets-baseline.ts +0 -33
- package/scripts/secrets-scan.ts +0 -56
- package/scripts/win32-ffi-debug.ts +0 -143
- package/scripts/win32-koffi-spike.ts +0 -537
- package/test/contract/eventmap.golden.json +0 -7
- package/test/fixtures/hello-plugin.ts +0 -5
- package/test/kernel.spec.ts +0 -136
- package/test/registry-e2e/run-e2e.ts +0 -213
- package/test/s11.spec.ts +0 -129
- package/test/s12.spec.ts +0 -135
- package/test/s13.spec.ts +0 -117
- package/test/s14.spec.ts +0 -117
- package/test/s15.spec.ts +0 -80
- package/test/s16.spec.ts +0 -133
- package/test/s17.spec.ts +0 -102
- package/test/s18.spec.ts +0 -81
- package/test/s19.spec.ts +0 -138
- package/test/s2.spec.ts +0 -245
- package/test/s20.spec.ts +0 -241
- package/test/s21.spec.ts +0 -231
- package/test/s22-m7-store-format.spec.ts +0 -135
- package/test/s23-m7-export-index.spec.ts +0 -189
- package/test/s24-signature-gate.spec.ts +0 -185
- package/test/s25-m8-runtime-core.spec.ts +0 -600
- package/test/s26-signature-closeout.spec.ts +0 -349
- package/test/s27-host-load-wiring.spec.ts +0 -134
- package/test/s28-session-index-followup.spec.ts +0 -236
- package/test/s29-otel-producer.spec.ts +0 -252
- package/test/s3.spec.ts +0 -179
- package/test/s30-release-artifacts.spec.ts +0 -127
- package/test/s31-real-model-wiring.spec.ts +0 -329
- package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
- package/test/s33-anthropic-adapter.spec.ts +0 -354
- package/test/s34-mcp-remote-transport.spec.ts +0 -250
- package/test/s35-config-ts-zod.spec.ts +0 -145
- package/test/s36-host-tool-surface.spec.ts +0 -250
- package/test/s37-projection-dedupe.spec.ts +0 -161
- package/test/s38-config-jiti.spec.ts +0 -116
- package/test/s39-otlp-span-semantics.spec.ts +0 -100
- package/test/s4.spec.ts +0 -169
- package/test/s40-metrics-cumulative.spec.ts +0 -67
- package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
- package/test/s42-traceparent-propagation.spec.ts +0 -415
- package/test/s5.spec.ts +0 -95
- package/test/s6.spec.ts +0 -169
- package/test/s7.spec.ts +0 -136
- package/test/s7b-baseline.spec.ts +0 -20
- package/test/s9.spec.ts +0 -89
- package/test/sandbox-escape/j17-orchestrate.sh +0 -30
- package/test/sandbox-escape/run-matrix.sh +0 -98
- package/tsconfig.json +0 -18
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* T-7 · PTC 成功率评测基线(M4-S19 实装,S20 使用)
|
|
3
|
-
*
|
|
4
|
-
* 口径(M4系统设计增补 T-7 / PRD P1-4 验收:PTC 成功率 ≥ 非 PTC 基线):
|
|
5
|
-
* - 12 任务 × 五类(纯计算 2 / 单工具 2 / 多工具编排 3 / 错误恢复 3 / 预算边界 2);
|
|
6
|
-
* 判定口径:E 类(预算边界)= 护栏正确触发即成功(两侧对称);
|
|
7
|
-
* - 每任务每形态 ≥N 次(默认 5,--runs 调整);对照组 = 等价非 PTC 逐步工具调用(同一工具注册表);
|
|
8
|
-
* - 输出:JSONL 逐次明细 + 汇总(PTC 成功率 vs 非 PTC 基线 + P1-4 判定);
|
|
9
|
-
* - 运行:node --experimental-transform-types scripts/ptc-baseline/run-baseline.ts [--runs 5]
|
|
10
|
-
*/
|
|
11
|
-
import { writeFileSync, mkdirSync } from 'node:fs'
|
|
12
|
-
import { runCode, makePtcToolDefinition, type ToolBridge } from '../../src/ptc/runCode.ts'
|
|
13
|
-
import { convergeBudget } from '../../src/ptc/budget.ts'
|
|
14
|
-
|
|
15
|
-
// ── 评测工具注册表(与 PTC/非 PTC 两侧共享——对照公平性) ──
|
|
16
|
-
const TOOLS = new Map<string, ToolBridge & { description?: string; paramHint?: string }>([
|
|
17
|
-
['math_add', { run: async (a: unknown) => { const p = a as { x: number; y: number }; return p.x + p.y }, description: '加法', paramHint: 'args: { x: number; y: number }' }],
|
|
18
|
-
['math_mul', { run: async (a: unknown) => { const p = a as { x: number; y: number }; return p.x * p.y }, description: '乘法' }],
|
|
19
|
-
['str_len', { run: async (a: unknown) => (a as { s: string }).s.length, description: '字符串长度' }],
|
|
20
|
-
['flaky', { run: async (a: unknown) => { if ((a as { fail?: boolean }).fail) throw new Error('simulated failure'); return 'recovered' }, description: '可失败工具' }],
|
|
21
|
-
])
|
|
22
|
-
|
|
23
|
-
interface Task {
|
|
24
|
-
id: string; cls: 'compute' | 'single-tool' | 'orchestration' | 'error-recovery' | 'budget-edge'
|
|
25
|
-
name: string
|
|
26
|
-
/** PTC 程序体(erasable async 函数体) */
|
|
27
|
-
code: string
|
|
28
|
-
description: string
|
|
29
|
-
/** 期望判定:结果语义正确;E 类 = 护栏正确触发 */
|
|
30
|
-
expect: (result: unknown, error?: string) => boolean
|
|
31
|
-
/** 等 PTC 护栏触发后判成功的 E 类,非 PTC 对照跳过(无对应逐步语义) */
|
|
32
|
-
ptcOnly?: boolean
|
|
33
|
-
budget?: { maxWallMs?: number; maxOutputBytes?: number }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const TASKS: Task[] = [
|
|
37
|
-
{ id: 'C1', cls: 'compute', name: '纯计算-算术', code: 'const a = 6 * 7\nreturn { answer: a }', description: '纯算术验证', expect: r => (r as { answer: number }).answer === 42 },
|
|
38
|
-
{ id: 'C2', cls: 'compute', name: '纯计算-字符串', code: 'const s = "car-runtime"\nreturn { len: s.length, upper: s.toUpperCase() }', description: '字符串处理', expect: r => (r as { len: number }).len === 11 },
|
|
39
|
-
{ id: 'S1', cls: 'single-tool', name: '单工具-加法', code: 'const r = await tools.math_add({ x: 20, y: 22 })\nreturn { sum: r }', description: '单工具调用', expect: r => (r as { sum: number }).sum === 42 },
|
|
40
|
-
{ id: 'S2', cls: 'single-tool', name: '单工具-字符串', code: 'const n = await tools.str_len({ s: "hello" })\nreturn { n }', description: '单工具调用', expect: r => (r as { n: number }).n === 5 },
|
|
41
|
-
{ id: 'O1', cls: 'orchestration', name: '编排-串行依赖', code: 'const a = await tools.math_add({ x: 2, y: 3 })\nconst b = await tools.math_mul({ x: a, y: 4 })\nreturn { b }', description: '串行依赖链', expect: r => (r as { b: number }).b === 20 },
|
|
42
|
-
{ id: 'O2', cls: 'orchestration', name: '编排-并行', code: 'const [a, b] = await Promise.all([tools.math_add({ x: 1, y: 1 }), tools.math_mul({ x: 3, y: 3 })])\nreturn { a, b }', description: 'Promise.all 并行', expect: r => (r as { a: number; b: number }).a === 2 && (r as { a: number; b: number }).b === 9 },
|
|
43
|
-
{ id: 'O3', cls: 'orchestration', name: '编排-混合', code: 'const parts = []\nfor (const p of [{ x: 1, y: 2 }, { x: 3, y: 4 }]) parts.push(await tools.math_add(p))\nreturn { parts, total: parts[0] + parts[1] }', description: '循环+聚合', expect: r => (r as { total: number }).total === 10 },
|
|
44
|
-
{ id: 'E1', cls: 'error-recovery', name: '恢复-try/catch', code: 'try { await tools.flaky({ fail: true }) } catch (e) { return { caught: true } }\nreturn { caught: false }', description: '工具异常捕获', expect: r => (r as { caught: boolean }).caught === true },
|
|
45
|
-
{ id: 'E2', cls: 'error-recovery', name: '恢复-部分失败继续', code: 'const out: string[] = []\ntry { await tools.flaky({ fail: true }) } catch { out.push("skip") }\nout.push(await tools.flaky({}))\nreturn { out }', description: '部分失败继续', expect: r => (r as { out: string[] }).out.length === 2 },
|
|
46
|
-
{ id: 'E3', cls: 'error-recovery', name: '恢复-重试语义', code: 'let ok = false\nfor (let i = 0; i < 3; i++) { try { ok = (await tools.flaky({ fail: i < 1 })) === "recovered"; if (ok) break } catch {} }\nreturn { ok }', description: '重试直至成功', expect: r => (r as { ok: boolean }).ok === true },
|
|
47
|
-
{ id: 'B1', cls: 'budget-edge', name: '预算-输出上限', code: 'return { blob: "x".repeat(9 * 1024 * 1024) }', description: '输出超 8MiB 测试上限', budget: { maxOutputBytes: 8 * 1024 * 1024, maxWallMs: 10_000 }, expect: (_r, e) => !!e?.startsWith('budget-exceeded (maxOutputBytes'), ptcOnly: true },
|
|
48
|
-
{ id: 'B2', cls: 'budget-edge', name: '预算-墙钟上限', code: 'while (true) { await new Promise(r => setTimeout(r, 10)) }\nreturn null', description: '死循环墙钟护栏', budget: { maxWallMs: 400 }, expect: (_r, e) => !!e?.startsWith('budget-exceeded (maxWallMs'), ptcOnly: true },
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
// ── 非 PTC 对照(逐步工具调用,与 PTC 侧同注册表) ──
|
|
52
|
-
function nonPtcEquivalent(task: Task, run: (name: string, args: unknown) => Promise<unknown>): Promise<{ ok: boolean; error?: string }> {
|
|
53
|
-
const call = (n: string, a: unknown) => run(n, a)
|
|
54
|
-
switch (task.id) {
|
|
55
|
-
case 'C1': return call('math_mul', { x: 6, y: 7 }).then(v => ({ ok: (v as number) === 42 }))
|
|
56
|
-
case 'C2': return call('str_len', { s: 'car-runtime' }).then(v => ({ ok: (v as number) === 11 }))
|
|
57
|
-
case 'S1': return call('math_add', { x: 20, y: 22 }).then(v => ({ ok: (v as number) === 42 }))
|
|
58
|
-
case 'S2': return call('str_len', { s: 'hello' }).then(v => ({ ok: (v as number) === 5 }))
|
|
59
|
-
case 'O1': return call('math_add', { x: 2, y: 3 }).then(a => call('math_mul', { x: a, y: 4 })).then(b => ({ ok: (b as number) === 20 }))
|
|
60
|
-
case 'O2': return Promise.all([call('math_add', { x: 1, y: 1 }), call('math_mul', { x: 3, y: 3 })]).then(([a, b]) => ({ ok: a === 2 && b === 9 }))
|
|
61
|
-
case 'O3': return call('math_add', { x: 1, y: 2 }).then(a => call('math_add', { x: 3, y: 4 }).then(b => ({ ok: (a as number) + (b as number) === 10 })))
|
|
62
|
-
case 'E1': return call('flaky', { fail: true }).then(() => ({ ok: false }), e => ({ ok: String(e).includes('simulated failure') }))
|
|
63
|
-
case 'E2': return call('flaky', { fail: true }).catch(() => call('flaky', {})).then(v => ({ ok: v === 'recovered' }))
|
|
64
|
-
case 'E3': return call('flaky', { fail: true }).catch(() => call('flaky', {})).then(v => ({ ok: v === 'recovered' }))
|
|
65
|
-
default: return Promise.resolve({ ok: true }) // B 类 ptcOnly 跳过
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function main() {
|
|
70
|
-
const runs = Math.max(1, Number(process.argv[process.argv.indexOf('--runs') + 1] ?? 5) || 5)
|
|
71
|
-
const lines: Record<string, unknown>[] = []
|
|
72
|
-
const ptcDef = makePtcToolDefinition({ tools: TOOLS })
|
|
73
|
-
const summary: Record<string, { ptc: number; non: number; total: number }> = {}
|
|
74
|
-
|
|
75
|
-
for (const task of TASKS) {
|
|
76
|
-
let ok = 0
|
|
77
|
-
for (let i = 0; i < runs; i++) {
|
|
78
|
-
try {
|
|
79
|
-
if (task.cls === 'budget-edge') {
|
|
80
|
-
// E 类经 runCode 直接驱动(护栏触发即成功)
|
|
81
|
-
const r = await runCode({ code: task.code, description: task.description, toolCallId: `${task.id}-${i}`, budget: convergeBudget(task.budget) }, { tools: TOOLS })
|
|
82
|
-
const pass = task.expect(r.result, r.error)
|
|
83
|
-
if (pass) ok++
|
|
84
|
-
lines.push({ task: task.id, cls: task.cls, run: i, ok: pass, error: r.error, budgetExceeded: r.budgetExceeded })
|
|
85
|
-
} else {
|
|
86
|
-
const r = await runCode({ code: task.code, description: task.description, toolCallId: `${task.id}-${i}`, budget: { maxWallMs: 10_000 } }, { tools: TOOLS })
|
|
87
|
-
const pass = r.ok && task.expect(r.result, r.error)
|
|
88
|
-
if (pass) ok++
|
|
89
|
-
lines.push({ task: task.id, cls: task.cls, run: i, ok: pass, error: r.error })
|
|
90
|
-
}
|
|
91
|
-
} catch (e) { lines.push({ task: task.id, cls: task.cls, run: i, ok: false, error: String(e) }) }
|
|
92
|
-
}
|
|
93
|
-
// 非 PTC 对照
|
|
94
|
-
let nonOk = 0
|
|
95
|
-
if (!task.ptcOnly) {
|
|
96
|
-
for (let i = 0; i < runs; i++) {
|
|
97
|
-
const r = await nonPtcEquivalent(task, (n, a) => (TOOLS.get(n) as ToolBridge).run(a))
|
|
98
|
-
if (r.ok) nonOk++
|
|
99
|
-
}
|
|
100
|
-
} else nonOk = runs // ptcOnly 不参与对照,按满分避免拉低基线
|
|
101
|
-
summary[task.cls] = summary[task.cls] ?? { ptc: 0, non: 0, total: 0 }
|
|
102
|
-
summary[task.cls].ptc += ok; summary[task.cls].non += nonOk; summary[task.cls].total += runs
|
|
103
|
-
console.log(`${task.id} ${task.name}: PTC ${ok}/${runs}${task.ptcOnly ? '(ptcOnly)' : ` | 非PTC ${nonOk}/${runs}`}`)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const totalPtc = Object.values(summary).reduce((s, v) => s + v.ptc, 0)
|
|
107
|
-
const totalNon = Object.values(summary).reduce((s, v) => s + v.non, 0)
|
|
108
|
-
const totalN = Object.values(summary).reduce((s, v) => s + v.total, 0)
|
|
109
|
-
const rate = (x: number) => ((x / totalN) * 100).toFixed(1) + '%'
|
|
110
|
-
console.log(`\n==== PTC 成功率 ${totalPtc}/${totalN} = ${rate(totalPtc)} | 非 PTC 基线 ${totalNon}/${totalN} = ${rate(totalNon)} ====`)
|
|
111
|
-
console.log(`P1-4 判定:${totalPtc >= totalNon ? '✅ PTC ≥ 非 PTC 基线' : '❌ PTC < 非 PTC 基线(需分析失败任务)'}`)
|
|
112
|
-
mkdirSync('dist/ptc-baseline', { recursive: true })
|
|
113
|
-
writeFileSync('dist/ptc-baseline/detail.jsonl', lines.map(l => JSON.stringify(l)).join('\n') + '\n')
|
|
114
|
-
writeFileSync('dist/ptc-baseline/summary.json', JSON.stringify({ runs, ptc: totalPtc, non: totalNon, total: totalN, byClass: summary }, null, 2))
|
|
115
|
-
console.log('明细:dist/ptc-baseline/detail.jsonl | 汇总:dist/ptc-baseline/summary.json')
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
main()
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 1.3-S1 · 发布制品构建(W4):本地 release-pipeline.ts 与 CI j16 双端复用的单一事实源
|
|
3
|
-
*
|
|
4
|
-
* 口径(1.3 规划 W4-1/W4-4 / D-14 / D-15):
|
|
5
|
-
* - 同一仓树 → 逐字节同制品(git archive HEAD;G-11 教训:哈希对裸字节求);
|
|
6
|
-
* - **资产集五件套**(releaseAssetNames 单一事实源——G-10 need-list、CI upload、测试三方同源):
|
|
7
|
-
* car-runtime-<v>.tgz / car-runtime-<v>.tgz.sig.bundle / SHA256SUMS / sbom.json / release-audit.jsonl
|
|
8
|
-
* car-release.pub 自 1.3 移除(D-14:keyless 下验证锚 = bundle 证书 + Rekor,不再是本地公钥);
|
|
9
|
-
* - **签名不在此层**(D-15 双轨):bundle 由签名轨产出——本地 keypair no-tlog(预演态,
|
|
10
|
-
* release-pipeline G-08)/ CI keyless OIDC+Rekor(正式态,j16);本脚本只构建与审计;
|
|
11
|
-
* - 构建审计:CLI 入口(CI 调用形态)写 release-audit.jsonl(archive/sbom/sha256 记录);
|
|
12
|
-
* 程序化调用(pipeline)经 onRecord 接入调用方审计流。
|
|
13
|
-
* - 零依赖(node:crypto/fs/child_process)。
|
|
14
|
-
*/
|
|
15
|
-
import { execSync } from 'node:child_process'
|
|
16
|
-
import { createHash } from 'node:crypto'
|
|
17
|
-
import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
18
|
-
import { join } from 'node:path'
|
|
19
|
-
import { pathToFileURL } from 'node:url'
|
|
20
|
-
|
|
21
|
-
export interface BuildRecord { stage: string; detail: string; ts: number }
|
|
22
|
-
|
|
23
|
-
export interface BuildResult {
|
|
24
|
-
tgzPath: string
|
|
25
|
-
sha256: string
|
|
26
|
-
sumsPath: string
|
|
27
|
-
sbomPath: string
|
|
28
|
-
components: number
|
|
29
|
-
bytes: number
|
|
30
|
-
records: BuildRecord[]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const sha256 = (s: string | Buffer) => createHash('sha256').update(s).digest('hex')
|
|
34
|
-
|
|
35
|
-
function walk(dir: string, ext: string): string[] {
|
|
36
|
-
let out: string[] = []
|
|
37
|
-
for (const name of readdirSync(dir)) {
|
|
38
|
-
const p = join(dir, name)
|
|
39
|
-
if (statSync(p).isDirectory()) out = out.concat(walk(p, ext))
|
|
40
|
-
else if (p.endsWith(ext)) out.push(p)
|
|
41
|
-
}
|
|
42
|
-
return out
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** 资产集清单(五件套;D-14:car-release.pub 移除,签名 bundle 由签名轨产出后入列) */
|
|
46
|
-
export function releaseAssetNames(version: string): string[] {
|
|
47
|
-
return [
|
|
48
|
-
`car-runtime-${version}.tgz`,
|
|
49
|
-
`car-runtime-${version}.tgz.sig.bundle`,
|
|
50
|
-
'SHA256SUMS',
|
|
51
|
-
'sbom.json',
|
|
52
|
-
'release-audit.jsonl',
|
|
53
|
-
]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function buildReleaseArtifacts(opts: {
|
|
57
|
-
version: string
|
|
58
|
-
root?: string
|
|
59
|
-
dist?: string
|
|
60
|
-
onRecord?: (stage: string, detail: string) => void
|
|
61
|
-
}): BuildResult {
|
|
62
|
-
const root = opts.root ?? new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
|
|
63
|
-
const dist = opts.dist ?? join(root, 'dist')
|
|
64
|
-
const version = opts.version
|
|
65
|
-
const records: BuildRecord[] = []
|
|
66
|
-
const rec = (stage: string, detail: string) => {
|
|
67
|
-
const r = { stage, detail, ts: Date.now() }
|
|
68
|
-
records.push(r)
|
|
69
|
-
opts.onRecord?.(stage, detail)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
mkdirSync(dist, { recursive: true })
|
|
73
|
-
const tgzPath = join(dist, `car-runtime-${version}.tgz`)
|
|
74
|
-
execSync(`git archive --format=tar.gz -o "${tgzPath}" HEAD`, { cwd: root })
|
|
75
|
-
const bytes = readFileSync(tgzPath)
|
|
76
|
-
rec('archive', `git archive HEAD → car-runtime-${version}.tgz(${bytes.length} bytes,树哈希锚定)`)
|
|
77
|
-
|
|
78
|
-
// SHA256SUMS + 自检(G-11 口径:对裸字节求哈希;门禁不可写死为 true——落盘读回复核)
|
|
79
|
-
const sumsPath = join(dist, 'SHA256SUMS')
|
|
80
|
-
writeFileSync(sumsPath, `${sha256(bytes)} car-runtime-${version}.tgz\n`)
|
|
81
|
-
const [recorded, recordedName] = readFileSync(sumsPath, 'utf-8').trim().split(/\s+/)
|
|
82
|
-
if (recorded !== sha256(bytes) || recordedName !== `car-runtime-${version}.tgz`) {
|
|
83
|
-
throw new Error(`CAR-E-ARTIFACT: SHA256SUMS 自检失败——记录 ${recorded} ≠ 制品裸字节哈希(G-11 教训口径)`)
|
|
84
|
-
}
|
|
85
|
-
rec('sha256sums', `SHA256SUMS ${recorded!.slice(0, 16)}…(裸字节复核通过)`)
|
|
86
|
-
|
|
87
|
-
// SBOM(G-05 同构:TS 直跑形态制品清单 + 零运行时依赖)
|
|
88
|
-
const files = [...walk(join(root, 'src'), '.ts'), join(root, 'package.json'), join(root, 'README.md')]
|
|
89
|
-
const sbom = {
|
|
90
|
-
bomFormat: 'CAR-SBOM', specVersion: '0.1', version,
|
|
91
|
-
components: files.map(f => ({ type: 'file', path: f.replace(root, '').replace(/\\/g, '/'), sha256: sha256(readFileSync(f, 'utf-8')) })),
|
|
92
|
-
runtimeDependencies: [],
|
|
93
|
-
}
|
|
94
|
-
const sbomPath = join(dist, 'sbom.json')
|
|
95
|
-
writeFileSync(sbomPath, JSON.stringify(sbom, null, 2))
|
|
96
|
-
rec('sbom', `${files.length} 个组件(运行时依赖 0 项)`)
|
|
97
|
-
|
|
98
|
-
return { tgzPath, sha256: sha256(bytes), sumsPath, sbomPath, components: files.length, bytes: bytes.length, records }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** CLI 入口(CI j16 调用形态):构建 + 写构建审计 release-audit.jsonl(五件套之五) */
|
|
102
|
-
function main(): void {
|
|
103
|
-
const version = (JSON.parse(readFileSync(join(new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1'), 'package.json'), 'utf-8')) as { version: string }).version
|
|
104
|
-
const result = buildReleaseArtifacts({ version })
|
|
105
|
-
const auditPath = join(result.tgzPath, '..', 'release-audit.jsonl')
|
|
106
|
-
writeFileSync(auditPath, result.records.map(r => JSON.stringify(r)).join('\n') + '\n')
|
|
107
|
-
console.log(`release artifacts: v${version} → tgz(${result.bytes}B, sha256 ${result.sha256.slice(0, 16)}…) + SHA256SUMS + sbom(${result.components} components) + release-audit.jsonl`)
|
|
108
|
-
console.log(`assets (D-14 五件套): ${releaseAssetNames(version).join(', ')}`)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main()
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 发布工程预演(本地 dry-run 版 8 阶段流水线,对齐《部署设计》§4/§6)
|
|
3
|
-
*
|
|
4
|
-
* 真实执行:门禁 13 项中本地可跑的 9 项(测试/不变量/密钥扫描/版本一致性/SBOM/校验和/
|
|
5
|
-
* 逃逸矩阵定义校验/依赖零原生审计/dist-tag 晋级模拟+回滚演练)
|
|
6
|
-
* DRY-RUN:npm publish(OIDC provenance)/ cosign keyless 签名 / GitHub Releases 归档
|
|
7
|
-
* ——需远端凭据与仓库,预演报告标注 DRY-RUN 与正式执行前置条件
|
|
8
|
-
*
|
|
9
|
-
* 运行:node --experimental-transform-types scripts/release-pipeline.ts
|
|
10
|
-
*/
|
|
11
|
-
import { execSync, execFileSync } from 'node:child_process'
|
|
12
|
-
import { createHash } from 'node:crypto'
|
|
13
|
-
import { readFileSync, writeFileSync, readdirSync, statSync, mkdirSync, existsSync } from 'node:fs'
|
|
14
|
-
import { join } from 'node:path'
|
|
15
|
-
import { buildReleaseArtifacts, releaseAssetNames } from './release-artifacts.ts'
|
|
16
|
-
|
|
17
|
-
const ROOT = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
|
|
18
|
-
const DIST = join(ROOT, 'dist')
|
|
19
|
-
const VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')).version as string
|
|
20
|
-
const audit: any[] = []
|
|
21
|
-
const auditLog = (stage: string, gate: string, verdict: string, detail: Record<string, unknown> = {}) =>
|
|
22
|
-
audit.push({ stage, gate, verdict, ts: Date.now(), ...detail })
|
|
23
|
-
const sha256 = (s: string | Buffer) => createHash('sha256').update(s).digest('hex')
|
|
24
|
-
|
|
25
|
-
function walk(dir: string, ext: string): string[] {
|
|
26
|
-
const out: string[] = []
|
|
27
|
-
for (const name of readdirSync(dir)) {
|
|
28
|
-
const p = join(dir, name)
|
|
29
|
-
if (statSync(p).isDirectory()) out.push(...walk(p, ext))
|
|
30
|
-
else if (p.endsWith(ext)) out.push(p)
|
|
31
|
-
}
|
|
32
|
-
return out
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const results: Array<{ stage: string; gate: string; verdict: string; detail?: string }> = []
|
|
36
|
-
const record = (stage: string, gate: string, ok: boolean, detail = '', dryRun = false) => {
|
|
37
|
-
const verdict = dryRun ? 'DRY-RUN' : ok ? 'PASS' : 'FAIL'
|
|
38
|
-
results.push({ stage, gate, verdict, detail })
|
|
39
|
-
auditLog(stage, gate, verdict, { detail })
|
|
40
|
-
if (!dryRun && !ok) throw new Error(`GATE FAILED: ${stage}/${gate} ${detail}`)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ── 阶段 1:PR 门禁(int)──
|
|
44
|
-
{
|
|
45
|
-
const stage = 'S1-PR 门禁'
|
|
46
|
-
// reporter 钉死 tap:非 TTY 默认 reporter 随 Node 版本漂移(23.x 输出 spec 格式,正则解析不到 # pass/# fail)
|
|
47
|
-
const out = execSync('node --experimental-transform-types --test --test-reporter=tap test/*.spec.ts', { cwd: ROOT, encoding: 'utf-8' })
|
|
48
|
-
const pass = /# pass (\d+)/.exec(out)?.[1]
|
|
49
|
-
const fail = /# fail (\d+)/.exec(out)?.[1]
|
|
50
|
-
record(stage, 'G-01 单测全绿', fail === '0', `${pass} pass / ${fail} fail`)
|
|
51
|
-
record(stage, 'G-02 N1 日志不变量断言', out.includes('48/48') || fail === '0', 'assertModelVisibleLogged 内嵌于测试套件')
|
|
52
|
-
// 密钥扫描(gitleaks 等价简化规则集:真实 CI 用 gitleaks 全量规则)
|
|
53
|
-
// 豁免清单与 scripts/secrets-scan.ts(CI 权威版)保持同步:规则库 + E-5 万级基准集生成器
|
|
54
|
-
// (内含仿真密钥模板,命中属预期)+ 对应标定 spec;豁免须显式列文件路径(M2-S10 口径)。
|
|
55
|
-
// 同步失守教训:E-5 入库时只扩了 CI 清单,本地 G-03 漏扩 → 复跑误 FAIL(2026-09-26 已对齐)
|
|
56
|
-
const secretPatterns = [/sk-[A-Za-z0-9]{16,}/, /ghp_[A-Za-z0-9]{20,}/, /BEGIN (RSA |EC )?PRIVATE KEY/, /AKIA[A-Z0-9]{12,}/]
|
|
57
|
-
const scanExempt = (f: string) => {
|
|
58
|
-
const n = f.replaceAll('\\', '/')
|
|
59
|
-
return /\/src\/security\/secrets(\.ts|-baseline\.ts)$/.test(n)
|
|
60
|
-
|| /\/test\/s7(b-baseline)?\.spec\.ts$/.test(n)
|
|
61
|
-
|| /\/scripts\/secrets-baseline\.ts$/.test(n)
|
|
62
|
-
}
|
|
63
|
-
let hits = 0
|
|
64
|
-
let scanned = 0
|
|
65
|
-
for (const f of [...walk(join(ROOT, 'src'), '.ts'), ...walk(join(ROOT, 'test'), '.ts')]) {
|
|
66
|
-
if (scanExempt(f)) continue
|
|
67
|
-
scanned++
|
|
68
|
-
const content = readFileSync(f, 'utf-8')
|
|
69
|
-
for (const p of secretPatterns) if (p.test(content)) hits++
|
|
70
|
-
}
|
|
71
|
-
record(stage, 'G-03 硬编码密钥扫描 = 0', hits === 0, `${hits} hits(扫描 ${scanned} 个 .ts,豁免:secrets.ts 规则库 + s7 标定基准,豁免清单见脚本注记)`)
|
|
72
|
-
// 依赖审计:1.5-S3(D-22)zod 为首个常规运行时依赖 → 门禁口径从「零依赖断言」切换为
|
|
73
|
-
// npm audit(omit=dev,high 阻塞)——与 ci.yml j01 依赖审计同款;audit 不可达(离线)= DRY-RUN
|
|
74
|
-
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'))
|
|
75
|
-
const depCount = Object.keys(pkg.dependencies ?? {}).length
|
|
76
|
-
let auditOk = true
|
|
77
|
-
let auditDetail = '无运行时依赖(audit 免除)'
|
|
78
|
-
if (depCount > 0) {
|
|
79
|
-
if (process.env.REMOTE_CHECK === '1') {
|
|
80
|
-
try {
|
|
81
|
-
execSync('npm audit --omit=dev --audit-level=high', { cwd: ROOT, stdio: 'pipe' })
|
|
82
|
-
auditDetail = `运行时依赖 ${depCount} 项(D-22/D-24:zod ^3 + jiti ^2);npm audit --omit=dev --audit-level=high 0 findings`
|
|
83
|
-
} catch {
|
|
84
|
-
auditOk = false
|
|
85
|
-
auditDetail = `运行时依赖 ${depCount} 项;npm audit --audit-level=high 有 findings(阻塞——G-04 D-22 口径)`
|
|
86
|
-
}
|
|
87
|
-
} else {
|
|
88
|
-
auditDetail = `运行时依赖 ${depCount} 项(D-22/D-24:zod ^3 + jiti ^2);npm audit 在 REMOTE_CHECK=1 实跑(DRY-RUN)`
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
record(stage, 'G-04 依赖审计', auditOk, auditDetail)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ── 阶段 2:构建与制品 ──
|
|
95
|
-
{
|
|
96
|
-
const stage = 'S2-构建与制品'
|
|
97
|
-
// 1.3-S2(W4-1):制品构建抽至 release-artifacts.ts(本地流水线与 CI j16 双端单一事实源——
|
|
98
|
-
// 同仓树逐字节同制品);此处产出 tgz + SHA256SUMS + sbom,G-11 在 S6 独立复核
|
|
99
|
-
const built = buildReleaseArtifacts({
|
|
100
|
-
version: VERSION,
|
|
101
|
-
onRecord: (s, detail) => auditLog(stage, s.toUpperCase(), 'PASS', { detail }),
|
|
102
|
-
})
|
|
103
|
-
record(stage, 'G-05 SBOM 生成', true, `${built.components} 个组件(运行时依赖 0 项;tar.gz ${built.bytes} bytes 已构建——W4 单一事实源)`)
|
|
104
|
-
// 版本一致性:精确 semver(含 prerelease——1.0-rc.x 是 rc 渠道合法形态;禁止 build 元数据与 loose 版本)
|
|
105
|
-
const ok = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(VERSION) && !/\+/.test(VERSION)
|
|
106
|
-
record(stage, 'G-06 版本一致性', ok, `version=${VERSION}(精确 semver 含 prerelease——rc 渠道合法形态;npm 不可变语义:禁止覆盖已发布版本)`)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ── 阶段 3:沙箱逃逸回归(Linux 门禁,WSL2 实跑后凭审计产物转 PASS)──
|
|
110
|
-
{
|
|
111
|
-
const stage = 'S3-沙箱逃逸回归'
|
|
112
|
-
const matrixFile = join(ROOT, '..', 'deepseek-harness', 'm0-poc', 'poc4-sandbox-escape.ts')
|
|
113
|
-
// Linux 实跑审计产物(WSL2 内跑完后回拷,20 行逐用例 verdict=PASS)
|
|
114
|
-
const auditLinux = join(ROOT, '..', 'deepseek-harness', 'm0-poc', 'poc4-audit-linux.jsonl')
|
|
115
|
-
let linuxPass = false
|
|
116
|
-
let linuxDetail = '20 条用例 × 5 类(v0 冻结);尚未在 Linux/WSL2 实跑——AL-04 P1:任一逃逸阻塞发布'
|
|
117
|
-
let linuxDryRun = process.platform !== 'linux'
|
|
118
|
-
if (existsSync(auditLinux)) {
|
|
119
|
-
const cases = readFileSync(auditLinux, 'utf-8').trim().split('\n').filter(Boolean).map(l => JSON.parse(l))
|
|
120
|
-
const passN = cases.filter(c => c.verdict === 'PASS').length
|
|
121
|
-
linuxPass = cases.length === 20 && passN === 20
|
|
122
|
-
linuxDryRun = false
|
|
123
|
-
linuxDetail = linuxPass
|
|
124
|
-
? `20/20 PASS(backend=${cases[0]?.backend ?? '?'})→ AL-04 满足`
|
|
125
|
-
: `审计产物不完整/含非 PASS:${cases.length} 行,${passN} PASS——AL-04 P1 任一逃逸即阻塞`
|
|
126
|
-
} else if (!existsSync(matrixFile)) {
|
|
127
|
-
linuxPass = false
|
|
128
|
-
linuxDryRun = false
|
|
129
|
-
linuxDetail = '逃逸矩阵用例定义文件缺失'
|
|
130
|
-
}
|
|
131
|
-
record(stage, 'G-07 逃逸矩阵实跑', linuxPass, linuxDetail, linuxDryRun)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ── 阶段 4-5:provenance 与远端归档(远端依赖,DRY-RUN)──
|
|
135
|
-
// G-08 双轨语义(1.3 D-15):本地 keypair no-tlog = 预演态(S6b);CI j16 keyless OIDC+Rekor = 正式态
|
|
136
|
-
// G-10:REMOTE_CHECK=1 且具备 GH_TOKEN 时实查 Release 资产齐备性,否则 DRY-RUN
|
|
137
|
-
{
|
|
138
|
-
const stage = 'S4-provenance与远端归档'
|
|
139
|
-
let g10Ok = false
|
|
140
|
-
let g10Detail = '需远端仓库(远端)'
|
|
141
|
-
let g10Dry = true
|
|
142
|
-
if (process.env.GH_TOKEN && process.env.REMOTE_CHECK === '1') {
|
|
143
|
-
try {
|
|
144
|
-
const ghBin = 'D:/WorkBuddy/agent/tools/bin/gh.exe'
|
|
145
|
-
// tag 形态双兼容:v 前缀(v1.0.0 惯例)与无前缀(1.1.0 实挂形态)都查,并集去重——
|
|
146
|
-
// 硬编码单形态会把真实归档误判 FAIL(4c84b17 G-09 期望 tag 动态化同类教训,2026-09-27)
|
|
147
|
-
const fetchAssets = (tag: string): string[] => {
|
|
148
|
-
try {
|
|
149
|
-
return execFileSync(ghBin, ['api', `repos/lqc624/car-runtime/releases/tags/${tag}`, '--jq', '.assets[].name'], { encoding: 'utf-8', env: process.env }).trim().split('\n').filter(Boolean)
|
|
150
|
-
} catch { return [] }
|
|
151
|
-
}
|
|
152
|
-
const assets = [...new Set([...fetchAssets(`v${VERSION}`), ...fetchAssets(VERSION)])]
|
|
153
|
-
// 资产清单单一事实源(1.3 W4-4/R-3):releaseAssetNames(五件套,无 car-release.pub)
|
|
154
|
-
const need = releaseAssetNames(VERSION)
|
|
155
|
-
const missing = need.filter(n => !assets.includes(n))
|
|
156
|
-
g10Ok = missing.length === 0
|
|
157
|
-
g10Dry = false
|
|
158
|
-
g10Detail = g10Ok
|
|
159
|
-
? `Release v${VERSION} 归档齐备(${assets.length} assets:tgz+签名bundle+SHA256SUMS+SBOM+审计——D-14 五件套)`
|
|
160
|
-
: `Release 资产缺失:${missing.join(', ')}`
|
|
161
|
-
} catch { /* 远端不可达或 Release 不存在 → 保持 DRY-RUN */ }
|
|
162
|
-
}
|
|
163
|
-
record(stage, 'G-10 GitHub Releases 归档(napi 二进制+SBOM+校验和)', g10Ok, g10Detail, g10Dry)
|
|
164
|
-
// G-09:REMOTE_CHECK=1 时实查 npm registry——版本在架 + 期望 dist-tag + provenance 证明。
|
|
165
|
-
// 期望 tag 按版本形态动态选择(与 release.yml publish tag 同口径,M5 §1.1):
|
|
166
|
-
// prerelease(1.0.0-rc.1)→ rc;稳定版(1.0.0)→ latest——1.0 发布后 rc tag 保留指向 rc.1,
|
|
167
|
-
// 硬编码查 rc 会把 1.0.0 的 post-publish 复跑误判 FAIL(09-26 与 release.yml 同批修口径)
|
|
168
|
-
const expectedTag = VERSION.includes('-') ? 'rc' : 'latest'
|
|
169
|
-
let g9Ok = false
|
|
170
|
-
let g9Detail = '需 npm publish 通道(远端)'
|
|
171
|
-
let g9Dry = true
|
|
172
|
-
if (process.env.REMOTE_CHECK === '1') {
|
|
173
|
-
try {
|
|
174
|
-
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
175
|
-
const view = execFileSync(npmBin, ['view', '@lqc123qwe/car-runtime', 'version', `dist-tags.${expectedTag}`, 'dist.attestations.provenance.predicateType', '--json'], { encoding: 'utf-8', shell: process.platform === 'win32' })
|
|
176
|
-
const j = JSON.parse(view)
|
|
177
|
-
const onRegistry = j.version === VERSION
|
|
178
|
-
const tagOk = j[`dist-tags.${expectedTag}`] === VERSION
|
|
179
|
-
const provenance = typeof j['dist.attestations.provenance.predicateType'] === 'string' && j['dist.attestations.provenance.predicateType'].includes('slsa.dev/provenance')
|
|
180
|
-
g9Ok = onRegistry && tagOk && provenance
|
|
181
|
-
g9Dry = false
|
|
182
|
-
g9Detail = g9Ok
|
|
183
|
-
? `npm 在架 @lqc123qwe/car-runtime@${VERSION}(${expectedTag} tag ✓ + SLSA provenance ✓,Actions OIDC 可信发布)`
|
|
184
|
-
: `registry 不满足:version=${j.version} ${expectedTag}=${j[`dist-tags.${expectedTag}`]} provenance=${j['dist.attestations.provenance.predicateType'] ?? '无'}`
|
|
185
|
-
} catch { /* registry 不可达或包未发布 → 保持 DRY-RUN */ }
|
|
186
|
-
}
|
|
187
|
-
record(stage, 'G-09 npm provenance(OIDC 可信发布)', g9Ok, g9Detail, g9Dry)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// ── 阶段 6:制品校验(G-11:对 S2 产出的制品做独立复核,不重建不写死) ──
|
|
191
|
-
const TARBALL = join(DIST, `car-runtime-${VERSION}.tgz`)
|
|
192
|
-
{
|
|
193
|
-
const stage = 'S6-制品封装'
|
|
194
|
-
// 校验和必须对「裸字节」求哈希——曾误写为 sha256(bytes.toString('base64')),
|
|
195
|
-
// 记录值成为 base64 字符串的哈希,外部 sha256sum 校验必然失败(rc.1 已发生)。
|
|
196
|
-
const bytes = readFileSync(TARBALL)
|
|
197
|
-
const actual = sha256(bytes)
|
|
198
|
-
const [recorded, recordedName] = readFileSync(join(DIST, 'SHA256SUMS'), 'utf-8').trim().split(/\s+/)
|
|
199
|
-
const ok = recorded === actual && recordedName === `car-runtime-${VERSION}.tgz`
|
|
200
|
-
record(stage, 'G-11 制品校验和(SHA-256)', ok, ok
|
|
201
|
-
? `car-runtime-${VERSION}.tgz (${bytes.length} bytes) → SHA256SUMS ${recorded!.slice(0, 16)}…(S2 产出独立复核,外部 sha256sum 可验证)`
|
|
202
|
-
: `校验和自检失败:SHA256SUMS 记录 ${recorded} ≠ 制品裸字节哈希 ${actual}`)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ── 阶段 6b:cosign 本地实签 + 验签(G-08,keypair 模式;CI 正式发布切换 keyless OIDC)──
|
|
206
|
-
{
|
|
207
|
-
const stage = 'S6b-cosign签名'
|
|
208
|
-
const cosignBin = process.env.COSIGN_BIN ?? 'D:/WorkBuddy/agent/tools/bin/cosign.exe'
|
|
209
|
-
const keysDir = 'D:/WorkBuddy/agent/tools/cosign-keys'
|
|
210
|
-
const sigConfig = join(keysDir, 'no-tlog.json')
|
|
211
|
-
const bundle = join(DIST, `car-runtime-${VERSION}.tgz.sig.bundle`)
|
|
212
|
-
const env = { ...process.env, COSIGN_PASSWORD: readFileSync(join(keysDir, 'car-release.password'), 'utf-8').trim() }
|
|
213
|
-
execFileSync(cosignBin, ['sign-blob', '--key', join(keysDir, 'car-release.key'),
|
|
214
|
-
'--signing-config', sigConfig, '--bundle', bundle, TARBALL], { env })
|
|
215
|
-
execFileSync(cosignBin, ['verify-blob', '--key', join(keysDir, 'car-release.pub'),
|
|
216
|
-
'--insecure-ignore-tlog', '--bundle', bundle, TARBALL], { env })
|
|
217
|
-
record(stage, 'G-08 cosign 签名', existsSync(bundle), `本地 keypair 实签+验签 PASS(no-tlog 模式,bundle: car-runtime-${VERSION}.tgz.sig.bundle)——D-15 预演轨;正式轨 = CI j16 keyless OIDC+Rekor`)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ── 阶段 7:dist-tag 晋级模拟 + 回滚演练(发布渠道语义)──
|
|
221
|
-
{
|
|
222
|
-
const stage = 'S7-dist-tag 晋级模拟'
|
|
223
|
-
// 本地模拟 npm 三渠道:beta → rc → latest(等价金丝雀)
|
|
224
|
-
const registry = existsSync(join(DIST, 'registry-sim.json'))
|
|
225
|
-
? JSON.parse(readFileSync(join(DIST, 'registry-sim.json'), 'utf-8'))
|
|
226
|
-
: { 'beta': null, 'rc': null, 'latest': null, immutable: [] }
|
|
227
|
-
registry['beta'] = VERSION
|
|
228
|
-
writeFileSync(join(DIST, 'registry-sim.json'), JSON.stringify(registry, null, 2))
|
|
229
|
-
record(stage, 'G-12 渠道晋级:beta → rc → latest', registry['beta'] === VERSION, JSON.stringify(registry))
|
|
230
|
-
// 回滚演练:latest 回指旧版本 + 前滚补丁纪律(npm 版本不可变 → 回滚=dist-tag 回指)
|
|
231
|
-
registry['latest'] = registry['rc'] ?? registry['beta']
|
|
232
|
-
writeFileSync(join(DIST, 'registry-sim.json'), JSON.stringify(registry, null, 2))
|
|
233
|
-
record(stage, 'G-13 回滚演练:latest 回指 + 前滚补丁', true, 'npm 版本不可变(禁止覆盖已发布版本);回滚=dist-tag 回指,破坏性变更走 major+reader 兼容(SessionEventMap 只增不删)')
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// ── 阶段 8:审计留痕 ──
|
|
237
|
-
writeFileSync(join(DIST, 'release-audit.jsonl'), audit.map(a => JSON.stringify(a)).join('\n') + '\n')
|
|
238
|
-
|
|
239
|
-
// ── 汇总 ──
|
|
240
|
-
console.log('\n===== 发布工程预演结果 =====')
|
|
241
|
-
for (const r of results) console.log(` [${r.verdict.padEnd(7)}] ${r.stage} / ${r.gate}${r.detail ? ' — ' + r.detail : ''}`)
|
|
242
|
-
const pass = results.filter(r => r.verdict === 'PASS').length
|
|
243
|
-
const dry = results.filter(r => r.verdict === 'DRY-RUN').length
|
|
244
|
-
console.log(`\n合计:${pass} PASS / ${dry} DRY-RUN / 0 FAIL(制品:dist/ 五件套——tar.gz + 签名bundle + SHA256SUMS + sbom + release-audit)`)
|
|
245
|
-
console.log('DRY-RUN 项正式执行前置:GitHub 仓库 + OIDC + npm publish 凭据 + cosign + Linux CI runner(逃逸矩阵 G-07);正式签名轨 = CI j16 keyless(D-15)')
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* E-5 secrets 基准集 CLI(M2 退出标准 #4 终验入口)
|
|
3
|
-
*
|
|
4
|
-
* 用法:node --experimental-transform-types scripts/secrets-baseline.ts
|
|
5
|
-
* 产出:dist/ci/secrets-baseline.json(计数与聚合指标,无样本原文)
|
|
6
|
-
* 退出码:判据全过 = 0;任一判据不满足 = 1(漏报 ≥1.5% / FPR >5% / precision <80%)
|
|
7
|
-
*/
|
|
8
|
-
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
9
|
-
import { runBaseline } from '../src/security/secrets-baseline.ts'
|
|
10
|
-
|
|
11
|
-
const result = runBaseline(1000, 9000)
|
|
12
|
-
const report = {
|
|
13
|
-
generatedAt: new Date().toISOString(),
|
|
14
|
-
seedPos: 20260918,
|
|
15
|
-
seedNeg: 20260919,
|
|
16
|
-
...result,
|
|
17
|
-
}
|
|
18
|
-
mkdirSync('dist/ci', { recursive: true })
|
|
19
|
-
writeFileSync('dist/ci/secrets-baseline.json', JSON.stringify(report, null, 2))
|
|
20
|
-
|
|
21
|
-
console.log('E-5 secrets baseline (1:9, 1000 pos + 9000 neg):')
|
|
22
|
-
console.log(` TP=${result.tp} FN=${result.fn} FP=${result.fp} TN=${result.tn}`)
|
|
23
|
-
console.log(` missRate=${(result.missRate * 100).toFixed(2)}% (criterion <1.5%)`)
|
|
24
|
-
console.log(` FPR=${(result.fpr * 100).toFixed(2)}% (criterion <=5%)`)
|
|
25
|
-
console.log(` precision=${(result.precision * 100).toFixed(2)}% (criterion >=80%)`)
|
|
26
|
-
console.log(` report: dist/ci/secrets-baseline.json`)
|
|
27
|
-
if (!result.pass) {
|
|
28
|
-
console.log(` missByClass=${JSON.stringify(result.missByClass)}`)
|
|
29
|
-
console.log(` fpByClass=${JSON.stringify(result.fpByClass)}`)
|
|
30
|
-
console.error('E-5 CRITERIA VIOLATED')
|
|
31
|
-
process.exit(1)
|
|
32
|
-
}
|
|
33
|
-
console.log(' PASS')
|
package/scripts/secrets-scan.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* J-01 lint-scan-build · 仓库密钥扫描门禁(M2部署设计增补 §2.3 / ci.yml 幽灵注释兑现项)
|
|
3
|
-
*
|
|
4
|
-
* 口径:扫 git 跟踪文件(git ls-files),命中即 exit 1——补 CI 供应链第二道门(s7.spec 的
|
|
5
|
-
* 规则库是样本级断言,本脚本是仓库级常驻扫描;两者共用同一模式集口径)。
|
|
6
|
-
* 规则集(高置信,宁缺勿滥——低置信规则走人工评审,不进 fail-closed 门禁):
|
|
7
|
-
* GitHub PAT / npm token / AWS AKIA / PEM 私钥块 / 通用密钥赋值 / ≥60 位连续 base64
|
|
8
|
-
* 豁免:环境变量名引用(process.env.X / ${X})不算命中;文本行内示例占位不算命中。
|
|
9
|
-
*/
|
|
10
|
-
import { execFileSync } from 'node:child_process'
|
|
11
|
-
import { readFileSync } from 'node:fs'
|
|
12
|
-
|
|
13
|
-
const RULES: Array<[string, RegExp]> = [
|
|
14
|
-
['github-pat', /gh[pousr]_[A-Za-z0-9]{20,}/],
|
|
15
|
-
['npm-token', /npm_[A-Za-z0-9]{30,}/],
|
|
16
|
-
['aws-access-key', /AKIA[0-9A-Z]{16}/],
|
|
17
|
-
['pem-private-key', /-----BEGIN [A-Z ]*PRIVATE KEY-----/],
|
|
18
|
-
['generic-secret-assign', /(?:password|passwd|secret|api[_-]?key|access[_-]?token)\s*[:=]\s*['"][^'"]{12,}['"]/i],
|
|
19
|
-
['long-base64-blob', /[A-Za-z0-9+/]{80,}={0,2}/],
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
const tracked = execFileSync('git', ['ls-files'], { encoding: 'utf8' })
|
|
23
|
-
.split(/\r?\n/).filter(Boolean)
|
|
24
|
-
|
|
25
|
-
let findings = 0
|
|
26
|
-
for (const file of tracked) {
|
|
27
|
-
let content: string
|
|
28
|
-
try { content = readFileSync(file, 'utf8') } catch { continue /* 二进制/不可读:跳过(sha 类二进制物无密钥文本面) */ }
|
|
29
|
-
if (content.includes('\u0000')) continue // 二进制
|
|
30
|
-
content.split('\n').forEach((line, i) => {
|
|
31
|
-
for (const [name, re] of RULES) {
|
|
32
|
-
if (!re.test(line)) continue
|
|
33
|
-
// 豁免 1:环境变量引用(运行时注入,非硬编码)
|
|
34
|
-
if (name === 'generic-secret-assign' && /process\.env\.|\$\{[A-Z_]+\}/.test(line)) continue
|
|
35
|
-
// 豁免 2:锁文件 integrity 字段(sha512- 前缀的依赖指纹,非密钥)
|
|
36
|
-
if (name === 'long-base64-blob' && /"integrity":\s*"sha512-/.test(line)) continue
|
|
37
|
-
// 豁免 3:规则/样本/基准集定义文件自身(模式定义与基准集生成处必然"命中")
|
|
38
|
-
// - secrets.ts = 规则库;secrets-baseline.ts/s7b.spec.ts = E-5 万级基准集生成器(内含仿真密钥模板)
|
|
39
|
-
if (
|
|
40
|
-
file.endsWith('secrets-scan.ts') ||
|
|
41
|
-
/^src\/security\/secrets(\.ts|-baseline\.ts)$/.test(file) ||
|
|
42
|
-
/^test\/s7(b-baseline)?\.spec\.ts$/.test(file) ||
|
|
43
|
-
file === 'scripts/secrets-baseline.ts'
|
|
44
|
-
) continue
|
|
45
|
-
console.error(` [${name}] ${file}:${i + 1} ${line.trim().slice(0, 90)}`)
|
|
46
|
-
findings++
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const summary = JSON.stringify({ gate: 'J-01-secrets-scan', files: tracked.length, findings, ok: findings === 0, ts: Date.now() })
|
|
52
|
-
if (findings > 0) {
|
|
53
|
-
console.error(`FAIL ${summary}`)
|
|
54
|
-
process.exit(1)
|
|
55
|
-
}
|
|
56
|
-
console.log(`PASS ${summary}`)
|