@lqc123qwe/car-runtime 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -1
- package/package.json +1 -1
- package/scripts/release-pipeline.ts +8 -2
- package/src/cli.ts +235 -22
- package/src/dx/doctor.ts +54 -0
- package/src/kernel/context.ts +15 -0
- package/src/load/config.ts +129 -0
- package/src/load/sign.ts +60 -0
- package/src/runtime-core/chatStep.ts +121 -0
- package/src/runtime-core/credentials.ts +135 -0
- package/src/runtime-core/errors.ts +47 -0
- package/src/runtime-core/llm.ts +304 -0
- package/src/runtime-core/redaction.ts +94 -0
- package/src/runtime-core/telemetry.ts +333 -0
- package/src/runtime-core/types.ts +79 -0
- package/src/session/indexStore.ts +178 -3
- package/test/s18.spec.ts +9 -0
- package/test/s25-m8-runtime-core.spec.ts +600 -0
- package/test/s26-signature-closeout.spec.ts +349 -0
- package/test/s27-host-load-wiring.spec.ts +134 -0
- package/test/s28-session-index-followup.spec.ts +236 -0
- package/test/s29-otel-producer.spec.ts +252 -0
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,40 @@ 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
|
+
- **1.1.0 正式发布(2026-09-27,S6 收官)**:npm `@lqc123qwe/car-runtime@1.1.0` 在架——**dist-tag latest → 1.1.0,rc 保留指向 1.0.0-rc.1**(§1.1 口径);SLSA provenance(slsa.dev/provenance/v1)。j16 run 36305729712 success;**post-publish 复跑 12 PASS / 1 DRY-RUN / 0 FAIL(G-09 实查 PASS)** + 外部验证者等价检查(Release 实下载件 SHA256 MATCH + cosign Verified OK)+ Release 六资产公开 API 实查齐备。发布工程事件:tag 实挂 `1.1.0`(无 v 前缀)→ G-10 双形态兼容修复(`3fdedf5`,4c84b17 同类)。收口报告:`car-docs/10-内核v1/1.1-签名收口终验报告.md`。
|
|
169
|
+
- **边界口径(防外推)**:mcp-serve 装载接线 = 「装载 + 采集全链」——插件 factory 执行 + bindCore 冲刷注册项真实发生;**宿主会话执行插件工具的执行接线属 W1 登记后续**(sessionTurn 仍为事件批归一化),「已实装」口径不外推。sigstore keyless 主轨 / anthropic 适配器 / W1 runTurn→chatStep 接线均为延后登记项(用户裁决 2026-09-27)。
|
|
170
|
+
- **翻转排期**:**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`。
|
|
171
|
+
|
|
172
|
+
## 1.2 状态(会话/遥测小项 W3)
|
|
173
|
+
|
|
174
|
+
| `src/session/indexStore.ts` +2 | 1.2-S1/S2:`lookupSession`(--session id → 文件;db 缺文件 CAR-E-INDEX 显式报错无建文件副作用;SQLite 惰性校验错误统一收口;未收录 = null「可索引 = 可验证」)+ `upsertSessionRow`(自举建库建表)+ `IndexUpdater`(**按行增量**——sink 先于事件入内存 fail-fast 时序下行自带 seq/hash;debounce 2s + close flush;**失败非致命**绝不阻塞 append;能力缺席显式登记一次后停用;file resolve 绝对化) | M7 §4/§5 登记后续出清 |
|
|
175
|
+
| `src/cli.ts` | 1.2:session verify/replay/export 三入口 `--session <id> [--db]`(与位置路径互斥显式拒绝;缺省 db = cwd sessions-index.db)+ `car run` sink 链接 IndexUpdater + mcp-serve **OTel meter 双写桥**(S17 counters 快照面不变 + shutdown flush) | 1.2 W3-1/2/3 |
|
|
176
|
+
| `src/runtime-core/telemetry.ts` | 1.2-S3 生产级策略:有界重试(429/5xx/网络 ≤2 退避 1s/2s,4xx 不重试,耗尽静默丢弃永不抛错——**D-12b 口径修订「0 重试」**,隐私三原则不变)+ 采样(always_on 缺省/always_off/{ratio} head 决策,spansSampledOut)+ 批上限(maxBatchSize 512 分批 / maxQueueSize 2048 溢出丢最旧 queueOverflows)+ `telemetryConfigFromEnv`(CAR_OTEL_ENDPOINT 唯一开关/CAR_OTEL_SAMPLING/CAR_OTEL_INTERVAL_MS;非法值保持缺省禁 fail-hard)+ **1.2-BUG-1/2 修复**(OTLP metric 名混入 labels JSON / res.ok 未判 5xx 计成功——均经生产调用点接入暴露) | M8 §4.4 登记后续出清 |
|
|
177
|
+
| `test/s28-session-index-followup.spec.ts` + `test/s29-otel-producer.spec.ts` | 1.2 测试:s28 6 test(lookup 四态 / CLI 三入口真进程 / 拒绝面四情形 / updater 自举+覆盖+close / 失败非致命 / car run 索引闭环)+ s29 8 test(env 七态 / 重试四路径 / res.ok 回归钉 / 采样四态 / 分批溢出 / 默认关 / 解耦静态断言 / **mcp-serve 真进程 OTLP 出站实证**) | 1.2-GO |
|
|
178
|
+
|
|
179
|
+
- **1.2 工程收口(2026-09-27)**:M7 §4/§5 + M8 §4.4 登记后续三子项全部出清。全量 **263(260 PASS + 3 能力门控 skip / 0 fail)** + tsc strict 0 错 + secrets-scan 101 文件 0 命中。QA 两轮制执行。**1.2-BUG-1(OTLP metric 名混 labels)为生产调用点核查直接产出**——meter 桥接入即暴露 M8 最小实现掩盖的缺陷。
|
|
180
|
+
- **边界口径(防外推)**:OTel span 面(turn/step span)**仍无生产调用点**(随 W1 登记后续);trace 传播(parentSpanId/共享 trace)随 W1 设计;mcp-serve 出站现仅 metrics(counter 桥)。
|
|
181
|
+
- **遗留登记**:W1 = 1.3 头号候选(含 D-12a TLS loopback 豁免实装、car run 装配面重做〔demo_tool 撞名观察项出清〕);car run demo_tool 撞名 DEPCYCLE 为预存在边缘(登记不修,W1 出清)。规划:`car-docs/10-内核v1/1.2-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.2-收口终验报告.md`。
|
|
182
|
+
|
|
149
183
|
## 状态
|
|
150
184
|
|
|
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)**
|
|
185
|
+
- **版本主线**: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 收口,1.1.0 已发布,enforce 翻转准备就绪)** | **1.2 会话/遥测小项 全量 263(260 PASS + 3 skip,2026-09-27 收口,1.2.0 发布编排中)**
|
|
186
|
+
- **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
187
|
- **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
188
|
- **轨道 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
189
|
- **决策已闭环**:**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.
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -127,8 +127,14 @@ const record = (stage: string, gate: string, ok: boolean, detail = '', dryRun =
|
|
|
127
127
|
if (process.env.GH_TOKEN && process.env.REMOTE_CHECK === '1') {
|
|
128
128
|
try {
|
|
129
129
|
const ghBin = 'D:/WorkBuddy/agent/tools/bin/gh.exe'
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
// tag 形态双兼容:v 前缀(v1.0.0 惯例)与无前缀(1.1.0 实挂形态)都查,并集去重——
|
|
131
|
+
// 硬编码单形态会把真实归档误判 FAIL(4c84b17 G-09 期望 tag 动态化同类教训,2026-09-27)
|
|
132
|
+
const fetchAssets = (tag: string): string[] => {
|
|
133
|
+
try {
|
|
134
|
+
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)
|
|
135
|
+
} catch { return [] }
|
|
136
|
+
}
|
|
137
|
+
const assets = [...new Set([...fetchAssets(`v${VERSION}`), ...fetchAssets(VERSION)])]
|
|
132
138
|
const need = [`car-runtime-${VERSION}.tgz`, `car-runtime-${VERSION}.tgz.sig.bundle`, 'SHA256SUMS', 'sbom.json', 'release-audit.jsonl', 'car-release.pub']
|
|
133
139
|
const missing = need.filter(n => !assets.includes(n))
|
|
134
140
|
g10Ok = missing.length === 0
|
package/src/cli.ts
CHANGED
|
@@ -8,26 +8,32 @@
|
|
|
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'
|
|
18
19
|
import { SessionFileStore } from './session/store.ts'
|
|
19
20
|
import { compressJsonlZstd, decodeLogBuffer, zstdAvailable } from './session/format.ts'
|
|
20
21
|
import { exportForensicsBundle, verifyBundle, type ExportMeta } from './session/export.ts'
|
|
22
|
+
import { lookupSession, IndexUpdater, type SessionIndexRow } from './session/indexStore.ts'
|
|
21
23
|
import { runTurn } from './loop/stop.ts'
|
|
22
24
|
import { Context } from './kernel/context.ts'
|
|
23
25
|
import { mountPlugin } from './load/loader.ts'
|
|
24
|
-
import { verifyPluginFile
|
|
25
|
-
import {
|
|
26
|
+
import { verifyPluginFile } from './load/sigGate.ts'
|
|
27
|
+
import { loadCarConfig, mergeSignatureGate } from './load/config.ts'
|
|
28
|
+
import { generateSigningKeypair, signPluginFile, resolveTrustRoot } from './load/sign.ts'
|
|
29
|
+
import { doctorCredentials, doctorConnectivity, doctorKeychain, doctorSignature } from './dx/doctor.ts'
|
|
30
|
+
import { createTelemetryFacade, telemetryConfigFromEnv } from './runtime-core/telemetry.ts'
|
|
31
|
+
import type { CounterName, AllowedLabels } from './telemetry/metrics.ts'
|
|
26
32
|
|
|
27
33
|
const HELP = `用法: car <command> [args]
|
|
28
|
-
run <plugin.ts> [--sig-enforce]
|
|
34
|
+
run <plugin.ts> [--sig-enforce] [--config <path>]
|
|
29
35
|
装配并运行插件(快速上手流;装载前签名门 warn 缺省)
|
|
30
|
-
reload <file|dir> [--sig-enforce]
|
|
36
|
+
reload <file|dir> [--sig-enforce] [--config <path>]
|
|
31
37
|
热重载插件并打印六阶段加载报告(含 verify 签名门)
|
|
32
38
|
session verify <file> 哈希链完整性校验(撕裂尾显式报告)
|
|
33
39
|
session replay <file> 会话回放(deriveMessages 投影)
|
|
@@ -35,9 +41,19 @@ const HELP = `用法: car <command> [args]
|
|
|
35
41
|
取证包导出(断链中止;落盘读回重验,离线自证)
|
|
36
42
|
session rebuild-index <root> [--db <path>]
|
|
37
43
|
从 JSONL 重建 SQLite 会话索引(幂等)
|
|
44
|
+
session verify/replay/export 均支持 --session <id> [--db <path>]
|
|
45
|
+
经 SQLite 索引定位会话(1.2;缺省 db = cwd sessions-index.db;索引未收录 = 显式报错)
|
|
38
46
|
doctor 环境自检
|
|
47
|
+
plugin-sign keygen [--out <前缀>] [--force]
|
|
48
|
+
生成 ed25519 签名密钥对(<前缀>.priv PKCS8 DER / <前缀>.pub SPKI base64;缺省前缀 car-release)
|
|
49
|
+
plugin-sign sign <file...> [--key <priv>]
|
|
50
|
+
对插件文件签名,写 <file>.minisig sidecar(被签物 = 文件裸字节 sha256 hex;改动文件必须重签)
|
|
51
|
+
plugin-sign verify <file...> [--trust-root <spki base64>]
|
|
52
|
+
验证签名(信任根序:--trust-root > CAR_TRUST_ROOT > ./car-release.pub;缺签/坏签均 FAIL)
|
|
39
53
|
签名门环境变量:CAR_SIG_ENFORCE=1 切 enforce;CAR_TRUST_ROOT=<ed25519 spki base64>;
|
|
40
|
-
CAR_UNSIGNED_ALLOW=1 显式声明豁免(warn 缺签路径计数 confirmed=yes
|
|
54
|
+
CAR_UNSIGNED_ALLOW=1 显式声明豁免(warn 缺签路径计数 confirmed=yes,横幅保留)
|
|
55
|
+
配置文件通道(1.1):cwd car.config.json 或 --config <path>;键位 sandbox.unsigned.allow /
|
|
56
|
+
sandbox.sig.enforce / sandbox.sig.trustRoot;优先级 flag > env > 配置 > 缺省;未知键 fail-visible`
|
|
41
57
|
|
|
42
58
|
// 进程内热重载会话(ReloadManager 持有 epoch 与实例注册表;同进程连续 reload 语义完整)
|
|
43
59
|
let reloadMgrPromise: Promise<import('./load/report.ts').ReloadManager> | undefined
|
|
@@ -46,6 +62,38 @@ function getReloadManager() {
|
|
|
46
62
|
return reloadMgrPromise
|
|
47
63
|
}
|
|
48
64
|
|
|
65
|
+
/** 取 flag 的值(--config <path> 等;缺位返回 undefined) */
|
|
66
|
+
function flagValue(argv: string[], name: string): string | undefined {
|
|
67
|
+
const i = argv.indexOf(name)
|
|
68
|
+
return i >= 0 ? argv[i + 1] : undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** mcp-serve 插件源收集:--plugin <file|dir> 可重复 + CAR_PLUGINS env(';' 分隔,Windows 安全) */
|
|
72
|
+
function collectPluginSources(argv: string[]): { sources: string[]; error?: string } {
|
|
73
|
+
const sources: string[] = []
|
|
74
|
+
for (let i = 0; i < argv.length; i++) {
|
|
75
|
+
if (argv[i] !== '--plugin') continue
|
|
76
|
+
const v = argv[++i]
|
|
77
|
+
if (!v) return { sources, error: '缺少 --plugin 路径' }
|
|
78
|
+
sources.push(v)
|
|
79
|
+
}
|
|
80
|
+
for (const s of (process.env.CAR_PLUGINS ?? '').split(';')) {
|
|
81
|
+
const t = s.trim()
|
|
82
|
+
if (t) sources.push(t)
|
|
83
|
+
}
|
|
84
|
+
return { sources }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 装载签名门选项装配(1.1-S3 统一入口):flag > env > car.config.json > 缺省 warn。
|
|
89
|
+
* 配置文件 fail-visible:显式路径不存在 / 坏 JSON / 未登记键 → 返回 error 由调用方中止(exit 2)。
|
|
90
|
+
*/
|
|
91
|
+
function resolveSigGate(argv: string[]): { options: import('./load/sigGate.ts').SignatureGateOptions; error?: string } {
|
|
92
|
+
const cfg = loadCarConfig({ explicitPath: flagValue(argv, '--config') })
|
|
93
|
+
if (cfg.error) return { options: {}, error: cfg.error }
|
|
94
|
+
return { options: mergeSignatureGate(process.env, cfg.config, argv.includes('--sig-enforce')) }
|
|
95
|
+
}
|
|
96
|
+
|
|
49
97
|
async function main(): Promise<number> {
|
|
50
98
|
const [, , cmd, ...rest] = process.argv
|
|
51
99
|
switch (cmd) {
|
|
@@ -53,10 +101,10 @@ async function main(): Promise<number> {
|
|
|
53
101
|
const file = rest[0]
|
|
54
102
|
if (!file) { console.error('缺少插件文件参数'); return 2 }
|
|
55
103
|
const t0 = Date.now()
|
|
56
|
-
// 环节 0:装载前签名门(M5-S28
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
const g = verifyPluginFile(file,
|
|
104
|
+
// 环节 0:装载前签名门(M5-S28 装载接线 + 1.1-S3 配置通道:flag > env > car.config.json > 缺省 warn)
|
|
105
|
+
const sig = resolveSigGate(rest)
|
|
106
|
+
if (sig.error) { console.error(sig.error); return 2 }
|
|
107
|
+
const g = verifyPluginFile(file, sig.options)
|
|
60
108
|
if (!g.allowed) { console.error(`签名门禁拒绝:${g.error ?? 'CAR-E-SIG: rejected'}`); return 1 }
|
|
61
109
|
if (g.warning) console.log(`[0/5 签名] ⚠ ${g.warning}`)
|
|
62
110
|
// 环节 1:装配加载
|
|
@@ -66,7 +114,10 @@ async function main(): Promise<number> {
|
|
|
66
114
|
const log = new SessionLog('S-' + Date.now().toString(36))
|
|
67
115
|
// §3.2.M7.5「先落日志后放行」:run 流全程逐事件 fsync 落盘(append 返回 = 已过掉电窗口)
|
|
68
116
|
const store = new SessionFileStore(`session-${log.sessionId}.jsonl`)
|
|
69
|
-
|
|
117
|
+
// 1.2-S2 W3-2「更新半边」:append 后按行增量入索引(sink 先于事件入内存——行自带 seq/hash;
|
|
118
|
+
// 失败非致命——索引为派生缓存,rebuild-index 兜底语义不变;SQLite 能力缺席 = 显式登记一次后停用)
|
|
119
|
+
const indexUpdater = new IndexUpdater({ dbPath: join(process.cwd(), 'sessions-index.db'), onError: e => console.error(`[index-updater] ${e.message}`) })
|
|
120
|
+
log.attachSink(line => { store.append(line); indexUpdater.record(log.sessionId, line, store.path) })
|
|
70
121
|
const hostTools: any[] = []
|
|
71
122
|
ctx.plugin({ name: plugin.manifest.name, apply: (c) => {
|
|
72
123
|
plugin.bindCore({
|
|
@@ -90,8 +141,9 @@ async function main(): Promise<number> {
|
|
|
90
141
|
model: async () => ({ stopReason: 'toolUse' as const, toolCalls: [{ id: 'c1', tool: 'demo_tool', args: {} }] }),
|
|
91
142
|
})
|
|
92
143
|
console.log(`[3/5 收口] turnEnd=${r.reason}(steps=${r.steps})`)
|
|
93
|
-
// 环节 4:落盘收口(全程逐事件 fsync
|
|
144
|
+
// 环节 4:落盘收口(全程逐事件 fsync 已发生,此处仅关句柄核对 + 索引 pending 落盘)
|
|
94
145
|
store.close()
|
|
146
|
+
await indexUpdater.close()
|
|
95
147
|
console.log(`[4/5 落盘] ${store.path}(${log.events.length} 事件逐事件 fsync,哈希链 ${log.verifyChain() === null ? '完整' : '断链!'})`)
|
|
96
148
|
// 环节 5:审计回放
|
|
97
149
|
const replay = log.deriveMessages()
|
|
@@ -104,10 +156,10 @@ async function main(): Promise<number> {
|
|
|
104
156
|
if (!target) { console.error('缺少插件文件或目录参数'); return 2 }
|
|
105
157
|
const { formatLoadReport } = await import('./load/report.ts')
|
|
106
158
|
const mgr = await getReloadManager()
|
|
107
|
-
// 装载签名门透传(M5-S28
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
const { report, plugins } = await mgr.reload(target, { signature:
|
|
159
|
+
// 装载签名门透传(M5-S28 + 1.1-S3 配置通道):flag > env > car.config.json,六阶段报告含 verify
|
|
160
|
+
const sig = resolveSigGate(rest)
|
|
161
|
+
if (sig.error) { console.error(sig.error); return 2 }
|
|
162
|
+
const { report, plugins } = await mgr.reload(target, { signature: sig.options })
|
|
111
163
|
for (const line of formatLoadReport(report)) console.log(line)
|
|
112
164
|
console.log(`装配插件:${plugins.map(p => `${p.manifest.name}@${p.manifest.version}`).join(', ') || '无'}`)
|
|
113
165
|
return report.stages.some(s => s.status === 'FAIL') ? 1 : 0
|
|
@@ -121,6 +173,12 @@ async function main(): Promise<number> {
|
|
|
121
173
|
for (const c of doctorCredentials()) {
|
|
122
174
|
console.log(`credentials.${c.envVar}: ${c.present ? '已设置(值不打印)' : `未设置 — ${c.hint}`}`)
|
|
123
175
|
}
|
|
176
|
+
// M8 增强:keychain 通道状态(模型凭据读取面;缺席显式降级不静默)
|
|
177
|
+
const kc = doctorKeychain()
|
|
178
|
+
console.log(`keychain: ${kc.available ? '就绪' : `缺席(显式降级)— ${kc.note}`}`)
|
|
179
|
+
// 1.1-S2 增强:签名就绪行(mode 生效值 / 信任根可解析性 / 配置文件通道发现态;诊断只读)
|
|
180
|
+
const sig = doctorSignature({ cwd: process.cwd(), configPath: flagValue(rest, '--config') })
|
|
181
|
+
console.log(`signature: ${sig.detail}`)
|
|
124
182
|
// M6 增强:registry 连通性(不可达 = SKIPPED 显式跳过,离线不失败)
|
|
125
183
|
const conn = await doctorConnectivity()
|
|
126
184
|
console.log(`connectivity: ${conn.status} — ${conn.detail}${conn.status === 'PASS' ? `(${conn.latencyMs}ms)` : ''}`)
|
|
@@ -133,7 +191,7 @@ async function main(): Promise<number> {
|
|
|
133
191
|
for (let i = 0; i < args.length; i++) {
|
|
134
192
|
const a = args[i]!
|
|
135
193
|
if (a === '--zstd') flags.zstd = true
|
|
136
|
-
else if (a === '--out' || a === '--db') flags[a.slice(2)] = args[++i] ?? ''
|
|
194
|
+
else if (a === '--out' || a === '--db' || a === '--session') flags[a.slice(2)] = args[++i] ?? ''
|
|
137
195
|
else positional.push(a)
|
|
138
196
|
}
|
|
139
197
|
// C-02 索引重建(§3.2.M7.5 Step 3「异步可容忍,丢失可 rebuild」;幂等单事务)
|
|
@@ -151,7 +209,20 @@ async function main(): Promise<number> {
|
|
|
151
209
|
return 0
|
|
152
210
|
} catch (e) { console.error((e as Error).message); return 2 }
|
|
153
211
|
}
|
|
154
|
-
|
|
212
|
+
// 1.2-S1 W3-1:--session <id> 经索引定位会话文件(与位置路径互斥,显式拒绝歧义)。
|
|
213
|
+
// db 缺省取 cwd sessions-index.db(缺文件 = CAR-E-INDEX 显式报错提示 rebuild-index);
|
|
214
|
+
// 索引未收录 = 显式报错(「可索引 = 可验证」:断链/坏格式会话不入索引)。
|
|
215
|
+
const sessionFlag = flags.session as string | undefined
|
|
216
|
+
let file = positional[0]
|
|
217
|
+
if (sessionFlag) {
|
|
218
|
+
if (file) { console.error('文件路径与 --session 互斥——请二选一'); return 2 }
|
|
219
|
+
const dbPath = (flags.db as string) || join(process.cwd(), 'sessions-index.db')
|
|
220
|
+
let row: SessionIndexRow | null
|
|
221
|
+
try { row = await lookupSession(dbPath, sessionFlag) } catch (e) { console.error((e as Error).message); return 2 }
|
|
222
|
+
if (!row) { console.error(`会话 "${sessionFlag}" 未收录索引(可索引 = 可验证;断链/坏格式会话不入索引)——可先 car session rebuild-index`); return 2 }
|
|
223
|
+
file = row.file
|
|
224
|
+
console.log(`[index] ${sessionFlag} → ${file}(events=${row.eventCount} encoding=${row.encoding})`)
|
|
225
|
+
}
|
|
155
226
|
if (!file) { console.error('缺少文件参数'); return 2 }
|
|
156
227
|
if (sub === 'export' && !flags.out) { console.error('缺少 --out 目录参数'); return 2 }
|
|
157
228
|
let loaded: ReturnType<typeof loadSessionLog>
|
|
@@ -225,6 +296,11 @@ async function main(): Promise<number> {
|
|
|
225
296
|
}
|
|
226
297
|
case 'mcp-serve': {
|
|
227
298
|
// E-6 宿主实连入口(M4-S18):CAR-as-MCP-Server——宿主(Claude Code/Codex)经 stdio JSON-RPC 接入
|
|
299
|
+
// 1.1-S4 装载接线:--plugin <file|dir>(可重复)/ CAR_PLUGINS env → 六阶段流水线(verify 门先于
|
|
300
|
+
// import())挂载进 Context + car_load_total/car_unsigned_confirmed 全链计数。
|
|
301
|
+
// 边界(1.1 规划 W2-4,防口径外推):此处是「装载接线 + 采集全链」——插件被真实装配
|
|
302
|
+
// (factory 执行 + bindCore 冲刷注册项),但宿主会话执行插件工具的执行接线属 W1 登记后续
|
|
303
|
+
// (sessionTurn 仍为事件批归一化)。
|
|
228
304
|
const hostIdx = rest.indexOf('--host')
|
|
229
305
|
const { HOST_MAPPINGS } = await import('./host/mappings.ts')
|
|
230
306
|
// 默认宿主取映射表首项(数据驱动,静态架构断言红线:宿主标识只在 mappings 数据文件)
|
|
@@ -233,22 +309,159 @@ async function main(): Promise<number> {
|
|
|
233
309
|
if (!HOST_MAPPINGS.some(h => h.hostId === hostId)) {
|
|
234
310
|
console.error(`未知宿主 "${hostId}"(可用:${HOST_MAPPINGS.map(h => h.hostId).join(' | ')})`); return 2
|
|
235
311
|
}
|
|
312
|
+
// 1.1-S3 配置通道(--config / cwd car.config.json)+ 签名门选项(flag > env > 配置 > 缺省 warn)
|
|
313
|
+
const sig = resolveSigGate(rest)
|
|
314
|
+
if (sig.error) { console.error(sig.error); return 2 }
|
|
315
|
+
const pluginsArg = collectPluginSources(rest)
|
|
316
|
+
if (pluginsArg.error) { console.error(pluginsArg.error); return 2 }
|
|
317
|
+
const { createCounters, assertZeroContent } = await import('./telemetry/metrics.ts')
|
|
318
|
+
// 1.2-S3 W3-3:OTel 门面经 env 通道(CAR_OTEL_ENDPOINT 唯一开关;缺省关 = noop 零出站)+
|
|
319
|
+
// S17 counters **双写桥**——快照面(登记表兜底通道)不变,meter 出站为加法通道;
|
|
320
|
+
// labels 为零内容枚举(metrics.ts AllowedLabels),出站同口径
|
|
321
|
+
const otel = createTelemetryFacade(telemetryConfigFromEnv())
|
|
322
|
+
const counters = createCounters()
|
|
323
|
+
const onCount = (name: CounterName, labels?: AllowedLabels): void => {
|
|
324
|
+
counters.onCount(name, labels)
|
|
325
|
+
if (otel.enabled) otel.getMeter().createCounter(name).add(1, labels as Record<string, string> | undefined)
|
|
326
|
+
}
|
|
327
|
+
if (pluginsArg.sources.length) {
|
|
328
|
+
const { loadPlugins, formatLoadReport } = await import('./load/report.ts')
|
|
329
|
+
const { Context } = await import('./kernel/context.ts')
|
|
330
|
+
const ctx = new Context()
|
|
331
|
+
const hostTools: any[] = []
|
|
332
|
+
for (const src of pluginsArg.sources) {
|
|
333
|
+
const result = await loadPlugins({ source: src, signature: { ...sig.options, onCount: (n, l) => onCount(n, l) } })
|
|
334
|
+
if (result.report.stages.some(s => s.status === 'FAIL')) {
|
|
335
|
+
// 加载期显式失败红线(两模式一致):FAIL = 插件未装上,服务不得静默缺插件启动。
|
|
336
|
+
// DEC-1 warn/enforce 之分仅在签名门缺签路径(warn=横幅放行,enforce=verify FAIL 走此分支)
|
|
337
|
+
for (const line of formatLoadReport(result.report)) console.error(line)
|
|
338
|
+
console.error('[car mcp-serve] 装载 FAIL——fail-closed 启动中止(warn 仅豁免缺签,不豁免装载失败)')
|
|
339
|
+
return 1
|
|
340
|
+
}
|
|
341
|
+
for (const w of result.report.warnings) console.error(`[car mcp-serve] ⚠ ${w}`)
|
|
342
|
+
for (const p of result.plugins) {
|
|
343
|
+
ctx.plugin({ name: p.manifest.name, apply: c => {
|
|
344
|
+
p.bindCore({
|
|
345
|
+
registerTool: t => { hostTools.push(t); c.provide('tool:' + t.name, t) },
|
|
346
|
+
getRegisteredTools: () => hostTools,
|
|
347
|
+
})
|
|
348
|
+
} })
|
|
349
|
+
}
|
|
350
|
+
const names = result.plugins.map(p => `${p.manifest.name}@${p.manifest.version}`).join(', ')
|
|
351
|
+
console.error(`[car mcp-serve] plugins loaded: ${names}(tools: ${hostTools.map(t => t.name).join(', ') || '无'};装载已接线,会话内执行接线属 W1 登记后续)`)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
236
354
|
const { HostGateway } = await import('./host/hostGateway.ts')
|
|
237
355
|
const { createRuntimeFacade } = await import('./host/facade.ts')
|
|
238
356
|
const { createStdioServer } = await import('./host/stdio.ts')
|
|
239
|
-
const { createCounters, assertZeroContent } = await import('./telemetry/metrics.ts')
|
|
240
|
-
const counters = createCounters()
|
|
241
357
|
const profiles = new Map(HOST_MAPPINGS.map(h => [h.hostId, h]))
|
|
242
358
|
const facade = createRuntimeFacade({ profiles })
|
|
243
|
-
const gw = new HostGateway({ facade, audit: e =>
|
|
359
|
+
const gw = new HostGateway({ facade, audit: e => onCount('car_registry_decision', { source: 'registry' }) })
|
|
244
360
|
for (const h of HOST_MAPPINGS) gw.registerHost({ hostId: h.hostId, profile: h, transport: {} as never })
|
|
245
361
|
const server = createStdioServer((tool, args) => gw.handle(hostId, tool, args))
|
|
246
362
|
const n = await server.serve(process.stdin, process.stdout)
|
|
247
363
|
// 采集窗口收口(登记表兜底通道):快照写 stderr(stdout 为协议通道不可污染)
|
|
248
364
|
const snap = counters.snapshot()
|
|
249
365
|
console.error(`[car mcp-serve] host=${hostId} requests=${n} snapshot=${JSON.stringify(snap)} zeroContent=${assertZeroContent(snap)}`)
|
|
366
|
+
// 1.2-S3:OTel 显式开路径收口 flush(shutdown = flush + 停 interval;缺省关为空转)
|
|
367
|
+
if (otel.enabled) {
|
|
368
|
+
await otel.shutdown()
|
|
369
|
+
console.error(`[car mcp-serve] otel=enabled stats=${JSON.stringify(otel.stats())}`)
|
|
370
|
+
}
|
|
250
371
|
return 0
|
|
251
372
|
}
|
|
373
|
+
case 'plugin-sign': {
|
|
374
|
+
// 1.1-S1 插件签名工具(指南 §2 node -e 手工流固化):keygen / sign / verify
|
|
375
|
+
// 退出码沿用家规:0=成功,1=数据校验失败(签名/验签不通过),2=用法/输入不可用
|
|
376
|
+
const [sub, ...sargs] = rest
|
|
377
|
+
const parseFiles = (argv: string[], keyFlag?: string): { files: string[]; keyPath?: string; trustRoot?: string; error?: string } => {
|
|
378
|
+
const files: string[] = []
|
|
379
|
+
let keyPath: string | undefined
|
|
380
|
+
let trustRoot: string | undefined
|
|
381
|
+
for (let i = 0; i < argv.length; i++) {
|
|
382
|
+
const a = argv[i]!
|
|
383
|
+
if (keyFlag && a === keyFlag) {
|
|
384
|
+
keyPath = argv[++i]
|
|
385
|
+
if (!keyPath) return { files, error: `缺少 ${keyFlag} 路径` }
|
|
386
|
+
} else if (a === '--trust-root') {
|
|
387
|
+
trustRoot = argv[++i]
|
|
388
|
+
if (!trustRoot) return { files, error: '缺少 --trust-root 值' }
|
|
389
|
+
} else if (a.startsWith('--')) {
|
|
390
|
+
return { files, error: `未知选项 ${a}` }
|
|
391
|
+
} else files.push(a)
|
|
392
|
+
}
|
|
393
|
+
return { files, keyPath, trustRoot }
|
|
394
|
+
}
|
|
395
|
+
if (sub === 'keygen') {
|
|
396
|
+
let prefix = 'car-release'
|
|
397
|
+
let force = false
|
|
398
|
+
for (let i = 0; i < sargs.length; i++) {
|
|
399
|
+
const a = sargs[i]!
|
|
400
|
+
if (a === '--out') { prefix = sargs[++i] ?? ''; if (!prefix) { console.error('缺少 --out 前缀'); return 2 } }
|
|
401
|
+
else if (a === '--force') force = true
|
|
402
|
+
else if (a.startsWith('--')) { console.error(`未知选项 ${a}`); return 2 }
|
|
403
|
+
else { console.error(`多余位置参数 ${a}`); return 2 }
|
|
404
|
+
}
|
|
405
|
+
const privPath = `${prefix}.priv`
|
|
406
|
+
const pubPath = `${prefix}.pub`
|
|
407
|
+
if (!force && (existsSync(privPath) || existsSync(pubPath))) {
|
|
408
|
+
console.error(`已存在 ${privPath} / ${pubPath}——拒绝覆盖(--force 显式覆盖)。注意:覆盖 ≠ 轮换,旧公钥仍在用请先按轮换流程处置`)
|
|
409
|
+
return 2
|
|
410
|
+
}
|
|
411
|
+
const kp = generateSigningKeypair()
|
|
412
|
+
writeFileSync(privPath, kp.privateKeyDer, { mode: 0o600 })
|
|
413
|
+
writeFileSync(pubPath, kp.publicKeyBase64)
|
|
414
|
+
console.log(`密钥对已生成(前缀 ${prefix}):`)
|
|
415
|
+
console.log(` 私钥 ${privPath}(PKCS8 DER;离线保管,不入仓库——secrets 红线)`)
|
|
416
|
+
console.log(` 公钥 ${pubPath}(SPKI DER base64)`)
|
|
417
|
+
console.log(`信任根(CAR_TRUST_ROOT 值,或 plugin-sign verify --trust-root):`)
|
|
418
|
+
console.log(kp.publicKeyBase64)
|
|
419
|
+
return 0
|
|
420
|
+
}
|
|
421
|
+
if (sub === 'sign') {
|
|
422
|
+
const p = parseFiles(sargs, '--key')
|
|
423
|
+
if (p.error) { console.error(p.error); return 2 }
|
|
424
|
+
if (!p.files.length) { console.error('缺少插件文件参数'); return 2 }
|
|
425
|
+
const keyPath = p.keyPath ?? 'car-release.priv'
|
|
426
|
+
let priv: Buffer
|
|
427
|
+
try { priv = readFileSync(keyPath) } catch {
|
|
428
|
+
console.error(`私钥不可读:${keyPath}(先 car plugin-sign keygen 生成,或 --key 指定路径)`)
|
|
429
|
+
return 2
|
|
430
|
+
}
|
|
431
|
+
let failed = false
|
|
432
|
+
for (const f of p.files) {
|
|
433
|
+
try {
|
|
434
|
+
const r = signPluginFile(f, priv)
|
|
435
|
+
console.log(`signed ${r.file} fp:${r.manifestHash.slice(0, 12)} → ${r.sidecar}`)
|
|
436
|
+
} catch (e) {
|
|
437
|
+
console.error(`sign FAIL ${f} — ${(e as Error).message}`)
|
|
438
|
+
failed = true
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return failed ? 1 : 0
|
|
442
|
+
}
|
|
443
|
+
if (sub === 'verify') {
|
|
444
|
+
const p = parseFiles(sargs)
|
|
445
|
+
if (p.error) { console.error(p.error); return 2 }
|
|
446
|
+
if (!p.files.length) { console.error('缺少插件文件参数'); return 2 }
|
|
447
|
+
// 信任根解析序(与指南 §4 对齐):--trust-root > CAR_TRUST_ROOT env > ./car-release.pub
|
|
448
|
+
const tr = resolveTrustRoot({ flag: p.trustRoot, env: process.env, pubFilePath: 'car-release.pub' })
|
|
449
|
+
if (!tr.value) {
|
|
450
|
+
console.error('无信任根:--trust-root / CAR_TRUST_ROOT / ./car-release.pub 均未提供(keygen 产出的 .pub 即信任根)')
|
|
451
|
+
return 2
|
|
452
|
+
}
|
|
453
|
+
let failed = false
|
|
454
|
+
for (const f of p.files) {
|
|
455
|
+
// verify 语义固定 enforce:缺签与坏签同为 FAIL(验签命令不做 warn 放行)
|
|
456
|
+
const g = verifyPluginFile(f, { mode: 'enforce', trustRootPublicKey: tr.value })
|
|
457
|
+
if (g.allowed && !g.warning) console.log(`verify: PASS ${f} fp:${g.manifestHash?.slice(0, 12)}(信任根来源 ${tr.source})`)
|
|
458
|
+
else { console.error(`verify: FAIL ${f} — ${g.error ?? g.warning ?? 'rejected'}`); failed = true }
|
|
459
|
+
}
|
|
460
|
+
return failed ? 1 : 0
|
|
461
|
+
}
|
|
462
|
+
console.error('用法: car plugin-sign <keygen|sign|verify> [args](--help 查看全部命令)')
|
|
463
|
+
return 2
|
|
464
|
+
}
|
|
252
465
|
default:
|
|
253
466
|
console.log(HELP)
|
|
254
467
|
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。
|
package/src/kernel/context.ts
CHANGED
|
@@ -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()) {
|