@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.
Files changed (178) hide show
  1. package/README.md +29 -1
  2. package/dist/SHA256SUMS +1 -0
  3. package/dist/authz/authz.d.ts +44 -0
  4. package/dist/authz/authz.js +65 -0
  5. package/dist/car-runtime-1.10.0.tgz +0 -0
  6. package/dist/car-runtime-1.10.0.tgz.sig.bundle +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +825 -0
  9. package/dist/dx/doctor.d.ts +62 -0
  10. package/dist/dx/doctor.js +127 -0
  11. package/dist/governance/dump.d.ts +47 -0
  12. package/dist/governance/dump.js +17 -0
  13. package/dist/host/facade.d.ts +13 -0
  14. package/dist/host/facade.js +154 -0
  15. package/dist/host/hostGateway.d.ts +117 -0
  16. package/dist/host/hostGateway.js +65 -0
  17. package/dist/host/mappings.d.ts +27 -0
  18. package/dist/host/mappings.js +52 -0
  19. package/dist/host/stdio.d.ts +14 -0
  20. package/dist/host/stdio.js +83 -0
  21. package/dist/kernel/context.d.ts +76 -0
  22. package/dist/kernel/context.js +203 -0
  23. package/dist/kernel/events.d.ts +37 -0
  24. package/dist/kernel/events.js +82 -0
  25. package/dist/load/config.d.ts +79 -0
  26. package/dist/load/config.js +285 -0
  27. package/dist/load/loader.d.ts +72 -0
  28. package/dist/load/loader.js +162 -0
  29. package/dist/load/registry.d.ts +44 -0
  30. package/dist/load/registry.js +58 -0
  31. package/dist/load/report.d.ts +98 -0
  32. package/dist/load/report.js +301 -0
  33. package/dist/load/sigGate.d.ts +22 -0
  34. package/dist/load/sigGate.js +68 -0
  35. package/dist/load/sign.d.ts +25 -0
  36. package/dist/load/sign.js +47 -0
  37. package/dist/load/verifier.d.ts +35 -0
  38. package/dist/load/verifier.js +56 -0
  39. package/dist/loop/goal.d.ts +31 -0
  40. package/dist/loop/goal.js +34 -0
  41. package/dist/loop/recover.d.ts +17 -0
  42. package/dist/loop/recover.js +39 -0
  43. package/dist/loop/stop.d.ts +79 -0
  44. package/dist/loop/stop.js +151 -0
  45. package/dist/mcp/gateway.d.ts +96 -0
  46. package/dist/mcp/gateway.js +69 -0
  47. package/dist/mcp/httpClient.d.ts +27 -0
  48. package/dist/mcp/httpClient.js +196 -0
  49. package/dist/mcp/stdioClient.d.ts +8 -0
  50. package/dist/mcp/stdioClient.js +73 -0
  51. package/dist/ptc/budget.d.ts +21 -0
  52. package/dist/ptc/budget.js +36 -0
  53. package/dist/ptc/erasable.d.ts +14 -0
  54. package/dist/ptc/erasable.js +27 -0
  55. package/dist/ptc/runCode.d.ts +65 -0
  56. package/dist/ptc/runCode.js +140 -0
  57. package/dist/ptc/schemaRender.d.ts +15 -0
  58. package/dist/ptc/schemaRender.js +90 -0
  59. package/dist/ptc/sdk.d.ts +27 -0
  60. package/dist/ptc/sdk.js +23 -0
  61. package/dist/ptc/worker-entry.d.ts +1 -0
  62. package/dist/ptc/worker-entry.js +71 -0
  63. package/dist/release-audit.jsonl +4 -0
  64. package/dist/runtime-core/chatStep.d.ts +40 -0
  65. package/dist/runtime-core/chatStep.js +104 -0
  66. package/dist/runtime-core/credentials.d.ts +49 -0
  67. package/dist/runtime-core/credentials.js +118 -0
  68. package/dist/runtime-core/errors.d.ts +30 -0
  69. package/dist/runtime-core/errors.js +36 -0
  70. package/dist/runtime-core/llm.d.ts +76 -0
  71. package/dist/runtime-core/llm.js +553 -0
  72. package/dist/runtime-core/redaction.d.ts +39 -0
  73. package/dist/runtime-core/redaction.js +79 -0
  74. package/dist/runtime-core/telemetry.d.ts +88 -0
  75. package/dist/runtime-core/telemetry.js +332 -0
  76. package/dist/runtime-core/tools.d.ts +15 -0
  77. package/dist/runtime-core/tools.js +12 -0
  78. package/dist/runtime-core/trace.d.ts +55 -0
  79. package/dist/runtime-core/trace.js +137 -0
  80. package/dist/runtime-core/types.d.ts +84 -0
  81. package/dist/runtime-core/types.js +13 -0
  82. package/dist/sandbox/sandbox.d.ts +68 -0
  83. package/dist/sandbox/sandbox.js +153 -0
  84. package/dist/sbom.json +263 -0
  85. package/dist/seam.d.ts +21 -0
  86. package/dist/seam.js +20 -0
  87. package/dist/security/secrets-baseline.d.ts +43 -0
  88. package/dist/security/secrets-baseline.js +176 -0
  89. package/dist/security/secrets.d.ts +45 -0
  90. package/dist/security/secrets.js +138 -0
  91. package/dist/session/export.d.ts +55 -0
  92. package/dist/session/export.js +81 -0
  93. package/dist/session/fork.d.ts +51 -0
  94. package/dist/session/fork.js +63 -0
  95. package/dist/session/format.d.ts +21 -0
  96. package/dist/session/format.js +49 -0
  97. package/dist/session/indexStore.d.ts +44 -0
  98. package/dist/session/indexStore.js +265 -0
  99. package/dist/session/log.d.ts +79 -0
  100. package/dist/session/log.js +157 -0
  101. package/dist/session/store.d.ts +9 -0
  102. package/dist/session/store.js +37 -0
  103. package/dist/telemetry/metrics.d.ts +25 -0
  104. package/dist/telemetry/metrics.js +26 -0
  105. package/package.json +17 -1
  106. package/src/mcp/gateway.ts +10 -2
  107. package/src/mcp/httpClient.ts +23 -5
  108. package/src/runtime-core/trace.ts +5 -0
  109. package/src/seam.ts +21 -0
  110. package/.gitattributes +0 -11
  111. package/.github/workflows/ci.yml +0 -293
  112. package/.github/workflows/release.yml +0 -54
  113. package/adr/ADR-001.md +0 -4
  114. package/adr/ADR-002.md +0 -5
  115. package/adr/ADR-003.md +0 -35
  116. package/adr/host-onboarding.md +0 -68
  117. package/adr/mlps-audit-checklist.md +0 -41
  118. package/adr/win32-spike.md +0 -45
  119. package/docs/plugin-authoring.md +0 -82
  120. package/native/landlock-run/Makefile +0 -19
  121. package/native/landlock-run/landlock-run.c +0 -162
  122. package/scripts/contract-check.ts +0 -56
  123. package/scripts/invariant-n1.ts +0 -96
  124. package/scripts/package-check.ts +0 -29
  125. package/scripts/ptc-baseline/run-baseline.ts +0 -118
  126. package/scripts/release-artifacts.ts +0 -111
  127. package/scripts/release-pipeline.ts +0 -245
  128. package/scripts/secrets-baseline.ts +0 -33
  129. package/scripts/secrets-scan.ts +0 -56
  130. package/scripts/win32-ffi-debug.ts +0 -143
  131. package/scripts/win32-koffi-spike.ts +0 -537
  132. package/test/contract/eventmap.golden.json +0 -7
  133. package/test/fixtures/hello-plugin.ts +0 -5
  134. package/test/kernel.spec.ts +0 -136
  135. package/test/registry-e2e/run-e2e.ts +0 -213
  136. package/test/s11.spec.ts +0 -129
  137. package/test/s12.spec.ts +0 -135
  138. package/test/s13.spec.ts +0 -117
  139. package/test/s14.spec.ts +0 -117
  140. package/test/s15.spec.ts +0 -80
  141. package/test/s16.spec.ts +0 -133
  142. package/test/s17.spec.ts +0 -102
  143. package/test/s18.spec.ts +0 -81
  144. package/test/s19.spec.ts +0 -138
  145. package/test/s2.spec.ts +0 -245
  146. package/test/s20.spec.ts +0 -241
  147. package/test/s21.spec.ts +0 -231
  148. package/test/s22-m7-store-format.spec.ts +0 -135
  149. package/test/s23-m7-export-index.spec.ts +0 -189
  150. package/test/s24-signature-gate.spec.ts +0 -185
  151. package/test/s25-m8-runtime-core.spec.ts +0 -600
  152. package/test/s26-signature-closeout.spec.ts +0 -349
  153. package/test/s27-host-load-wiring.spec.ts +0 -134
  154. package/test/s28-session-index-followup.spec.ts +0 -236
  155. package/test/s29-otel-producer.spec.ts +0 -252
  156. package/test/s3.spec.ts +0 -179
  157. package/test/s30-release-artifacts.spec.ts +0 -127
  158. package/test/s31-real-model-wiring.spec.ts +0 -329
  159. package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
  160. package/test/s33-anthropic-adapter.spec.ts +0 -354
  161. package/test/s34-mcp-remote-transport.spec.ts +0 -250
  162. package/test/s35-config-ts-zod.spec.ts +0 -145
  163. package/test/s36-host-tool-surface.spec.ts +0 -250
  164. package/test/s37-projection-dedupe.spec.ts +0 -161
  165. package/test/s38-config-jiti.spec.ts +0 -116
  166. package/test/s39-otlp-span-semantics.spec.ts +0 -100
  167. package/test/s4.spec.ts +0 -169
  168. package/test/s40-metrics-cumulative.spec.ts +0 -67
  169. package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
  170. package/test/s42-traceparent-propagation.spec.ts +0 -415
  171. package/test/s5.spec.ts +0 -95
  172. package/test/s6.spec.ts +0 -169
  173. package/test/s7.spec.ts +0 -136
  174. package/test/s7b-baseline.spec.ts +0 -20
  175. package/test/s9.spec.ts +0 -89
  176. package/test/sandbox-escape/j17-orchestrate.sh +0 -30
  177. package/test/sandbox-escape/run-matrix.sh +0 -98
  178. package/tsconfig.json +0 -18
@@ -0,0 +1,98 @@
1
+ import { type Manifest, type LoadedPlugin } from './loader.ts';
2
+ import { type SignatureGateOptions } from './sigGate.ts';
3
+ export type LoadStage = 'discover' | 'verify' | 'parse' | 'validate' | 'topo' | 'register';
4
+ export type StageStatus = 'PASS' | 'FAIL' | 'SKIPPED';
5
+ /** 冲突链定位:文件 / 依赖路径 / 区间 / 实际版本(§3.2.M6.3 stages[].conflicts) */
6
+ export interface PeerConflictChain {
7
+ /** 违规声明所在插件文件 */
8
+ file: string;
9
+ peer: string;
10
+ range: string;
11
+ installed: string | null;
12
+ /** 依赖路径(声明来源,如 manifest.peers) */
13
+ path: string;
14
+ }
15
+ export interface LoadReportVO {
16
+ installId: string;
17
+ startedTime: string;
18
+ durationMs: number;
19
+ stages: Array<{
20
+ stage: LoadStage;
21
+ status: StageStatus;
22
+ /** validate FAIL 时的 peer 冲突链 */
23
+ conflicts?: PeerConflictChain[];
24
+ /** 规格外扩展:FAIL 错误定位(文件 + 原因;冲突类失败用 conflicts,其余用 error) */
25
+ error?: {
26
+ file: string;
27
+ reason: string;
28
+ };
29
+ }>;
30
+ warnings: string[];
31
+ }
32
+ /** 装配计划(renderLoadReport 的第一参;由 loadPlugins 在各阶段推进中冻结) */
33
+ export interface LoadPlan {
34
+ installId: string;
35
+ source: string;
36
+ /** ISO 8601 起始时间(进入报告) */
37
+ startedTime: string;
38
+ /** 内部计时基准(epoch ms,用于 durationMs) */
39
+ startedAtMs: number;
40
+ /** discover 产物:插件文件绝对路径(拓扑序待 topo 阶段确定) */
41
+ files: string[];
42
+ reloadEpoch?: number;
43
+ /** 装配期告警(git 直载 / peer 豁免等,E-03 语义) */
44
+ warnings: string[];
45
+ }
46
+ /** 阶段执行中间结果(内部类型,冻结前由各阶段填充) */
47
+ export interface LoadStageResult {
48
+ stage: LoadStage;
49
+ status: StageStatus;
50
+ conflicts?: PeerConflictChain[];
51
+ error?: {
52
+ file: string;
53
+ reason: string;
54
+ };
55
+ }
56
+ /** 生成安装 ID(进程内单调 + 时间戳,可读可留审计) */
57
+ export declare function newInstallId(now?: number): string;
58
+ export declare function renderLoadReport(plan: LoadPlan, stageReports: LoadStageResult[]): LoadReportVO;
59
+ export interface DiscoveredEntry {
60
+ file: string;
61
+ manifestRaw: unknown;
62
+ }
63
+ export interface LoadPluginsOptions {
64
+ /** 插件目录(扫描 *.ts)或单插件文件路径 */
65
+ source: string;
66
+ /** 热重载版本号:>0 时以 ?epoch=N 查询串击穿 ESM 缓存 */
67
+ reloadEpoch?: number;
68
+ /** 批外已安装插件版本(peer 校验的 installed 基线;缺省仅用本批次内 manifest) */
69
+ installedVersions?: Map<string, string>;
70
+ /** 显式声明 git 直载来源(如经 git URL 拉取后未走 registry 安装)——补充扫描 .git 探测 */
71
+ gitDirect?: boolean;
72
+ /** 装载签名门(M5-S28):缺省 warn 模式全量启用——缺签横幅放行,enforce 显式切入 */
73
+ signature?: SignatureGateOptions;
74
+ }
75
+ export interface LoadPluginsResult {
76
+ report: LoadReportVO;
77
+ /** register 成功装配的插件(拓扑序;FAIL 短路时为已装配前缀或空) */
78
+ plugins: LoadedPlugin[];
79
+ /** 拓扑序 manifest(供 CLI/测试断言装配顺序) */
80
+ order: Manifest[];
81
+ }
82
+ /** 六阶段装配:discover → verify → parse → validate → topo → register(任一 FAIL 短路,后续 SKIPPED) */
83
+ export declare function loadPlugins(opts: LoadPluginsOptions): Promise<LoadPluginsResult>;
84
+ /** 报告 → stdout 行(CLI 打印用;PASS/FAIL + 定位 + 耗时) */
85
+ export declare function formatLoadReport(report: LoadReportVO): string[];
86
+ /** 热重载会话:持有 epoch 与实例注册表,reload() 重走五阶段并 invalidate 旧实例 */
87
+ export declare class ReloadManager {
88
+ #private;
89
+ get epoch(): number;
90
+ get instances(): readonly LoadedPlugin[];
91
+ get history(): readonly LoadReportVO[];
92
+ /**
93
+ * SQ-05 时序:epoch+1 击穿缓存 → invalidate 旧实例(旧 ctx 后续访问抛 CAR-INVALIDATED)
94
+ * → 重走 discover→register → 五阶段报告。任一阶段 FAIL 即返回 FAIL 报告(旧实例已失效,
95
+ * 不做静默回退——加载期显式失败红线)。
96
+ */
97
+ reload(source: string, opts?: Omit<LoadPluginsOptions, 'source' | 'reloadEpoch'>): Promise<LoadPluginsResult>;
98
+ }
@@ -0,0 +1,301 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ /**
10
+ * M6 · 加载报告与装配编排(《系统设计》§3.2.M6)
11
+ *
12
+ * 口径:
13
+ * - 六阶段流水线:discover → verify → parse → validate → topo → register,任一阶段 FAIL 短路
14
+ * 后续阶段(status=SKIPPED),FAIL 必附错误定位(文件 + 原因,冲突链进 conflicts)。
15
+ * verify 为 M5-S28 加法演进(2026-09-26):装载前签名门(ADR-003 / DEC-1 ②)——必须先于
16
+ * parse 的 import(),模块执行前拦截(否则恶意代码已运行,验签失去供应链意义)
17
+ * - LoadReportVO 字段名逐字对齐 §3.2.M6.3(installId/startedTime/durationMs/stages/warnings);
18
+ * `error` 为规格外扩展字段(非冲突类失败——如 manifest 语法错——也需要文件+原因定位,BD-03)
19
+ * - git 直载(工作树内含 .git 即视为未经 registry 安装的直载来源)产生 CAR-W-GIT-DIRECT 告警(E-03 语义)
20
+ * - 热重载(SQ-05):epoch+1 击穿缓存 → invalidate 旧实例(旧 ctx 后续访问经 Proxy 抛
21
+ * CAR-INVALIDATED)→ 重走 discover→register,产出六阶段加载报告
22
+ */
23
+ import { readdirSync, existsSync, statSync } from 'node:fs';
24
+ import { join, basename } from 'node:path';
25
+ import { pathToFileURL } from 'node:url';
26
+ import { parseManifest, validatePeers, computePeerViolations, mountPlugin, } from './loader.js';
27
+ import { verifyPluginFile } from './sigGate.js';
28
+ // ==================== renderLoadReport(报告冻结:plan + 阶段执行结果 → VO) ====================
29
+ let installSeq = 0;
30
+ /** 生成安装 ID(进程内单调 + 时间戳,可读可留审计) */
31
+ export function newInstallId(now = Date.now()) {
32
+ installSeq += 1;
33
+ return `car-install-${now.toString(36)}-${installSeq.toString(36).padStart(3, '0')}`;
34
+ }
35
+ export function renderLoadReport(plan, stageReports) {
36
+ return {
37
+ installId: plan.installId,
38
+ startedTime: plan.startedTime,
39
+ durationMs: Date.now() - plan.startedAtMs,
40
+ stages: stageReports.map(s => ({
41
+ stage: s.stage,
42
+ status: s.status,
43
+ ...(s.conflicts ? { conflicts: s.conflicts } : {}),
44
+ ...(s.error ? { error: s.error } : {}),
45
+ })),
46
+ warnings: [...plan.warnings],
47
+ };
48
+ }
49
+ const GIT_URL_RE = /^(?:git\+|git@|ssh:\/\/git@)/;
50
+ const CAR_W_GIT = 'CAR-W-GIT-DIRECT';
51
+ const CAR_E = (stage, msg) => `CAR-E-${stage.toUpperCase()}: ${msg}`;
52
+ const STAGE_ORDER = ['discover', 'verify', 'parse', 'validate', 'topo', 'register'];
53
+ /** 六阶段装配:discover → verify → parse → validate → topo → register(任一 FAIL 短路,后续 SKIPPED) */
54
+ export async function loadPlugins(opts) {
55
+ const startedAtMs = Date.now();
56
+ const startedTime = new Date(startedAtMs).toISOString();
57
+ const epoch = opts.reloadEpoch ?? 0;
58
+ const warnings = [];
59
+ const stages = [];
60
+ let files = [];
61
+ let ordered = [];
62
+ // 短路收口:为尚未执行的阶段补 SKIPPED,冻结 plan → LoadReportVO
63
+ const finish = (ps) => {
64
+ const complete = [...stages];
65
+ for (const s of STAGE_ORDER) {
66
+ if (!complete.some(d => d.stage === s))
67
+ complete.push({ stage: s, status: 'SKIPPED' });
68
+ }
69
+ const plan = {
70
+ installId: newInstallId(startedAtMs),
71
+ source: opts.source,
72
+ startedTime,
73
+ startedAtMs,
74
+ files,
75
+ reloadEpoch: epoch,
76
+ warnings,
77
+ };
78
+ return { report: renderLoadReport(plan, complete), plugins: ps, order: ordered };
79
+ };
80
+ // ===== 阶段 1:discover(扫描插件目录 *.ts;git 直载来源产生告警) =====
81
+ if (opts.gitDirect || GIT_URL_RE.test(opts.source)) {
82
+ // git 直载告警先于扫描:直载 URL 本地不存在也能在 FAIL 报告中携带来源告警(E-03)
83
+ warnings.push(`${CAR_W_GIT}: 插件来源为 git 直载(${opts.source})——未经 registry 验签,建议改用 registry 安装(E-03)`);
84
+ }
85
+ try {
86
+ files = discoverFiles(opts.source);
87
+ if (existsSync(opts.source) && statSync(opts.source).isDirectory() && existsSync(join(opts.source, '.git'))) {
88
+ warnings.push(`${CAR_W_GIT}: 插件目录位于 git 工作树(${opts.source})——未经 registry 验签的直载来源(E-03)`);
89
+ }
90
+ if (!files.length) {
91
+ stages.push({ stage: 'discover', status: 'FAIL', error: { file: opts.source, reason: CAR_E('discover', `no *.ts plugin files found under "${opts.source}"`) } });
92
+ return finish([]);
93
+ }
94
+ stages.push({ stage: 'discover', status: 'PASS' });
95
+ }
96
+ catch (e) {
97
+ stages.push({ stage: 'discover', status: 'FAIL', error: { file: opts.source, reason: `${e.message}` } });
98
+ return finish([]);
99
+ }
100
+ // ===== 阶段 2:verify(装载前签名门——先于 parse 的 import(),模块执行前拦截;ADR-003 / DEC-1 ②) =====
101
+ const sig = opts.signature ?? {};
102
+ const sigOnCount = sig.onCount;
103
+ for (const file of files) {
104
+ const g = verifyPluginFile(file, sig);
105
+ if (!g.allowed) {
106
+ sigOnCount?.('car_load_total', { result: 'failed' });
107
+ stages.push({ stage: 'verify', status: 'FAIL', error: { file, reason: g.error ?? 'CAR-E-SIG: rejected by signature gate' } });
108
+ return finish([]);
109
+ }
110
+ if (g.warning)
111
+ warnings.push(g.warning);
112
+ }
113
+ stages.push({ stage: 'verify', status: 'PASS' });
114
+ // ===== 阶段 3:parse(parseManifest;manifest 取模块命名导出,缺失则按文件名合成) =====
115
+ const entries = [];
116
+ for (const file of files) {
117
+ try {
118
+ const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(file).href + (epoch ? `?epoch=${epoch}` : ''))));
119
+ entries.push({ file, manifestRaw: mod.manifest ?? { name: basename(file).replace(/\.ts$/, ''), version: '0.0.1' } });
120
+ }
121
+ catch (e) {
122
+ stages.push({ stage: 'parse', status: 'FAIL', error: { file, reason: `CAR-E-PARSE: module import failed — ${e.message}` } });
123
+ return finish([]);
124
+ }
125
+ }
126
+ const parsed = [];
127
+ for (const { file, manifestRaw } of entries) {
128
+ try {
129
+ parsed.push({ file, manifest: parseManifest(manifestRaw, file) });
130
+ }
131
+ catch (e) {
132
+ stages.push({ stage: 'parse', status: 'FAIL', error: { file, reason: e.message } });
133
+ return finish([]);
134
+ }
135
+ }
136
+ const manifests = parsed.map(p => p.manifest);
137
+ stages.push({ stage: 'parse', status: 'PASS' });
138
+ // ===== 阶段 4:validate(peer 冲突链;豁免需 reason → 告警留痕) =====
139
+ const installed = opts.installedVersions ?? new Map();
140
+ for (const m of manifests)
141
+ installed.set(m.name, m.version);
142
+ const conflicts = [];
143
+ const exemptDetail = [];
144
+ let exemptedAny = false;
145
+ for (const { file, manifest: m } of parsed) {
146
+ try {
147
+ const r = validatePeers(m, installed);
148
+ if (r.exempted) {
149
+ // 豁免生效:不进 FAIL 冲突链,仅告警留痕(reason 已在 manifest 声明)
150
+ exemptedAny = true;
151
+ for (const v of r.violations.filter(v => !v.optional)) {
152
+ exemptDetail.push(`${file}: peer ${v.peer}@${v.range} vs ${v.installed ?? '<missing>'}`);
153
+ }
154
+ }
155
+ }
156
+ catch {
157
+ // 严格模式硬冲突:不中止校验其余插件,收集全量冲突链后统一 FAIL(BD-03 定位口径)
158
+ for (const v of computePeerViolations(m, installed).filter(v => !v.optional))
159
+ conflicts.push(toChain(file, v));
160
+ }
161
+ }
162
+ if (conflicts.length) {
163
+ stages.push({ stage: 'validate', status: 'FAIL', conflicts });
164
+ return finish([]);
165
+ }
166
+ if (exemptedAny)
167
+ warnings.push(`CAR-W-PEER-EXEMPT: peer 冲突经 manifest.peerPolicyOverride 显式豁免(${exemptDetail.join('; ')})——reason 已声明,审计留痕`);
168
+ stages.push({ stage: 'validate', status: 'PASS' });
169
+ // ===== 阶段 5:topo(peer 依赖边 → Kahn 分层拓扑排序;环 = 显式报错) =====
170
+ try {
171
+ ordered = topoSort(manifests);
172
+ }
173
+ catch (e) {
174
+ stages.push({ stage: 'topo', status: 'FAIL', error: { file: opts.source, reason: e.message } });
175
+ return finish([]);
176
+ }
177
+ stages.push({ stage: 'topo', status: 'PASS' });
178
+ // ===== 阶段 6:register(mountPlugin 按拓扑序装配;重名 = 显式冲突 + 冲突链定位) =====
179
+ const plugins = [];
180
+ const seenNames = new Set();
181
+ for (const m of ordered) {
182
+ const file = parsed.find(p => p.manifest.name === m.name)?.file ?? '';
183
+ if (seenNames.has(m.name)) {
184
+ // M6-BUG-1:重名冲突链 = 全部同名文件路径(BD-03:文件级定位,不再误导为依赖环)
185
+ const dupFiles = parsed.filter(p => p.manifest.name === m.name).map(p => p.file);
186
+ stages.push({ stage: 'register', status: 'FAIL', error: { file, reason: `CAR-E-DUP: plugin "${m.name}" registered more than once (duplicate registration is blocked) (conflict chain: ${dupFiles.join(', ')})` } });
187
+ return finish(plugins);
188
+ }
189
+ seenNames.add(m.name);
190
+ try {
191
+ plugins.push(await mountPlugin({ file, manifest: m, reloadEpoch: epoch }));
192
+ // S17 采集面:load_total(ok) 逐插件计数(unsigned 占比分母;零内容 labels)
193
+ sigOnCount?.('car_load_total', { result: 'ok' });
194
+ }
195
+ catch (e) {
196
+ stages.push({ stage: 'register', status: 'FAIL', error: { file, reason: e.message } });
197
+ return finish(plugins);
198
+ }
199
+ }
200
+ stages.push({ stage: 'register', status: 'PASS' });
201
+ return finish(plugins);
202
+ }
203
+ function toChain(declaringFile, v) {
204
+ return { file: declaringFile, peer: v.peer, range: v.range, installed: v.installed, path: 'manifest.peers' };
205
+ }
206
+ /** discover:目录扫描 *.ts(排除 .d.ts / .spec.ts);单文件直载原样返回 */
207
+ function discoverFiles(source) {
208
+ if (!existsSync(source)) {
209
+ throw new Error(CAR_E('discover', `source not found: "${source}"`));
210
+ }
211
+ if (statSync(source).isFile())
212
+ return [source];
213
+ return readdirSync(source)
214
+ .filter(f => f.endsWith('.ts') && !f.endsWith('.d.ts') && !f.endsWith('.spec.ts'))
215
+ .sort()
216
+ .map(f => join(source, f));
217
+ }
218
+ /**
219
+ * topo:Kahn 分层——边 = 非可选 peer 指向批次内另一插件;环 = CAR-E-DEPCYCLE。
220
+ * 重名安全(M6-BUG-1):放置判定按 manifest 逐个推进 + pushedNames 记名级放置——
221
+ * 重名 manifest 在首个同名放置后即可全部放置,不再因 name 键碰撞被误判为依赖环;
222
+ * 重名的显式冲突由 register 阶段 CAR-E-DUP 报出(BD-03:不误导定位到 topo)。
223
+ */
224
+ function topoSort(manifests) {
225
+ const names = new Set(manifests.map(m => m.name));
226
+ const depsOf = new Map();
227
+ for (const m of manifests) {
228
+ const deps = depsOf.get(m.name) ?? new Set();
229
+ for (const p of m.peers ?? []) {
230
+ if (!p.optional && names.has(p.peer) && p.peer !== m.name)
231
+ deps.add(p.peer);
232
+ }
233
+ depsOf.set(m.name, deps);
234
+ }
235
+ const ordered = [];
236
+ const pushed = new Set();
237
+ const pushedNames = new Set();
238
+ let progressed = true;
239
+ while (ordered.length < manifests.length && progressed) {
240
+ progressed = false;
241
+ for (const m of manifests) {
242
+ if (pushed.has(m))
243
+ continue;
244
+ // 就绪条件:自身名已放置(重名后续项)或全部 peer 依赖已放置
245
+ if (pushedNames.has(m.name) || [...depsOf.get(m.name)].every(d => pushedNames.has(d))) {
246
+ ordered.push(m);
247
+ pushed.add(m);
248
+ pushedNames.add(m.name);
249
+ progressed = true;
250
+ }
251
+ }
252
+ }
253
+ if (ordered.length < manifests.length) {
254
+ // 仅「未放置且依赖永不齐全」的真实依赖环落入此分支(重名已被 pushedNames 消化)
255
+ const cyclic = [...new Set(manifests.filter(m => !pushed.has(m)).map(m => m.name))];
256
+ throw new Error(`CAR-E-DEPCYCLE: dependency cycle among [${cyclic.join(', ')}] (peer 边成环,拓扑排序中止)`);
257
+ }
258
+ return ordered;
259
+ }
260
+ // ==================== 热重载(SQ-05:car reload) ====================
261
+ /** 报告 → stdout 行(CLI 打印用;PASS/FAIL + 定位 + 耗时) */
262
+ export function formatLoadReport(report) {
263
+ const lines = [];
264
+ lines.push(`加载报告 installId=${report.installId} startedTime=${report.startedTime}`);
265
+ for (const s of report.stages) {
266
+ let line = ` [${s.stage.padEnd(8)}] ${s.status}`;
267
+ if (s.error)
268
+ line += ` — ${s.error.reason} (file: ${s.error.file})`;
269
+ if (s.conflicts?.length) {
270
+ line += ' — ' + s.conflicts.map(c => `${c.file}: peer ${c.peer}@${c.range} required but ${c.installed ?? '<missing>'} installed (path: ${c.path})`).join('; ');
271
+ }
272
+ lines.push(line);
273
+ }
274
+ lines.push(` durationMs=${report.durationMs} warnings=${report.warnings.length}`);
275
+ for (const w of report.warnings)
276
+ lines.push(` ⚠ ${w}`);
277
+ return lines;
278
+ }
279
+ /** 热重载会话:持有 epoch 与实例注册表,reload() 重走五阶段并 invalidate 旧实例 */
280
+ export class ReloadManager {
281
+ #epoch = 0;
282
+ #instances = [];
283
+ #reports = [];
284
+ get epoch() { return this.#epoch; }
285
+ get instances() { return this.#instances; }
286
+ get history() { return this.#reports; }
287
+ /**
288
+ * SQ-05 时序:epoch+1 击穿缓存 → invalidate 旧实例(旧 ctx 后续访问抛 CAR-INVALIDATED)
289
+ * → 重走 discover→register → 五阶段报告。任一阶段 FAIL 即返回 FAIL 报告(旧实例已失效,
290
+ * 不做静默回退——加载期显式失败红线)。
291
+ */
292
+ async reload(source, opts = {}) {
293
+ this.#epoch += 1;
294
+ for (const old of this.#instances)
295
+ old.invalidate();
296
+ const result = await loadPlugins({ ...opts, source, reloadEpoch: this.#epoch });
297
+ this.#instances = result.plugins;
298
+ this.#reports = [...this.#reports, result.report];
299
+ return result;
300
+ }
301
+ }
@@ -0,0 +1,22 @@
1
+ import type { AllowedLabels, CounterName } from '../telemetry/metrics.ts';
2
+ export interface SignatureGateOptions {
3
+ /** 缺省 'warn'(DEC-1 ②:1.0 以 warn 发布,宽限期到期翻转为 enforce = 计划内默认值变更) */
4
+ mode?: 'warn' | 'enforce';
5
+ /** 信任锚公钥(ed25519 spki base64);缺省走 CAR_TRUST_ROOT env,无则全部按 unsigned 处理 */
6
+ trustRootPublicKey?: string;
7
+ /** S20 §4 路径②:显式声明豁免(workspace 生效;warn 缺签路径计数 confirmed=yes,横幅保留) */
8
+ unsignedAllow?: boolean;
9
+ /** 零内容计数桥(metrics.ts Counters.onCount 兼容;labels 仅枚举,禁路径/内容) */
10
+ onCount?: (name: CounterName, labels?: AllowedLabels) => void;
11
+ }
12
+ export interface SigGateResult {
13
+ allowed: boolean;
14
+ warning?: string;
15
+ error?: string;
16
+ /** 入口文件裸字节 sha256 hex(读取失败时缺省)——横幅指纹与审计留痕用 */
17
+ manifestHash?: string;
18
+ }
19
+ /** env 配置通道(与 CAR_OFFLINE/CAR_REGISTRY_URL 同为先例):CAR_SIG_ENFORCE/CAR_TRUST_ROOT/CAR_UNSIGNED_ALLOW */
20
+ export declare function gateDepsFromEnv(env?: NodeJS.ProcessEnv): SignatureGateOptions;
21
+ /** 单插件文件签名门:读文件算指纹 → 读 sidecar → verifier 判定(装载前三段第一道,先于任何模块执行) */
22
+ export declare function verifyPluginFile(file: string, opts?: SignatureGateOptions): SigGateResult;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * M5-S28 · 装载签名门(sigGate):verifier 的装载接线层(fs 层;verifier 保持纯验签、无 I/O)
3
+ *
4
+ * 口径:
5
+ * - 被签物锚定:manifestHash = sha256(插件入口 .ts 文件裸字节) 的 hex 串。manifest 是运行时对象
6
+ * (parse 阶段 import() 后才存在),无独立 manifest 文件字节可签——对入口文件整字节求哈希
7
+ * 同时覆盖 manifest 声明与全部代码,即宽限文本承诺的「manifestHash 指纹」。被签消息 =
8
+ * hex 字符串的 UTF-8 字节(verifyMinisig Buffer.from(manifestHash) 口径;registry-e2e
9
+ * run-e2e.ts「对制品原始字节求 sha256 再签 hex 串」先例同源)。哈希对裸字节求
10
+ * (release-pipeline G-11 教训:禁止对编码文本求哈希)。
11
+ * - 签名载体:sidecar `<file>.minisig`(ed25519 签名 base64,唯一实装轨);sigstore 主轨
12
+ * 维持 verifier 骨架期预留(SignatureBundle.sigstoreBundle),sidecar 命名随主轨实装再登记。
13
+ * - 门禁语义(ADR-003 / DEC-1 ②,2026-09-26 裁决):缺省 mode=warn——缺签=横幅放行+指纹兜底
14
+ * +计数 confirmed=no;mode=enforce——缺签硬拒绝;有签名但验签失败=硬拒绝(不可配置放行,
15
+ * unsignedAllow 亦不豁免)。
16
+ * - unsignedAllow(S20 §4 路径②:workspace 显式声明豁免):仅作用于 warn 缺签路径——
17
+ * 计数改写 confirmed=yes(S20 §1 口径:yes=显式确认豁免动作后),横幅保留不消失。
18
+ * - 信任根交付:CAR_TRUST_ROOT env(ed25519 spki,base64)或显式入参。缺省无信任根 =
19
+ * 全部插件按 unsigned 处理(warn 下横幅显式可见;enforce 下全部拒绝属 fail-closed,非静默)。
20
+ * - 计数桥:onCount 采用 metrics.ts 宽签名(CounterName);对 verifier 的窄回调参数做
21
+ * 适配包装(宽回调不能直接赋给 verifier 的窄参数位——strictFunctionTypes 逆变)。
22
+ */
23
+ import { createHash } from 'node:crypto';
24
+ import { existsSync, readFileSync } from 'node:fs';
25
+ import { enforceSignature } from './verifier.js';
26
+ /** env 配置通道(与 CAR_OFFLINE/CAR_REGISTRY_URL 同为先例):CAR_SIG_ENFORCE/CAR_TRUST_ROOT/CAR_UNSIGNED_ALLOW */
27
+ export function gateDepsFromEnv(env = process.env) {
28
+ return {
29
+ mode: env.CAR_SIG_ENFORCE === '1' ? 'enforce' : 'warn',
30
+ trustRootPublicKey: env.CAR_TRUST_ROOT || undefined,
31
+ unsignedAllow: env.CAR_UNSIGNED_ALLOW === '1',
32
+ };
33
+ }
34
+ /** 单插件文件签名门:读文件算指纹 → 读 sidecar → verifier 判定(装载前三段第一道,先于任何模块执行) */
35
+ export function verifyPluginFile(file, opts = {}) {
36
+ let manifestHash;
37
+ try {
38
+ manifestHash = createHash('sha256').update(readFileSync(file)).digest('hex');
39
+ }
40
+ catch (e) {
41
+ return { allowed: false, error: `CAR-E-SIG: plugin file unreadable — ${e.message}` };
42
+ }
43
+ let minisig;
44
+ const sidecar = `${file}.minisig`;
45
+ if (existsSync(sidecar)) {
46
+ try {
47
+ const raw = readFileSync(sidecar, 'utf-8').trim();
48
+ if (raw)
49
+ minisig = raw;
50
+ }
51
+ catch (e) {
52
+ // sidecar 存在但不可读 = 环境失败,fail-closed 显式拒绝(不静默降级为 unsigned)
53
+ return { allowed: false, manifestHash, error: `CAR-E-SIG: sidecar unreadable — ${e.message}` };
54
+ }
55
+ }
56
+ // 显式豁免改写:S20 §1 confirmed=yes = 显式确认豁免动作后(仅 warn 缺签路径会被 verifier 计数)
57
+ const onCount = opts.unsignedAllow
58
+ ? (name, labels) => opts.onCount?.(name, { ...labels, confirmed: 'yes' })
59
+ : (name, labels) => opts.onCount?.(name, labels);
60
+ const deps = {
61
+ trustRootPublicKey: opts.trustRootPublicKey ?? '',
62
+ mode: opts.mode ?? 'warn',
63
+ };
64
+ const r = enforceSignature(manifestHash, minisig ? { minisig } : undefined, deps, onCount);
65
+ // 横幅附指纹前缀:宽限文本承诺的「manifestHash 指纹兜底」在告警面可见(前 12 hex 位)
66
+ const warning = r.warning ? `${r.warning} [fp:${manifestHash.slice(0, 12)}]` : undefined;
67
+ return { allowed: r.allowed, warning, error: r.error, manifestHash };
68
+ }
@@ -0,0 +1,25 @@
1
+ export interface SigningKeypair {
2
+ /** PKCS8 DER 私钥字节(写 <前缀>.priv;离线保管不入仓库——secrets 红线) */
3
+ privateKeyDer: Buffer;
4
+ /** SPKI DER base64 公钥(写 <前缀>.pub;即 CAR_TRUST_ROOT 信任根值) */
5
+ publicKeyBase64: string;
6
+ }
7
+ /** ed25519 签名密钥对生成(指南 §2 第 1 步 node -e 流程固化) */
8
+ export declare function generateSigningKeypair(): SigningKeypair;
9
+ export interface SignResult {
10
+ file: string;
11
+ sidecar: string;
12
+ /** 文件裸字节 sha256 hex(横幅指纹与审计留痕口径同 sigGate.manifestHash) */
13
+ manifestHash: string;
14
+ }
15
+ /** 对单文件签名并写 sidecar `<file>.minisig`(指南 §2 第 2 步 node -e 流程固化) */
16
+ export declare function signPluginFile(file: string, privateKeyDer: Buffer): SignResult;
17
+ /** 信任根解析(plugin-sign verify 用):显式 flag > CAR_TRUST_ROOT env > <前缀>.pub 文件 */
18
+ export declare function resolveTrustRoot(opts: {
19
+ flag?: string;
20
+ env?: NodeJS.ProcessEnv;
21
+ pubFilePath?: string;
22
+ }): {
23
+ value?: string;
24
+ source: 'flag' | 'env' | 'pubfile' | 'none';
25
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 1.1-S1 · plugin-sign 签名原语(keygen / sign 纯函数;verify 复用 sigGate.verifyPluginFile)
3
+ *
4
+ * 口径(与 M2-S6 verifier / M5-S28 sigGate 签验同源,逐字对齐签名强制启用指南 §2):
5
+ * - 被签物 = 插件入口 .ts 文件裸字节 sha256 的 hex 串;被签消息 = hex 字符串的 UTF-8 字节
6
+ * (sigGate.ts 头注释同源;哈希对裸字节求——G-11 教训:禁止对编码文本求哈希)
7
+ * - 私钥 = ed25519 PKCS8 DER 文件(<前缀>.priv);公钥 = SPKI DER base64(<前缀>.pub,
8
+ * 即 CAR_TRUST_ROOT 信任根值);sidecar = `<file>.minisig` 单段 base64(sigGate 读取口径)
9
+ * - 本模块只产出密钥与签名;门禁判定(warn/enforce 分级、fail-closed 分级)在 verifier/sigGate——
10
+ * sign 不判门。边界登记(1.1 规划 W2-1):minisign 离线单轨;sigstore keyless 主轨延后(1.2 候选)
11
+ */
12
+ import { createHash, generateKeyPairSync, sign as cryptoSign } from 'node:crypto';
13
+ import { readFileSync, writeFileSync } from 'node:fs';
14
+ /** ed25519 签名密钥对生成(指南 §2 第 1 步 node -e 流程固化) */
15
+ export function generateSigningKeypair() {
16
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519');
17
+ return {
18
+ privateKeyDer: privateKey.export({ type: 'pkcs8', format: 'der' }),
19
+ publicKeyBase64: publicKey.export({ type: 'spki', format: 'der' }).toString('base64'),
20
+ };
21
+ }
22
+ /** 对单文件签名并写 sidecar `<file>.minisig`(指南 §2 第 2 步 node -e 流程固化) */
23
+ export function signPluginFile(file, privateKeyDer) {
24
+ const manifestHash = createHash('sha256').update(readFileSync(file)).digest('hex');
25
+ const minisig = cryptoSign(null, Buffer.from(manifestHash), { key: privateKeyDer, format: 'der', type: 'pkcs8' }).toString('base64');
26
+ const sidecar = `${file}.minisig`;
27
+ writeFileSync(sidecar, minisig);
28
+ return { file, sidecar, manifestHash };
29
+ }
30
+ /** 信任根解析(plugin-sign verify 用):显式 flag > CAR_TRUST_ROOT env > <前缀>.pub 文件 */
31
+ export function resolveTrustRoot(opts) {
32
+ if (opts.flag)
33
+ return { value: opts.flag, source: 'flag' };
34
+ const envVal = opts.env?.CAR_TRUST_ROOT;
35
+ if (envVal)
36
+ return { value: envVal, source: 'env' };
37
+ const pub = opts.pubFilePath;
38
+ if (pub) {
39
+ try {
40
+ const v = readFileSync(pub, 'utf-8').trim();
41
+ if (v)
42
+ return { value: v, source: 'pubfile' };
43
+ }
44
+ catch { /* pub 文件不可读 → 视同未提供,由调用方显式报错 */ }
45
+ }
46
+ return { value: undefined, source: 'none' };
47
+ }
@@ -0,0 +1,35 @@
1
+ export interface SignatureBundle {
2
+ /** minisign 式离线轨:对 manifestHash 的 ed25519 签名(base64) */
3
+ minisig?: string;
4
+ /** Sigstore keyless 主轨:离线 bundle(S6 接口预留——bundle 校验需生态依赖,M3/沙箱外实装) */
5
+ sigstoreBundle?: string;
6
+ }
7
+ export interface VerifyResult {
8
+ ok: boolean;
9
+ track: 'minisign' | 'sigstore' | 'none';
10
+ error?: string;
11
+ }
12
+ export interface VerifierDeps {
13
+ /** 信任锚公钥(ed25519,base64;内置根 pinning 或 trustRoot 替换点) */
14
+ trustRootPublicKey: string;
15
+ /** D-2:S6-S9 = warn,S10 起 = enforce */
16
+ mode: 'warn' | 'enforce';
17
+ /** Sigstore 主轨探针(骨架期返回 false → 回退静态轨,复现 ADR-003 回退语义) */
18
+ sigstoreAvailable?: () => boolean;
19
+ }
20
+ /** 静态轨:ed25519 验证 manifestHash 签名 */
21
+ export declare function verifyMinisig(manifestHash: string, minisig: string, trustRootPublicKey: string): {
22
+ ok: boolean;
23
+ error?: string;
24
+ };
25
+ /**
26
+ * 验签入口(装载前调用,A010001 之前的供应链第一道门):
27
+ * 返回 { allowed, warning? }——warning 非空时调用方必须告警放行并留痕(enforce 前过渡期)
28
+ */
29
+ export declare function enforceSignature(manifestHash: string, sig: SignatureBundle | undefined, deps: VerifierDeps, onCount?: (name: 'car_unsigned_confirmed', labels: {
30
+ confirmed: 'yes' | 'no';
31
+ }) => void): {
32
+ allowed: boolean;
33
+ warning?: string;
34
+ error?: string;
35
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * S6 验签器(ADR-003 双轨实装):minisign 离线轨(node:crypto ed25519)+ Sigstore 接口预留
3
+ *
4
+ * fail-closed 分级(D-2 裁决):
5
+ * - 校验失败(有签名但不对)= 硬拒绝,不可配置放行
6
+ * - 签名缺失 = warn 模式告警放行(manifestHash 兜底)→ enforce 模式硬拒绝
7
+ * - Sigstore 轨不可达 = 回退静态轨;双轨皆败按缺失处理
8
+ *
9
+ * 接线登记(M5-S28,2026-09-26):本模块自 S6 入库即为纯验签函数;装载路径接线由
10
+ * sigGate.ts(fs 层:文件指纹 + sidecar + env 配置)与 loadPlugins verify 阶段 /
11
+ * car run 直载门完成——verify 先于 parse 的 import(),模块执行前拦截。
12
+ * manifestHash 锚定口径 = sha256(插件入口文件裸字节) hex(被签消息 = hex 串 UTF-8 字节),
13
+ * 详见 sigGate.ts 头注释。本文件 API 面不变(s6/s17 契约冻结)。
14
+ */
15
+ import { verify, createPublicKey } from 'node:crypto';
16
+ /** 静态轨:ed25519 验证 manifestHash 签名 */
17
+ export function verifyMinisig(manifestHash, minisig, trustRootPublicKey) {
18
+ try {
19
+ const key = createPublicKey({ key: Buffer.from(trustRootPublicKey, 'base64'), format: 'der', type: 'spki' });
20
+ const ok = verify(null, Buffer.from(manifestHash), key, Buffer.from(minisig, 'base64'));
21
+ return ok ? { ok: true } : { ok: false, error: 'signature mismatch' };
22
+ }
23
+ catch (e) {
24
+ return { ok: false, error: `verify error: ${String(e)}` };
25
+ }
26
+ }
27
+ /**
28
+ * 验签入口(装载前调用,A010001 之前的供应链第一道门):
29
+ * 返回 { allowed, warning? }——warning 非空时调用方必须告警放行并留痕(enforce 前过渡期)
30
+ */
31
+ export function enforceSignature(manifestHash, sig, deps, onCount) {
32
+ // 主轨探针:可用则先走 Sigstore(S6 骨架期默认不可用 → 回退静态轨)
33
+ if (deps.sigstoreAvailable?.()) {
34
+ if (sig?.sigstoreBundle) {
35
+ // bundle 校验需生态依赖(@sigstore/verify),骨架期视为不可达 → 回退静态轨(ADR-003 回退语义)
36
+ }
37
+ else if (deps.mode === 'enforce') {
38
+ return { allowed: false, error: 'CAR-E-SIG: sigstore bundle missing (enforce mode)' };
39
+ }
40
+ }
41
+ // 静态轨
42
+ if (sig?.minisig) {
43
+ const r = verifyMinisig(manifestHash, sig.minisig, deps.trustRootPublicKey);
44
+ if (r.ok)
45
+ return { allowed: true };
46
+ // 有签名但校验失败 = 硬拒绝(不可配置放行——ADR-003 fail-closed 第一级)
47
+ return { allowed: false, error: `CAR-E-SIG: signature verification FAILED: ${r.error}(不可配置放行)` };
48
+ }
49
+ // 签名缺失
50
+ if (deps.mode === 'enforce') {
51
+ return { allowed: false, error: 'CAR-E-SIG: signature missing (enforce mode)——manifestHash 指纹兜底不替代签名' };
52
+ }
53
+ // S17 采集面:unsigned 路径计数(零内容 labels;confirmed=no=未确认 warn / yes=显式确认豁免动作)
54
+ onCount?.('car_unsigned_confirmed', { confirmed: 'no' });
55
+ return { allowed: true, warning: 'CAR-W-SIG: unsigned plugin(warn 过渡期放行,manifestHash 指纹兜底;enforce 切换见 D-2 终局条件)' };
56
+ }