@lqc123qwe/car-runtime 1.0.0 → 1.1.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 CHANGED
@@ -124,11 +124,15 @@ node --experimental-transform-types --test test/*.spec.ts
124
124
  | `src/load/sigGate.ts` + `src/load/report.ts` verify 阶段 | M5-S28 装载签名门接线:manifestHash = 插件入口文件裸字节 sha256 hex(`<file>.minisig` sidecar,被签消息 = hex 串 UTF-8 字节)+ 流水线五阶段→**六阶段**(discover→**verify**→parse→…,先于 import() 模块执行前拦截)+ `car run` 直载门 + `car reload` 透传 + `car_load_total`/`car_unsigned_confirmed` 生产计数 + `CAR_TRUST_ROOT`/`CAR_SIG_ENFORCE`/`CAR_UNSIGNED_ALLOW` env 通道 + unsignedAllow→confirmed=yes 豁免改写 + fp 指纹横幅 | ADR-003 追记、DEC-1 ②、S20 §5 复评行 |
125
125
  | `test/s24-signature-gate.spec.ts` | M5-S28 测试:8 个 test(warn 缺签横幅+fp+confirmed=no / unsignedAllow=yes 改写+横幅保留 / enforce 缺签 FAIL+空 plugins / 坏签名硬拒绝不可豁免 / 好签名静默 / load_total 分母 / reload 透传 / 单文件门+env 映射) | 全量 204 |
126
126
 
127
+ | `src/runtime-core/`(7 文件)+ `src/kernel` Context 根作用域 `effect()` + `src/dx/doctor.ts` keychain 行 | M8 运行时底座:types DTO(finishReason 不可变透传口径)/ llm.ts(AdapterRegistry 重复 id 显式报错 + withFinishReasonGuard 不可变守卫 + RuntimeCore Effect 可逆注册 + openai-compat SSE 适配器:TLS 强制 / 未映射 finish fail-visible / 首块前重试 ≤2 指数退避 / 凭据门 resolve→reveal→Bearer / A080001 首块前传播)/ credentials(Keychain→env,A080001 引导 car doctor)/ redaction(redact + StreamRedactor 跨 chunk 驻留)/ telemetry(OTel 门面默认关三原则)/ chatStep(SQ-07:N1 前置断言→投影请求→流消费→assistant 落 M7→toolUse 交 M4)/ errors(CarM8Error 双轨码) | §3.2.M8、SQ-07、O-11/O-13/O-14 |
128
+ | `test/s25-m8-runtime-core.spec.ts` | M8 测试:27 个 test(注册表/finishReason 守卫/Effect 可逆/凭据三层/脱敏/遥测双路径/openai-compat SSE·重试·凭据门/chatStep 集成六面/SQ-07 端到端) | §3.2.M8 AC |
129
+
127
130
  ## M5 状态
128
131
 
129
132
  - **M5 收口(2026-09-12)**:三项 deferred 全部转正并验证——win32 sandbox spike 四判据自动断言全 PASS(S24/S25/S26)、JSON Schema→TS 深度参数渲染(dee4d67)、跨宿主 fork/resume(03c0fec)。全量 **156/156**。
130
133
  - 工具面 9→10(新增 `session_fork`);`session_start` 增 importJsonl 导入路径;RC-3 冻结未生效(1.0-rc tag 未切),工具面契约变更合规登记。
131
134
  - **S28 收口(2026-09-26)**:**DEC-1 裁决 = ②**(warn + 《签名强制启用指南》组合发布 + RC-3 承诺显式降级;数据窗口 0 会话不满足切 enforce 前提,D-2 红线禁止无数据 enforce;宽限期 = 1.0 发布日 + 90 天,到期 enforce 默认值翻转为**计划内变更**——S20 §5 复评行归档)。配套工程同日收口:**装载签名门接线**——verifier 自 M2-S6(e11d23b)入库零生产调用点的缺口补齐(此前「已实装仅切默认值」口径经双向核查不成立,S20 §6 勘误登记):loadPlugins 五阶段→六阶段(verify 先于 parse 的 import(),模块执行前拦截)+ car run 直载门 + car reload 透传 + car_load_total/car_unsigned_confirmed 生产计数 + manifestHash 指纹兜底实装 + CAR_TRUST_ROOT/CAR_SIG_ENFORCE/CAR_UNSIGNED_ALLOW env 通道。全量 **204(201 PASS + 3 能力门控 skip / 0 fail)**。S29 前置三修复(ce0bf65):G-01 钉 tap reporter(Node 23 spec 默认致解析落空)/ G-03 豁免清单对齐 CI 权威版 / release.yml publish tag 动态化(1.0.0→latest,rc 保留指向 rc.1)。**GO-1 复跑达成:12 PASS / 1 DRY-RUN / 0 FAIL**(G-10 Release 实查挂 GO-7 token rotate 后全链验证收口)。
135
+ - **S29/S30 收口(2026-09-26)**:**1.0.0 正式发布,M5 全程闭环**。GO-7 token rotate 收官(新 npm token 入 repo secret + PAT `*0Fbq`,全链 13 PASS,旧凭证双撤销);version 1.0.0(`9435f99`)→ tag v1.0.0 → 发布前彩排 11+2 → Release v1.0.0(用户创建)→ j16 首跑 E403(npm 2FA 策略,bypass token 回退路径兑现)→ re-run 绿 `npm publish --provenance --tag latest` → dist-tags 终态 latest=1.0.0 / rc=rc.1 → Release 资产两件错挂修正 → **post-publish 13 PASS / 0 DRY-RUN / 0 FAIL** → 外部验证者等价检查(SHA256 MATCH + cosign Verified OK)→ S30 终验报告。**宽限期:2026-09-26 → 2026-12-25 到期 enforce 翻转**。
132
136
 
133
137
  ## M6 状态
134
138
 
@@ -146,9 +150,28 @@ node --experimental-transform-types --test test/*.spec.ts
146
150
  - **CI 能力真跑(防幽灵门禁)**:j02/j03/j04 测试命令直挂 `--experimental-sqlite`(22.5+ 带 flag 引入);zstd 为 **22.15+ 原生内置(无 CLI flag**,Stability: 1 实验标记),22.19 原生在场直跑。排障两轮(run 35451413014/35451612564):①node 运行时 flag 不能走 `NODE_OPTIONS`(白名单拒绝 + job 级 env 炸 actions 自身 node24),必须直挂命令行;②「not allowed in NODE_OPTIONS」是通用拒绝文案、**不代表 flag 存在**——`--experimental-zstd` 从未存在过。zstd 在场/缺席双路径各由在场/缺席环境实跑(skip 显式登记非静默)。**CI 收官(run 35452486131):16/16 全绿**,三平台 195 用例实跑(zstd 在场路径含 ≥3x 压缩比断言 CI 首次实测通过)。
147
151
  - 口径备注:§3.2.M7.2 的 `--session id` 形态以 index(sessionId→file 映射)为底座登记后续增强,export 本迭代以文件路径为入口(与 verify/replay 形态一致);运行时 append 异步索引自动更新登记后续(缺失由 rebuild 兜底,异步可容忍语义自洽);deriveMessages 的 roleConsistencyChecked 在 v1 无消息改写面,登记 N/A。详见 `car-docs/10-内核v1/M7-会话日志与审计收口.md`。
148
152
 
153
+ ## M8 状态
154
+
155
+ - **M8 收口(2026-09-26)**:§3.2.M8 运行时底座(模型接入 A4 + 遥测 A5)收口——WIP 七文件(09-19 产出)经检查点 `93b3938` 入库(kernel 根作用域 effect() 补齐宿主侧 Effect 生命周期 + doctor keychain 通道)后,本迭代交付:**M8-BUG-1 修复**(AL-05 兜底 sawFinish 条件化——成功流零 error chunk,缺失仍显式报错)+ openai-compat SSE/重试/凭据门 8 测试 + chatStep SQ-07 集成 7 测试。全量 **231(228 PASS + 3 能力门控 skip / 0 fail)** + tsc strict 0 错(CI 首跑揪出 93b3938 遗留 strict 错误,`9898563` 修复)+ secrets-scan 97 文件 0 命中(93b3938 夹具命中已修——上次会话 PASS 声明系伪造渲染产物)。**CI 收官(run 36237615992):16/16 全绿**。
156
+ - 口径备注:chatStep 库面已备已测,**runTurn(M4)侧消费接线登记后续**(「已实装」口径不外推);openai-compat 为参考适配器(ACL 接口面已钉死);遥测 OTLP 最小实现(无重试/采样)。详见 `car-docs/10-内核v1/M8-运行时底座收口.md`。
157
+
158
+ ## 1.1 状态(签名收尾最小集,enforce 翻转准备)
159
+
160
+ | `src/load/sign.ts` | 1.1-S1 签名原语:generateSigningKeypair(ed25519 PKCS8 DER / SPKI base64)+ signPluginFile(sidecar `<file>.minisig` 单段 base64)+ resolveTrustRoot(flag > env > pub 文件)——指南 §2 node -e 手工流固化,与 verifier/sigGate 签验同源互验 | 1.1 W2-1 |
161
+ | `src/load/config.ts` | 1.1-S3 配置文件通道:`car.config.json`(cwd 发现 + `--config` 显式;D-11a ①)——键位 `sandbox.unsigned.allow`/`sandbox.sig.enforce`/`sandbox.sig.trustRoot`,手写校验 fail-visible(CAR-E-CONFIG:坏 JSON/未知键/类型错显式拒绝)+ mergeSignatureGate 优先级 flag > env > 配置 > 缺省(env 已定义即显式意见) | 1.1 W2-3、部署设计 §4.5.5 |
162
+ | `src/dx/doctor.ts` +2 | 1.1-S2 doctorSignature 签名就绪行(mode 生效值含配置层 / 信任根可解析性 / 配置文件发现态)+ doctorKeychain 专测补齐(M8 §4.6「已接线未专测」出清) | 1.1 W2-2/W2-5 |
163
+ | `src/cli.ts` | 1.1:`car plugin-sign keygen/sign/verify`(退出码家规 0/1/2;私钥 0600;重复 keygen 拒绝覆盖)+ `--config` 通道接入 run/reload/mcp-serve + **mcp-serve 装载接线**(`--plugin` 可重复 / CAR_PLUGINS env;六阶段门 verify 先于 import();car_load_total/car_unsigned_confirmed 全链进 stderr 快照;装载 FAIL = 启动中止 exit 1 两模式一致,warn 仅豁免缺签) | 1.1 W2-1/W2-4 |
164
+ | `test/s26-signature-closeout.spec.ts` + `test/s27-host-load-wiring.spec.ts` + `test/s18.spec.ts` 边界行 | 1.1 测试:s26 13 test(keygen 签验闭环 / 手工口径字节等价 / CLI spawn 全链退出码 / 配置 fail-visible / 优先级四向 / doctor 三通道 / keychain 平台三态)+ s27 4 test(真进程装载四态:warn 全链 / enforce fail-closed / parse FAIL 不静默 / 配置通道好签名)+ s18 无 --plugin 边界断言 | 1.1-GO |
165
+
166
+ - **1.1 工程收口(2026-09-27)**:S30 §8 工程侧清单签名收尾五子项全部兑现(plugin-sign CLI / doctor 签名行 / 配置文件通道 / mcp-serve 装载接线+采集全链 / keychain 专测)。全量 **249(246 PASS + 3 能力门控 skip / 0 fail)** + tsc strict 0 错 + secrets-scan 97 文件 0 命中(密钥/签名夹具仅运行期临时目录——R-1)。QA 两轮制执行。**CI 收官(run 36304931085):16/16 全绿**。
167
+ - **1.1 发布编排(S6,D-11b ①直发)**:13 门禁本地预演 **11 PASS / 2 DRY-RUN / 0 FAIL**(G-07 逃逸矩阵本机 WSL 实跑 20/20;G-09/G-10 远端口径 DRY-RUN);version 1.1.0(`3d8658e`)已推送 main;dist 六资产就绪(tgz/bundle/SHA256SUMS/SBOM/审计/公钥)。**Release v1.1.0 创建(用户)→ j16 自动 `npm publish --provenance --tag latest` → post-publish REMOTE_CHECK=1 复跑**。
168
+ - **边界口径(防外推)**:mcp-serve 装载接线 = 「装载 + 采集全链」——插件 factory 执行 + bindCore 冲刷注册项真实发生;**宿主会话执行插件工具的执行接线属 W1 登记后续**(sessionTurn 仍为事件批归一化),「已实装」口径不外推。sigstore keyless 主轨 / anthropic 适配器 / W1 runTurn→chatStep 接线均为延后登记项(用户裁决 2026-09-27)。
169
+ - **翻转排期**:**2026-12-25 到期 enforce 缺省值翻转**落 12-25 后续发版(一行缺省值变更 + 决策记录归档,部署设计 §4.5.5 既有承诺);数据前提 T-1 阈值随本迭代宿主采集链落成可观测。规划:`car-docs/10-内核v1/1.1-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.1-签名收口终验报告.md`。
170
+
149
171
  ## 状态
150
172
 
151
- - **版本主线**:S1-S4 48/48 | M2 89/89(v0.2.0)| M3 127/127(v0.3.0)| M4 139/139(**1.0.0-rc.1 已发布**)| M5 156/156(DEC-4 预埋项转正)| M6 179/179(2026-09-15 收口)| M7 全量 195(192 PASS + 3 能力门控 skip,2026-09-19 收口)| **M5-S28 装载签名门 全量 204(201 PASS + 3 skip,2026-09-26)**
173
+ - **版本主线**:S1-S4 48/48 | M2 89/89(v0.2.0)| M3 127/127(v0.3.0)| M4 139/139(**1.0.0-rc.1 已发布**)| M5 156/156(DEC-4 预埋项转正)| M6 179/179(2026-09-15 收口)| M7 全量 195(192 PASS + 3 能力门控 skip,2026-09-19 收口)| **M5-S28 装载签名门 全量 204(201 PASS + 3 skip,2026-09-26)** | **M8 运行时底座 全量 231(228 PASS + 3 skip,2026-09-26 收口,§3.2 八模块全部收口)** | **1.1 签名收尾最小集 全量 249(246 PASS + 3 skip,2026-09-27 工程收口,enforce 翻转准备)**
174
+ - **1.0.0 正式发布(2026-09-26,S29/S30)**:npm `@lqc123qwe/car-runtime@1.0.0` 在架——**dist-tag latest → 1.0.0,rc 保留指向 1.0.0-rc.1**(§1.1 口径);SLSA provenance(Actions OIDC,透明日志在案);GitHub Release v1.0.0 六资产(tgz/bundle/SHA256SUMS/SBOM/审计/公钥);**post-publish 全链 13 PASS / 0 DRY-RUN / 0 FAIL** + 外部验证者等价检查(Release 实下载件 SHA256 MATCH + cosign Verified OK)。**宽限期时钟:2026-09-26 起算,2026-12-25 到期 verifier 默认值 warn→enforce 翻转(计划内变更)**。终验报告:`car-docs/10-内核v1/M5-S30-1.0收口终验报告.md`。发布工程事件:j16 首跑 E403(npm 2FA 策略 vs granular token 无 bypass——清单预设回退路径兑现)→ bypass token re-run 绿;G-09 期望 tag 动态化(`4c84b17`)在真实发布兑现价值。
152
175
  - **1.0.0-rc.1 已发布(2026-09-09)**:npm `@lqc123qwe/car-runtime@1.0.0-rc.1`(rc + latest 双 tag,SLSA provenance 在案)+ GitHub Release 6 制品;发布预演 **13 PASS / 0 DRY-RUN / 0 FAIL**(c762b72)。S22 GO 清单已全绿收口。
153
176
  - **轨道 A 三项全部完成**:E-1 WSL2 逃逸矩阵 **20/20 PASS 零逃逸**(2026-09-07,G-07 转 PASS)| E-3 远端发布通道 **13/13 全门禁 PASS**(2026-09-09,G-08/09/10 转 PASS)| E-2 Windows koffi FFI **四判据双环境全 PASS**(S24/S25/S26,2026-09-09~10;本机非提权 + windows-latest 提权 runner 双绿,CI 门禁已接入)。
154
177
  - **决策已闭环**:**T-1** enforce 阈值定稿 **A=5% / B=2% / C=30 会话** + 裁决**显式延期**(2026-09-09,S20 §5 归档;红线禁止以 warn 静默进 1.0)|**T-4** MLPS-G1 定时导出维持部署方配套(2026-09-09,D-4 追记)|**DEC-2** Windows CI 接入 / **DEC-3** 发布前 token rotate / **DEC-4** M3 预埋项纳入 1.0(均 2026-09-09 裁决)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lqc123qwe/car-runtime",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Composable Agent Runtime (CAR) - microkernel plugin system with sandboxed PTC, append-only hashed session log, and MCP host bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/cli.ts CHANGED
@@ -8,10 +8,11 @@
8
8
  * car session replay <events.jsonl[.zstd]> deriveMessages 投影回放(不依赖模型状态)
9
9
  * car session export <file> --out <dir> [--zstd] 取证包导出(SQ-06:断链中止;落盘读回重验,离线自证)
10
10
  * car session rebuild-index <root> [--db <path>] 从 JSONL 重建 SQLite 会话索引(幂等;断链/坏格式文件入 errors 跳过)
11
- * car doctor 环境自检(Node 版本/沙箱/凭据/连通性)
11
+ * car doctor 环境自检(Node 版本/沙箱/凭据/签名/keychain/连通性)
12
+ * car plugin-sign keygen|sign|verify 插件签名工具(1.1-S1:keygen/sign/verify;离线 minisign 轨)
12
13
  */
13
14
  import { createHash } from 'node:crypto'
14
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
15
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
15
16
  import { dirname, join } from 'node:path'
16
17
  import { probeCapabilities, SandboxExecutor } from './sandbox/sandbox.ts'
17
18
  import { SessionLog, loadSessionLog, type SessionEvent } from './session/log.ts'
@@ -21,13 +22,15 @@ import { exportForensicsBundle, verifyBundle, type ExportMeta } from './session/
21
22
  import { runTurn } from './loop/stop.ts'
22
23
  import { Context } from './kernel/context.ts'
23
24
  import { mountPlugin } from './load/loader.ts'
24
- import { verifyPluginFile, gateDepsFromEnv } from './load/sigGate.ts'
25
- import { doctorCredentials, doctorConnectivity } from './dx/doctor.ts'
25
+ import { verifyPluginFile } from './load/sigGate.ts'
26
+ import { loadCarConfig, mergeSignatureGate } from './load/config.ts'
27
+ import { generateSigningKeypair, signPluginFile, resolveTrustRoot } from './load/sign.ts'
28
+ import { doctorCredentials, doctorConnectivity, doctorKeychain, doctorSignature } from './dx/doctor.ts'
26
29
 
27
30
  const HELP = `用法: car <command> [args]
28
- run <plugin.ts> [--sig-enforce]
31
+ run <plugin.ts> [--sig-enforce] [--config <path>]
29
32
  装配并运行插件(快速上手流;装载前签名门 warn 缺省)
30
- reload <file|dir> [--sig-enforce]
33
+ reload <file|dir> [--sig-enforce] [--config <path>]
31
34
  热重载插件并打印六阶段加载报告(含 verify 签名门)
32
35
  session verify <file> 哈希链完整性校验(撕裂尾显式报告)
33
36
  session replay <file> 会话回放(deriveMessages 投影)
@@ -36,8 +39,16 @@ const HELP = `用法: car <command> [args]
36
39
  session rebuild-index <root> [--db <path>]
37
40
  从 JSONL 重建 SQLite 会话索引(幂等)
38
41
  doctor 环境自检
42
+ plugin-sign keygen [--out <前缀>] [--force]
43
+ 生成 ed25519 签名密钥对(<前缀>.priv PKCS8 DER / <前缀>.pub SPKI base64;缺省前缀 car-release)
44
+ plugin-sign sign <file...> [--key <priv>]
45
+ 对插件文件签名,写 <file>.minisig sidecar(被签物 = 文件裸字节 sha256 hex;改动文件必须重签)
46
+ plugin-sign verify <file...> [--trust-root <spki base64>]
47
+ 验证签名(信任根序:--trust-root > CAR_TRUST_ROOT > ./car-release.pub;缺签/坏签均 FAIL)
39
48
  签名门环境变量:CAR_SIG_ENFORCE=1 切 enforce;CAR_TRUST_ROOT=<ed25519 spki base64>;
40
- CAR_UNSIGNED_ALLOW=1 显式声明豁免(warn 缺签路径计数 confirmed=yes,横幅保留)`
49
+ CAR_UNSIGNED_ALLOW=1 显式声明豁免(warn 缺签路径计数 confirmed=yes,横幅保留)
50
+ 配置文件通道(1.1):cwd car.config.json 或 --config <path>;键位 sandbox.unsigned.allow /
51
+ sandbox.sig.enforce / sandbox.sig.trustRoot;优先级 flag > env > 配置 > 缺省;未知键 fail-visible`
41
52
 
42
53
  // 进程内热重载会话(ReloadManager 持有 epoch 与实例注册表;同进程连续 reload 语义完整)
43
54
  let reloadMgrPromise: Promise<import('./load/report.ts').ReloadManager> | undefined
@@ -46,6 +57,38 @@ function getReloadManager() {
46
57
  return reloadMgrPromise
47
58
  }
48
59
 
60
+ /** 取 flag 的值(--config <path> 等;缺位返回 undefined) */
61
+ function flagValue(argv: string[], name: string): string | undefined {
62
+ const i = argv.indexOf(name)
63
+ return i >= 0 ? argv[i + 1] : undefined
64
+ }
65
+
66
+ /** mcp-serve 插件源收集:--plugin <file|dir> 可重复 + CAR_PLUGINS env(';' 分隔,Windows 安全) */
67
+ function collectPluginSources(argv: string[]): { sources: string[]; error?: string } {
68
+ const sources: string[] = []
69
+ for (let i = 0; i < argv.length; i++) {
70
+ if (argv[i] !== '--plugin') continue
71
+ const v = argv[++i]
72
+ if (!v) return { sources, error: '缺少 --plugin 路径' }
73
+ sources.push(v)
74
+ }
75
+ for (const s of (process.env.CAR_PLUGINS ?? '').split(';')) {
76
+ const t = s.trim()
77
+ if (t) sources.push(t)
78
+ }
79
+ return { sources }
80
+ }
81
+
82
+ /**
83
+ * 装载签名门选项装配(1.1-S3 统一入口):flag > env > car.config.json > 缺省 warn。
84
+ * 配置文件 fail-visible:显式路径不存在 / 坏 JSON / 未登记键 → 返回 error 由调用方中止(exit 2)。
85
+ */
86
+ function resolveSigGate(argv: string[]): { options: import('./load/sigGate.ts').SignatureGateOptions; error?: string } {
87
+ const cfg = loadCarConfig({ explicitPath: flagValue(argv, '--config') })
88
+ if (cfg.error) return { options: {}, error: cfg.error }
89
+ return { options: mergeSignatureGate(process.env, cfg.config, argv.includes('--sig-enforce')) }
90
+ }
91
+
49
92
  async function main(): Promise<number> {
50
93
  const [, , cmd, ...rest] = process.argv
51
94
  switch (cmd) {
@@ -53,10 +96,10 @@ async function main(): Promise<number> {
53
96
  const file = rest[0]
54
97
  if (!file) { console.error('缺少插件文件参数'); return 2 }
55
98
  const t0 = Date.now()
56
- // 环节 0:装载前签名门(M5-S28;--sig-enforce 显式切 enforce,env 通道见 HELP)
57
- const sigOpts = gateDepsFromEnv()
58
- if (rest.includes('--sig-enforce')) sigOpts.mode = 'enforce'
59
- const g = verifyPluginFile(file, sigOpts)
99
+ // 环节 0:装载前签名门(M5-S28 装载接线 + 1.1-S3 配置通道:flag > env > car.config.json > 缺省 warn)
100
+ const sig = resolveSigGate(rest)
101
+ if (sig.error) { console.error(sig.error); return 2 }
102
+ const g = verifyPluginFile(file, sig.options)
60
103
  if (!g.allowed) { console.error(`签名门禁拒绝:${g.error ?? 'CAR-E-SIG: rejected'}`); return 1 }
61
104
  if (g.warning) console.log(`[0/5 签名] ⚠ ${g.warning}`)
62
105
  // 环节 1:装配加载
@@ -104,10 +147,10 @@ async function main(): Promise<number> {
104
147
  if (!target) { console.error('缺少插件文件或目录参数'); return 2 }
105
148
  const { formatLoadReport } = await import('./load/report.ts')
106
149
  const mgr = await getReloadManager()
107
- // 装载签名门透传(M5-S28):--sig-enforce / env 通道,六阶段报告含 verify
108
- const sigOpts = gateDepsFromEnv()
109
- if (rest.includes('--sig-enforce')) sigOpts.mode = 'enforce'
110
- const { report, plugins } = await mgr.reload(target, { signature: sigOpts })
150
+ // 装载签名门透传(M5-S28 + 1.1-S3 配置通道):flag > env > car.config.json,六阶段报告含 verify
151
+ const sig = resolveSigGate(rest)
152
+ if (sig.error) { console.error(sig.error); return 2 }
153
+ const { report, plugins } = await mgr.reload(target, { signature: sig.options })
111
154
  for (const line of formatLoadReport(report)) console.log(line)
112
155
  console.log(`装配插件:${plugins.map(p => `${p.manifest.name}@${p.manifest.version}`).join(', ') || '无'}`)
113
156
  return report.stages.some(s => s.status === 'FAIL') ? 1 : 0
@@ -121,6 +164,12 @@ async function main(): Promise<number> {
121
164
  for (const c of doctorCredentials()) {
122
165
  console.log(`credentials.${c.envVar}: ${c.present ? '已设置(值不打印)' : `未设置 — ${c.hint}`}`)
123
166
  }
167
+ // M8 增强:keychain 通道状态(模型凭据读取面;缺席显式降级不静默)
168
+ const kc = doctorKeychain()
169
+ console.log(`keychain: ${kc.available ? '就绪' : `缺席(显式降级)— ${kc.note}`}`)
170
+ // 1.1-S2 增强:签名就绪行(mode 生效值 / 信任根可解析性 / 配置文件通道发现态;诊断只读)
171
+ const sig = doctorSignature({ cwd: process.cwd(), configPath: flagValue(rest, '--config') })
172
+ console.log(`signature: ${sig.detail}`)
124
173
  // M6 增强:registry 连通性(不可达 = SKIPPED 显式跳过,离线不失败)
125
174
  const conn = await doctorConnectivity()
126
175
  console.log(`connectivity: ${conn.status} — ${conn.detail}${conn.status === 'PASS' ? `(${conn.latencyMs}ms)` : ''}`)
@@ -225,6 +274,11 @@ async function main(): Promise<number> {
225
274
  }
226
275
  case 'mcp-serve': {
227
276
  // E-6 宿主实连入口(M4-S18):CAR-as-MCP-Server——宿主(Claude Code/Codex)经 stdio JSON-RPC 接入
277
+ // 1.1-S4 装载接线:--plugin <file|dir>(可重复)/ CAR_PLUGINS env → 六阶段流水线(verify 门先于
278
+ // import())挂载进 Context + car_load_total/car_unsigned_confirmed 全链计数。
279
+ // 边界(1.1 规划 W2-4,防口径外推):此处是「装载接线 + 采集全链」——插件被真实装配
280
+ // (factory 执行 + bindCore 冲刷注册项),但宿主会话执行插件工具的执行接线属 W1 登记后续
281
+ // (sessionTurn 仍为事件批归一化)。
228
282
  const hostIdx = rest.indexOf('--host')
229
283
  const { HOST_MAPPINGS } = await import('./host/mappings.ts')
230
284
  // 默认宿主取映射表首项(数据驱动,静态架构断言红线:宿主标识只在 mappings 数据文件)
@@ -233,11 +287,43 @@ async function main(): Promise<number> {
233
287
  if (!HOST_MAPPINGS.some(h => h.hostId === hostId)) {
234
288
  console.error(`未知宿主 "${hostId}"(可用:${HOST_MAPPINGS.map(h => h.hostId).join(' | ')})`); return 2
235
289
  }
290
+ // 1.1-S3 配置通道(--config / cwd car.config.json)+ 签名门选项(flag > env > 配置 > 缺省 warn)
291
+ const sig = resolveSigGate(rest)
292
+ if (sig.error) { console.error(sig.error); return 2 }
293
+ const pluginsArg = collectPluginSources(rest)
294
+ if (pluginsArg.error) { console.error(pluginsArg.error); return 2 }
295
+ const { createCounters, assertZeroContent } = await import('./telemetry/metrics.ts')
296
+ const counters = createCounters()
297
+ if (pluginsArg.sources.length) {
298
+ const { loadPlugins, formatLoadReport } = await import('./load/report.ts')
299
+ const { Context } = await import('./kernel/context.ts')
300
+ const ctx = new Context()
301
+ const hostTools: any[] = []
302
+ for (const src of pluginsArg.sources) {
303
+ const result = await loadPlugins({ source: src, signature: { ...sig.options, onCount: (n, l) => counters.onCount(n, l) } })
304
+ if (result.report.stages.some(s => s.status === 'FAIL')) {
305
+ // 加载期显式失败红线(两模式一致):FAIL = 插件未装上,服务不得静默缺插件启动。
306
+ // DEC-1 warn/enforce 之分仅在签名门缺签路径(warn=横幅放行,enforce=verify FAIL 走此分支)
307
+ for (const line of formatLoadReport(result.report)) console.error(line)
308
+ console.error('[car mcp-serve] 装载 FAIL——fail-closed 启动中止(warn 仅豁免缺签,不豁免装载失败)')
309
+ return 1
310
+ }
311
+ for (const w of result.report.warnings) console.error(`[car mcp-serve] ⚠ ${w}`)
312
+ for (const p of result.plugins) {
313
+ ctx.plugin({ name: p.manifest.name, apply: c => {
314
+ p.bindCore({
315
+ registerTool: t => { hostTools.push(t); c.provide('tool:' + t.name, t) },
316
+ getRegisteredTools: () => hostTools,
317
+ })
318
+ } })
319
+ }
320
+ const names = result.plugins.map(p => `${p.manifest.name}@${p.manifest.version}`).join(', ')
321
+ console.error(`[car mcp-serve] plugins loaded: ${names}(tools: ${hostTools.map(t => t.name).join(', ') || '无'};装载已接线,会话内执行接线属 W1 登记后续)`)
322
+ }
323
+ }
236
324
  const { HostGateway } = await import('./host/hostGateway.ts')
237
325
  const { createRuntimeFacade } = await import('./host/facade.ts')
238
326
  const { createStdioServer } = await import('./host/stdio.ts')
239
- const { createCounters, assertZeroContent } = await import('./telemetry/metrics.ts')
240
- const counters = createCounters()
241
327
  const profiles = new Map(HOST_MAPPINGS.map(h => [h.hostId, h]))
242
328
  const facade = createRuntimeFacade({ profiles })
243
329
  const gw = new HostGateway({ facade, audit: e => counters.onCount('car_registry_decision', { source: 'registry' }) })
@@ -249,6 +335,98 @@ async function main(): Promise<number> {
249
335
  console.error(`[car mcp-serve] host=${hostId} requests=${n} snapshot=${JSON.stringify(snap)} zeroContent=${assertZeroContent(snap)}`)
250
336
  return 0
251
337
  }
338
+ case 'plugin-sign': {
339
+ // 1.1-S1 插件签名工具(指南 §2 node -e 手工流固化):keygen / sign / verify
340
+ // 退出码沿用家规:0=成功,1=数据校验失败(签名/验签不通过),2=用法/输入不可用
341
+ const [sub, ...sargs] = rest
342
+ const parseFiles = (argv: string[], keyFlag?: string): { files: string[]; keyPath?: string; trustRoot?: string; error?: string } => {
343
+ const files: string[] = []
344
+ let keyPath: string | undefined
345
+ let trustRoot: string | undefined
346
+ for (let i = 0; i < argv.length; i++) {
347
+ const a = argv[i]!
348
+ if (keyFlag && a === keyFlag) {
349
+ keyPath = argv[++i]
350
+ if (!keyPath) return { files, error: `缺少 ${keyFlag} 路径` }
351
+ } else if (a === '--trust-root') {
352
+ trustRoot = argv[++i]
353
+ if (!trustRoot) return { files, error: '缺少 --trust-root 值' }
354
+ } else if (a.startsWith('--')) {
355
+ return { files, error: `未知选项 ${a}` }
356
+ } else files.push(a)
357
+ }
358
+ return { files, keyPath, trustRoot }
359
+ }
360
+ if (sub === 'keygen') {
361
+ let prefix = 'car-release'
362
+ let force = false
363
+ for (let i = 0; i < sargs.length; i++) {
364
+ const a = sargs[i]!
365
+ if (a === '--out') { prefix = sargs[++i] ?? ''; if (!prefix) { console.error('缺少 --out 前缀'); return 2 } }
366
+ else if (a === '--force') force = true
367
+ else if (a.startsWith('--')) { console.error(`未知选项 ${a}`); return 2 }
368
+ else { console.error(`多余位置参数 ${a}`); return 2 }
369
+ }
370
+ const privPath = `${prefix}.priv`
371
+ const pubPath = `${prefix}.pub`
372
+ if (!force && (existsSync(privPath) || existsSync(pubPath))) {
373
+ console.error(`已存在 ${privPath} / ${pubPath}——拒绝覆盖(--force 显式覆盖)。注意:覆盖 ≠ 轮换,旧公钥仍在用请先按轮换流程处置`)
374
+ return 2
375
+ }
376
+ const kp = generateSigningKeypair()
377
+ writeFileSync(privPath, kp.privateKeyDer, { mode: 0o600 })
378
+ writeFileSync(pubPath, kp.publicKeyBase64)
379
+ console.log(`密钥对已生成(前缀 ${prefix}):`)
380
+ console.log(` 私钥 ${privPath}(PKCS8 DER;离线保管,不入仓库——secrets 红线)`)
381
+ console.log(` 公钥 ${pubPath}(SPKI DER base64)`)
382
+ console.log(`信任根(CAR_TRUST_ROOT 值,或 plugin-sign verify --trust-root):`)
383
+ console.log(kp.publicKeyBase64)
384
+ return 0
385
+ }
386
+ if (sub === 'sign') {
387
+ const p = parseFiles(sargs, '--key')
388
+ if (p.error) { console.error(p.error); return 2 }
389
+ if (!p.files.length) { console.error('缺少插件文件参数'); return 2 }
390
+ const keyPath = p.keyPath ?? 'car-release.priv'
391
+ let priv: Buffer
392
+ try { priv = readFileSync(keyPath) } catch {
393
+ console.error(`私钥不可读:${keyPath}(先 car plugin-sign keygen 生成,或 --key 指定路径)`)
394
+ return 2
395
+ }
396
+ let failed = false
397
+ for (const f of p.files) {
398
+ try {
399
+ const r = signPluginFile(f, priv)
400
+ console.log(`signed ${r.file} fp:${r.manifestHash.slice(0, 12)} → ${r.sidecar}`)
401
+ } catch (e) {
402
+ console.error(`sign FAIL ${f} — ${(e as Error).message}`)
403
+ failed = true
404
+ }
405
+ }
406
+ return failed ? 1 : 0
407
+ }
408
+ if (sub === 'verify') {
409
+ const p = parseFiles(sargs)
410
+ if (p.error) { console.error(p.error); return 2 }
411
+ if (!p.files.length) { console.error('缺少插件文件参数'); return 2 }
412
+ // 信任根解析序(与指南 §4 对齐):--trust-root > CAR_TRUST_ROOT env > ./car-release.pub
413
+ const tr = resolveTrustRoot({ flag: p.trustRoot, env: process.env, pubFilePath: 'car-release.pub' })
414
+ if (!tr.value) {
415
+ console.error('无信任根:--trust-root / CAR_TRUST_ROOT / ./car-release.pub 均未提供(keygen 产出的 .pub 即信任根)')
416
+ return 2
417
+ }
418
+ let failed = false
419
+ for (const f of p.files) {
420
+ // verify 语义固定 enforce:缺签与坏签同为 FAIL(验签命令不做 warn 放行)
421
+ const g = verifyPluginFile(f, { mode: 'enforce', trustRootPublicKey: tr.value })
422
+ if (g.allowed && !g.warning) console.log(`verify: PASS ${f} fp:${g.manifestHash?.slice(0, 12)}(信任根来源 ${tr.source})`)
423
+ else { console.error(`verify: FAIL ${f} — ${g.error ?? g.warning ?? 'rejected'}`); failed = true }
424
+ }
425
+ return failed ? 1 : 0
426
+ }
427
+ console.error('用法: car plugin-sign <keygen|sign|verify> [args](--help 查看全部命令)')
428
+ return 2
429
+ }
252
430
  default:
253
431
  console.log(HELP)
254
432
  return cmd ? 2 : 0
package/src/dx/doctor.ts CHANGED
@@ -4,7 +4,13 @@
4
4
  * 口径:
5
5
  * - 凭据检查只探存在性、绝不打印值(secrets 红线:值入日志即泄露面)
6
6
  * - 连通性检查网络不可达 = 显式 SKIPPED 而非 FAIL——测试环境必须可离线通过(不失败)
7
+ * - 1.1-S2:签名就绪检查行(mode / 信任根 / 配置文件通道发现态)——诊断面只读,
8
+ * 门禁判定仍在 sigGate/verifier;公钥可打印、信任根值本身不打印
7
9
  */
10
+ import { createPublicKey } from 'node:crypto'
11
+ import { CredentialService } from '../runtime-core/credentials.ts'
12
+ import { loadCarConfig, mergeSignatureGate } from '../load/config.ts'
13
+
8
14
  export interface CredentialCheck {
9
15
  name: string
10
16
  envVar: string
@@ -36,6 +42,54 @@ export interface ConnectivityResult {
36
42
  latencyMs: number
37
43
  }
38
44
 
45
+ /** M8 增强:keychain 通道状态(模型凭据读取面,§3.2.M8.2 凭据行)——能力探测口径,缺席显式降级不静默;离线可过 */
46
+ export function doctorKeychain(opts: { platform?: NodeJS.Platform } = {}): { available: boolean; note: string } {
47
+ return new CredentialService({ platform: opts.platform }).channelStatus
48
+ }
49
+
50
+ export interface SignatureCheck {
51
+ /** 生效模式(flag > env > 配置 > 缺省 warn——DEC-1 ②;enforce 缺省翻转 2026-12-25 登记计划内变更) */
52
+ mode: 'warn' | 'enforce'
53
+ /** 信任根通道:absent=未配置(全部插件按 unsigned 处理);valid=在场且可解析;invalid=在场但解析失败(enforce 下拒签风险) */
54
+ trustRoot: 'absent' | 'valid' | 'invalid'
55
+ /** 配置文件通道(1.1-S3):absent=cwd 未发现;found=已发现且校验通过;invalid=发现但解析/校验失败(fail-visible) */
56
+ configFile: 'absent' | 'found' | 'invalid'
57
+ detail: string
58
+ }
59
+
60
+ /** 1.1-S2 签名就绪检查:mode 生效值 + 信任根在场可解析性 + car.config.json 发现态(离线可过;值不打印) */
61
+ export function doctorSignature(opts: { env?: NodeJS.ProcessEnv; cwd?: string; configPath?: string } = {}): SignatureCheck {
62
+ const env = opts.env ?? process.env
63
+ const cfg = loadCarConfig({ explicitPath: opts.configPath, cwd: opts.cwd })
64
+ let trustRoot: SignatureCheck['trustRoot'] = 'absent'
65
+ const raw = env.CAR_TRUST_ROOT || cfg.config.sandbox?.sigTrustRoot
66
+ if (raw) {
67
+ try {
68
+ createPublicKey({ key: Buffer.from(raw, 'base64'), format: 'der', type: 'spki' })
69
+ trustRoot = 'valid'
70
+ } catch {
71
+ trustRoot = 'invalid'
72
+ }
73
+ }
74
+ // 生效模式含配置层(配置文件解析失败时按 env/缺省口径合并,invalid 态在 configFile 显式呈现)
75
+ const { mode } = mergeSignatureGate(env, cfg.error ? {} : cfg.config)
76
+ const configFile: SignatureCheck['configFile'] = cfg.error ? 'invalid' : cfg.path ? 'found' : 'absent'
77
+ const parts = [
78
+ `mode=${mode}${mode === 'warn' ? '(DEC-1 ② 缺省;2026-12-25 到期翻转)' : ''}`,
79
+ trustRoot === 'absent'
80
+ ? '信任根未配置(全部插件按 unsigned 处理;car plugin-sign keygen 产出 .pub 即信任根)'
81
+ : trustRoot === 'valid'
82
+ ? '信任根已配置(ed25519 spki 可解析,值不打印)'
83
+ : '信任根已配置但不可解析(CAR-E-SIG 拒签风险——检查 CAR_TRUST_ROOT / sandbox.sig.trustRoot 是否为 spki base64)',
84
+ configFile === 'absent'
85
+ ? '配置文件未发现(car.config.json)'
86
+ : configFile === 'found'
87
+ ? `配置文件 ${cfg.path}(校验通过)`
88
+ : `配置文件无效(${cfg.error})`,
89
+ ]
90
+ return { mode, trustRoot, configFile, detail: parts.join(';') }
91
+ }
92
+
39
93
  /**
40
94
  * registry 连通性检查:可达 = PASS(含延迟);不可达/显式跳过 = SKIPPED(不失败)。
41
95
  * 默认 registry 取 CAR_REGISTRY_URL,缺省 npmjs。
@@ -50,6 +50,16 @@ export class Context {
50
50
  return s.impl
51
51
  }
52
52
 
53
+ /** 根作用域副作用登记(宿主 SDK 面,§3.2.M8 SDK 行)——与插件 fiber Effect 同纪律:body 即执行,disposer 入回卷清单 */
54
+ private rootDisposables: { label: string; disposer: Disposer }[] = []
55
+
56
+ /** 根作用域 Effect:宿主侧注册(RuntimeCore.registerLlmAdapter 等)入 disposeRuntime 回卷范围 */
57
+ effect(body: () => Disposer | Disposer[], label = 'anonymous'): void {
58
+ const res = body()
59
+ const disposers = typeof res === 'function' ? [res] : [...res]
60
+ for (const d of disposers) this.rootDisposables.push({ label, disposer: d as Disposer })
61
+ }
62
+
53
63
  /** 注册插件:依赖就绪即挂载,否则进入 PENDING 队列 */
54
64
  plugin(def: PluginDef): void {
55
65
  if (this.fibers.has(def.name)) {
@@ -115,6 +125,11 @@ export class Context {
115
125
  async disposeRuntime(opts: { order?: 'strict-topo' | 'concurrent' } = {}): Promise<DisposeReport> {
116
126
  const order = this.topoOrder()
117
127
  const report: DisposeReport = { unloaded: [], order: [], errors: [] }
128
+ // 根作用域(宿主 SDK 注册面,§3.2.M8)先于插件回卷:创建序的逆 = 根最后建、最先撤
129
+ for (const { label, disposer } of [...this.rootDisposables].reverse()) {
130
+ try { await disposer() } catch (error) { report.errors.push({ plugin: '<root>', label, error }) }
131
+ }
132
+ this.rootDisposables = []
118
133
  if ((opts.order ?? 'strict-topo') === 'strict-topo') {
119
134
  // 逐层排空:Kahn 分层为 provider→consumer 方向,卸载需反转(消费者层先于提供者层)
120
135
  for (const layer of [...order.layers].reverse()) {
@@ -0,0 +1,129 @@
1
+ /**
2
+ * 1.1-S3 · 签名配置文件通道(sandbox.* 键位空间;部署设计 §4.5.5 载体落地)
3
+ *
4
+ * 口径:
5
+ * - 载体 = car.config.json(D-11a ①,2026-09-27 裁决:零依赖 / 免代码求值 / fail-visible 简单;
6
+ * 系统设计 §3.2 的 car.config.ts + zod configLoader 完整形态登记后续,届时迁移并保持键位兼容)
7
+ * - 键位空间(嵌套 JSON 渲染点分键名):
8
+ * sandbox.unsigned.allow (部署设计 §4.5.5 冻结键;= CAR_UNSIGNED_ALLOW 等价通道)
9
+ * sandbox.sig.enforce (= CAR_SIG_ENFORCE)
10
+ * sandbox.sig.trustRoot (= CAR_TRUST_ROOT;ed25519 公钥非凭据——CR-05「凭据禁入配置明文」不破)
11
+ * 未登记键 / 类型错 / 坏 JSON 一律 CAR-E-CONFIG 显式拒绝(fail-visible,禁静默忽略——
12
+ * 防拼写漂移导致的「配置写了但不生效」)
13
+ * - 优先级(1.1 规划 §3 S3):flag > env > 配置文件 > 缺省。env 已定义即为显式意见
14
+ * (CAR_SIG_ENFORCE 非 '1' = 显式 warn,可压过配置 enforce;未定义才落到配置层)。
15
+ * 无配置文件 + 无 env 时行为与 1.0 完全一致(warn / 无信任根 / 不豁免)。
16
+ * - 本模块只读配置与合并优先级;门禁判定在 sigGate/verifier。发现序:显式路径 > cwd car.config.json。
17
+ */
18
+ import { existsSync, readFileSync } from 'node:fs'
19
+ import { isAbsolute, join } from 'node:path'
20
+ import type { SignatureGateOptions } from './sigGate.ts'
21
+
22
+ export const CONFIG_FILENAME = 'car.config.json'
23
+
24
+ export interface SandboxSigConfig {
25
+ unsignedAllow?: boolean
26
+ sigEnforce?: boolean
27
+ sigTrustRoot?: string
28
+ }
29
+
30
+ export interface CarConfig {
31
+ sandbox?: SandboxSigConfig
32
+ }
33
+
34
+ export interface ConfigLoadResult {
35
+ /** 实际读取的配置文件路径(cwd 未发现时缺省——未发现是正常态非错误) */
36
+ path?: string
37
+ config: CarConfig
38
+ /** fail-visible:显式路径不存在 / 坏 JSON / 未登记键 / 类型错的原因(非空 = 调用方必须中止) */
39
+ error?: string
40
+ }
41
+
42
+ /** 加载配置:显式路径(必须存在,否则报错)或 cwd 发现(未发现 = 正常态返回空配置) */
43
+ export function loadCarConfig(opts: { explicitPath?: string; cwd?: string } = {}): ConfigLoadResult {
44
+ let path: string | undefined
45
+ if (opts.explicitPath) {
46
+ path = isAbsolute(opts.explicitPath) ? opts.explicitPath : join(opts.cwd ?? process.cwd(), opts.explicitPath)
47
+ if (!existsSync(path)) {
48
+ return { path, config: {}, error: `CAR-E-CONFIG: 配置文件不存在:${path}(--config 显式指定的路径必须存在)` }
49
+ }
50
+ } else {
51
+ path = join(opts.cwd ?? process.cwd(), CONFIG_FILENAME)
52
+ if (!existsSync(path)) return { config: {} }
53
+ }
54
+ let raw: unknown
55
+ try {
56
+ raw = JSON.parse(readFileSync(path, 'utf-8'))
57
+ } catch (e) {
58
+ return { path, config: {}, error: `CAR-E-CONFIG: 配置文件解析失败(${(e as Error).message})——坏 JSON 显式拒绝,不静默忽略` }
59
+ }
60
+ const v = validate(raw)
61
+ return v.error ? { path, config: {}, error: v.error } : { path, config: v.config }
62
+ }
63
+
64
+ /** 手写校验(fail-visible;零依赖红线——不引 zod,D-11a ①) */
65
+ function validate(raw: unknown): { config: CarConfig; error?: string } {
66
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
67
+ return { config: {}, error: 'CAR-E-CONFIG: 配置顶层必须为 JSON 对象' }
68
+ }
69
+ const out: CarConfig = {}
70
+ for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
71
+ if (k !== 'sandbox') {
72
+ return { config: {}, error: `CAR-E-CONFIG: 未知顶层键 "${k}"(1.1 登记键位空间仅 sandbox.*)` }
73
+ }
74
+ if (typeof v !== 'object' || v === null || Array.isArray(v)) {
75
+ return { config: {}, error: 'CAR-E-CONFIG: sandbox 必须为对象' }
76
+ }
77
+ const sb: SandboxSigConfig = {}
78
+ for (const [sk, sv] of Object.entries(v as Record<string, unknown>)) {
79
+ if (sk === 'unsigned') {
80
+ if (typeof sv !== 'object' || sv === null || Array.isArray(sv)) return { config: {}, error: 'CAR-E-CONFIG: sandbox.unsigned 必须为对象' }
81
+ for (const [uk, uv] of Object.entries(sv as Record<string, unknown>)) {
82
+ if (uk !== 'allow') return { config: {}, error: `CAR-E-CONFIG: 未知键 sandbox.unsigned.${uk}(登记键位:sandbox.unsigned.allow)` }
83
+ if (typeof uv !== 'boolean') return { config: {}, error: 'CAR-E-CONFIG: sandbox.unsigned.allow 必须为布尔' }
84
+ sb.unsignedAllow = uv
85
+ }
86
+ } else if (sk === 'sig') {
87
+ if (typeof sv !== 'object' || sv === null || Array.isArray(sv)) return { config: {}, error: 'CAR-E-CONFIG: sandbox.sig 必须为对象' }
88
+ for (const [gk, gv] of Object.entries(sv as Record<string, unknown>)) {
89
+ if (gk === 'enforce') {
90
+ if (typeof gv !== 'boolean') return { config: {}, error: 'CAR-E-CONFIG: sandbox.sig.enforce 必须为布尔' }
91
+ sb.sigEnforce = gv
92
+ } else if (gk === 'trustRoot') {
93
+ if (typeof gv !== 'string' || !gv.trim()) return { config: {}, error: 'CAR-E-CONFIG: sandbox.sig.trustRoot 必须为非空字符串(ed25519 spki base64)' }
94
+ sb.sigTrustRoot = gv.trim()
95
+ } else {
96
+ return { config: {}, error: `CAR-E-CONFIG: 未知键 sandbox.sig.${gk}(登记键位:sandbox.sig.enforce / sandbox.sig.trustRoot)` }
97
+ }
98
+ }
99
+ } else {
100
+ return { config: {}, error: `CAR-E-CONFIG: 未知 sandbox 键 "${sk}"(1.1 登记键位:sandbox.unsigned.* / sandbox.sig.*)` }
101
+ }
102
+ }
103
+ out.sandbox = sb
104
+ }
105
+ return { config: out }
106
+ }
107
+
108
+ /** 合并产出(mode/unsignedAllow 恒有值——比 SignatureGateOptions 的可选字段更窄,doctor 等消费方免缺省分支) */
109
+ export interface ResolvedSignatureGate {
110
+ mode: 'warn' | 'enforce'
111
+ trustRootPublicKey?: string
112
+ unsignedAllow: boolean
113
+ }
114
+
115
+ /**
116
+ * 优先级合并(flag > env > 配置 > 缺省):产出装载签名门选项。
117
+ * flagEnforce = CLI --sig-enforce;env 已定义即为显式意见(压过配置),未定义落配置层。
118
+ */
119
+ export function mergeSignatureGate(env: NodeJS.ProcessEnv, cfg: CarConfig, flagEnforce?: boolean): ResolvedSignatureGate {
120
+ // cfg 为校验器产出的扁平形状(sandbox.{unsignedAllow,sigEnforce,sigTrustRoot},JSON 嵌套键已拍平)
121
+ let mode: 'warn' | 'enforce' = cfg.sandbox?.sigEnforce === true ? 'enforce' : 'warn'
122
+ if (env.CAR_SIG_ENFORCE !== undefined) mode = env.CAR_SIG_ENFORCE === '1' ? 'enforce' : 'warn'
123
+ if (flagEnforce) mode = 'enforce'
124
+ let trustRootPublicKey = cfg.sandbox?.sigTrustRoot
125
+ if (env.CAR_TRUST_ROOT) trustRootPublicKey = env.CAR_TRUST_ROOT
126
+ let unsignedAllow = cfg.sandbox?.unsignedAllow === true
127
+ if (env.CAR_UNSIGNED_ALLOW !== undefined) unsignedAllow = env.CAR_UNSIGNED_ALLOW === '1'
128
+ return { mode, trustRootPublicKey: trustRootPublicKey || undefined, unsignedAllow }
129
+ }