@lqc123qwe/car-runtime 1.1.0 → 1.3.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/.github/workflows/release.yml +28 -3
- package/README.md +16 -1
- package/package.json +1 -1
- package/scripts/release-artifacts.ts +111 -0
- package/scripts/release-pipeline.ts +27 -26
- package/src/cli.ts +41 -6
- package/src/runtime-core/telemetry.ts +113 -23
- package/src/session/indexStore.ts +178 -3
- package/test/s25-m8-runtime-core.spec.ts +2 -1
- package/test/s28-session-index-followup.spec.ts +236 -0
- package/test/s29-otel-producer.spec.ts +252 -0
- package/test/s30-release-artifacts.spec.ts +127 -0
|
@@ -4,10 +4,17 @@ on:
|
|
|
4
4
|
release:
|
|
5
5
|
types: [published]
|
|
6
6
|
permissions:
|
|
7
|
-
contents
|
|
7
|
+
# 1.3-S3(W4-2):contents 提升为 write——gh release upload 资产上传需要;
|
|
8
|
+
# id-token: write 为 keyless cosign(OIDC + Rekor)凭据源(D-15 正式轨)
|
|
9
|
+
contents: write
|
|
8
10
|
id-token: write
|
|
9
|
-
# J-16 release-merge(M2 §2.3
|
|
10
|
-
#
|
|
11
|
+
# J-16 release-merge(M2 §2.3)+ 1.3 W4 全自动化:
|
|
12
|
+
# 构建制品(release-artifacts.ts——与本地 release-pipeline.ts 单一事实源,R-3)
|
|
13
|
+
# → cosign keyless 签名 tgz(OIDC + Rekor 透明日志;私钥不出本地,D-15 正式轨)
|
|
14
|
+
# → 上传五件套到 Release(gh release upload --clobber;消除 1.0 错挂/1.2 树漂移的人工拖拽面)
|
|
15
|
+
# → npm publish --provenance(原步不动)。
|
|
16
|
+
# 步骤序红线:upload 先于 publish——上传失败 job 即红,publish 不带病出街。
|
|
17
|
+
# 回退预案(R-1,演练预案非静默):keyless 签名/上传失败时可临时人工拖拽资产,事后修复重发。
|
|
11
18
|
jobs:
|
|
12
19
|
j16-release-merge:
|
|
13
20
|
runs-on: ubuntu-latest
|
|
@@ -17,6 +24,24 @@ jobs:
|
|
|
17
24
|
with:
|
|
18
25
|
node-version: 22
|
|
19
26
|
registry-url: https://registry.npmjs.org/
|
|
27
|
+
- name: Build release artifacts (single-source with local pipeline, W4-1)
|
|
28
|
+
run: node --experimental-transform-types scripts/release-artifacts.ts
|
|
29
|
+
- name: Install cosign (pinned, R-4)
|
|
30
|
+
uses: sigstore/cosign-installer@v3.7.0
|
|
31
|
+
- name: Keyless sign tarball (OIDC + Rekor, D-15 formal track)
|
|
32
|
+
run: |
|
|
33
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
34
|
+
cosign sign-blob --yes --bundle "dist/car-runtime-${VERSION}.tgz.sig.bundle" "dist/car-runtime-${VERSION}.tgz"
|
|
35
|
+
echo "{\"stage\":\"cosign-keyless\",\"detail\":\"signed car-runtime-${VERSION}.tgz (keyless OIDC + Rekor)\",\"ts\":$(date +%s%3N)}" >> dist/release-audit.jsonl
|
|
36
|
+
- name: "Upload release assets (W4 - zero manual drag-and-drop)"
|
|
37
|
+
env:
|
|
38
|
+
GH_TOKEN: ${{ github.token }}
|
|
39
|
+
run: |
|
|
40
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
41
|
+
for f in dist/car-runtime-${VERSION}.tgz dist/car-runtime-${VERSION}.tgz.sig.bundle dist/SHA256SUMS dist/sbom.json dist/release-audit.jsonl; do
|
|
42
|
+
# 上传目标 = 包版本号(D-14 裸 tag 形态:release tag == version);dispatch 触发亦可用
|
|
43
|
+
gh release upload "$VERSION" "$f" --clobber
|
|
44
|
+
done
|
|
20
45
|
- name: Publish to npm with provenance
|
|
21
46
|
# dist-tag 按版本形态动态选择:prerelease(如 1.0.0-rc.1)→ rc;稳定版(1.0.0)→ latest。
|
|
22
47
|
# M5 §1.1 口径:1.0 发布时 latest→1.0.0,rc tag 保留指向 rc.1 不回指——稳定版禁止 --tag rc。
|
package/README.md
CHANGED
|
@@ -165,12 +165,27 @@ node --experimental-transform-types --test test/*.spec.ts
|
|
|
165
165
|
|
|
166
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
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`。
|
|
168
169
|
- **边界口径(防外推)**:mcp-serve 装载接线 = 「装载 + 采集全链」——插件 factory 执行 + bindCore 冲刷注册项真实发生;**宿主会话执行插件工具的执行接线属 W1 登记后续**(sessionTurn 仍为事件批归一化),「已实装」口径不外推。sigstore keyless 主轨 / anthropic 适配器 / W1 runTurn→chatStep 接线均为延后登记项(用户裁决 2026-09-27)。
|
|
169
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`。
|
|
170
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
|
+
- **1.2.0 正式发布(2026-09-27,S5 收官)**:npm `@lqc123qwe/car-runtime@1.2.0` 在架——**dist-tag latest → 1.2.0,rc 保留指向 1.0.0-rc.1**;SLSA provenance(slsa.dev/provenance/v1)。Release `1.2.0`(用户创建,六资产)→ **j16 run 36312617477 success** → **post-publish REMOTE_CHECK=1 复跑 12 PASS / 1 DRY-RUN / 0 FAIL(G-09 实查 PASS)** + 外部验证者等价检查(Release 实下载件 SHA256 MATCH `6d6d4da1…` + cosign Verified OK)。发布工程事件:registry CDN 缓存滞后登记(cache-buster 口径);制品自 tag 树重建后再挂(消除 1.1 的树漂移)。
|
|
183
|
+
|
|
184
|
+
- **1.3 发布资产自动化(W4,2026-09-27 收口)**:`scripts/release-artifacts.ts` 制品构建单一事实源(本地流水线/CI j16 双端复用;SHA256SUMS 裸字节自检 G-11 口径 + SBOM G-05 同构 + releaseAssetNames 五件套清单——G-10/CI/测试三方同源)+ release.yml 重构(构建 → cosign keyless 签名〔D-15 正式轨:OIDC+Rekor,CI 零 keypair 材料〕→ `gh release upload --clobber` 五件套 → npm publish 原步不动;**upload 先于 publish**)+ D-14 资产集迁移(car-release.pub 移出——keyless 验证锚 = bundle 证书 + Rekor)+ D-14 tag 形态统一(裸版本号)。s30 六测(构建/清单/同源/yml 结构与步骤序红线)。全量 **269(266 PASS + 3 skip / 0 fail)**。1.3.0 发布 = W4 验收载体(**全链零人工拖拽资产**)。W1 三度延后登记 **1.4 头号候选**(设计账本 D-12a loopback 豁免 + D-13 --demo 保留)。规划:`car-docs/10-内核v1/1.3-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.3-收口终验报告.md`。
|
|
185
|
+
|
|
171
186
|
## 状态
|
|
172
187
|
|
|
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
|
|
188
|
+
- **版本主线**: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 已发布)** | **1.3 发布资产自动化 全量 269(266 PASS + 3 skip,2026-09-27 收口,发布全链去人工拖拽)**
|
|
174
189
|
- **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`)在真实发布兑现价值。
|
|
175
190
|
- **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 清单已全绿收口。
|
|
176
191
|
- **轨道 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 门禁已接入)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lqc123qwe/car-runtime",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1.3-S1 · 发布制品构建(W4):本地 release-pipeline.ts 与 CI j16 双端复用的单一事实源
|
|
3
|
+
*
|
|
4
|
+
* 口径(1.3 规划 W4-1/W4-4 / D-14 / D-15):
|
|
5
|
+
* - 同一仓树 → 逐字节同制品(git archive HEAD;G-11 教训:哈希对裸字节求);
|
|
6
|
+
* - **资产集五件套**(releaseAssetNames 单一事实源——G-10 need-list、CI upload、测试三方同源):
|
|
7
|
+
* car-runtime-<v>.tgz / car-runtime-<v>.tgz.sig.bundle / SHA256SUMS / sbom.json / release-audit.jsonl
|
|
8
|
+
* car-release.pub 自 1.3 移除(D-14:keyless 下验证锚 = bundle 证书 + Rekor,不再是本地公钥);
|
|
9
|
+
* - **签名不在此层**(D-15 双轨):bundle 由签名轨产出——本地 keypair no-tlog(预演态,
|
|
10
|
+
* release-pipeline G-08)/ CI keyless OIDC+Rekor(正式态,j16);本脚本只构建与审计;
|
|
11
|
+
* - 构建审计:CLI 入口(CI 调用形态)写 release-audit.jsonl(archive/sbom/sha256 记录);
|
|
12
|
+
* 程序化调用(pipeline)经 onRecord 接入调用方审计流。
|
|
13
|
+
* - 零依赖(node:crypto/fs/child_process)。
|
|
14
|
+
*/
|
|
15
|
+
import { execSync } from 'node:child_process'
|
|
16
|
+
import { createHash } from 'node:crypto'
|
|
17
|
+
import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { pathToFileURL } from 'node:url'
|
|
20
|
+
|
|
21
|
+
export interface BuildRecord { stage: string; detail: string; ts: number }
|
|
22
|
+
|
|
23
|
+
export interface BuildResult {
|
|
24
|
+
tgzPath: string
|
|
25
|
+
sha256: string
|
|
26
|
+
sumsPath: string
|
|
27
|
+
sbomPath: string
|
|
28
|
+
components: number
|
|
29
|
+
bytes: number
|
|
30
|
+
records: BuildRecord[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const sha256 = (s: string | Buffer) => createHash('sha256').update(s).digest('hex')
|
|
34
|
+
|
|
35
|
+
function walk(dir: string, ext: string): string[] {
|
|
36
|
+
let out: string[] = []
|
|
37
|
+
for (const name of readdirSync(dir)) {
|
|
38
|
+
const p = join(dir, name)
|
|
39
|
+
if (statSync(p).isDirectory()) out = out.concat(walk(p, ext))
|
|
40
|
+
else if (p.endsWith(ext)) out.push(p)
|
|
41
|
+
}
|
|
42
|
+
return out
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 资产集清单(五件套;D-14:car-release.pub 移除,签名 bundle 由签名轨产出后入列) */
|
|
46
|
+
export function releaseAssetNames(version: string): string[] {
|
|
47
|
+
return [
|
|
48
|
+
`car-runtime-${version}.tgz`,
|
|
49
|
+
`car-runtime-${version}.tgz.sig.bundle`,
|
|
50
|
+
'SHA256SUMS',
|
|
51
|
+
'sbom.json',
|
|
52
|
+
'release-audit.jsonl',
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildReleaseArtifacts(opts: {
|
|
57
|
+
version: string
|
|
58
|
+
root?: string
|
|
59
|
+
dist?: string
|
|
60
|
+
onRecord?: (stage: string, detail: string) => void
|
|
61
|
+
}): BuildResult {
|
|
62
|
+
const root = opts.root ?? new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
|
|
63
|
+
const dist = opts.dist ?? join(root, 'dist')
|
|
64
|
+
const version = opts.version
|
|
65
|
+
const records: BuildRecord[] = []
|
|
66
|
+
const rec = (stage: string, detail: string) => {
|
|
67
|
+
const r = { stage, detail, ts: Date.now() }
|
|
68
|
+
records.push(r)
|
|
69
|
+
opts.onRecord?.(stage, detail)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
mkdirSync(dist, { recursive: true })
|
|
73
|
+
const tgzPath = join(dist, `car-runtime-${version}.tgz`)
|
|
74
|
+
execSync(`git archive --format=tar.gz -o "${tgzPath}" HEAD`, { cwd: root })
|
|
75
|
+
const bytes = readFileSync(tgzPath)
|
|
76
|
+
rec('archive', `git archive HEAD → car-runtime-${version}.tgz(${bytes.length} bytes,树哈希锚定)`)
|
|
77
|
+
|
|
78
|
+
// SHA256SUMS + 自检(G-11 口径:对裸字节求哈希;门禁不可写死为 true——落盘读回复核)
|
|
79
|
+
const sumsPath = join(dist, 'SHA256SUMS')
|
|
80
|
+
writeFileSync(sumsPath, `${sha256(bytes)} car-runtime-${version}.tgz\n`)
|
|
81
|
+
const [recorded, recordedName] = readFileSync(sumsPath, 'utf-8').trim().split(/\s+/)
|
|
82
|
+
if (recorded !== sha256(bytes) || recordedName !== `car-runtime-${version}.tgz`) {
|
|
83
|
+
throw new Error(`CAR-E-ARTIFACT: SHA256SUMS 自检失败——记录 ${recorded} ≠ 制品裸字节哈希(G-11 教训口径)`)
|
|
84
|
+
}
|
|
85
|
+
rec('sha256sums', `SHA256SUMS ${recorded!.slice(0, 16)}…(裸字节复核通过)`)
|
|
86
|
+
|
|
87
|
+
// SBOM(G-05 同构:TS 直跑形态制品清单 + 零运行时依赖)
|
|
88
|
+
const files = [...walk(join(root, 'src'), '.ts'), join(root, 'package.json'), join(root, 'README.md')]
|
|
89
|
+
const sbom = {
|
|
90
|
+
bomFormat: 'CAR-SBOM', specVersion: '0.1', version,
|
|
91
|
+
components: files.map(f => ({ type: 'file', path: f.replace(root, '').replace(/\\/g, '/'), sha256: sha256(readFileSync(f, 'utf-8')) })),
|
|
92
|
+
runtimeDependencies: [],
|
|
93
|
+
}
|
|
94
|
+
const sbomPath = join(dist, 'sbom.json')
|
|
95
|
+
writeFileSync(sbomPath, JSON.stringify(sbom, null, 2))
|
|
96
|
+
rec('sbom', `${files.length} 个组件(运行时依赖 0 项)`)
|
|
97
|
+
|
|
98
|
+
return { tgzPath, sha256: sha256(bytes), sumsPath, sbomPath, components: files.length, bytes: bytes.length, records }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** CLI 入口(CI j16 调用形态):构建 + 写构建审计 release-audit.jsonl(五件套之五) */
|
|
102
|
+
function main(): void {
|
|
103
|
+
const version = (JSON.parse(readFileSync(join(new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1'), 'package.json'), 'utf-8')) as { version: string }).version
|
|
104
|
+
const result = buildReleaseArtifacts({ version })
|
|
105
|
+
const auditPath = join(result.tgzPath, '..', 'release-audit.jsonl')
|
|
106
|
+
writeFileSync(auditPath, result.records.map(r => JSON.stringify(r)).join('\n') + '\n')
|
|
107
|
+
console.log(`release artifacts: v${version} → tgz(${result.bytes}B, sha256 ${result.sha256.slice(0, 16)}…) + SHA256SUMS + sbom(${result.components} components) + release-audit.jsonl`)
|
|
108
|
+
console.log(`assets (D-14 五件套): ${releaseAssetNames(version).join(', ')}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main()
|
|
@@ -12,6 +12,7 @@ import { execSync, execFileSync } from 'node:child_process'
|
|
|
12
12
|
import { createHash } from 'node:crypto'
|
|
13
13
|
import { readFileSync, writeFileSync, readdirSync, statSync, mkdirSync, existsSync } from 'node:fs'
|
|
14
14
|
import { join } from 'node:path'
|
|
15
|
+
import { buildReleaseArtifacts, releaseAssetNames } from './release-artifacts.ts'
|
|
15
16
|
|
|
16
17
|
const ROOT = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
|
|
17
18
|
const DIST = join(ROOT, 'dist')
|
|
@@ -76,16 +77,13 @@ const record = (stage: string, gate: string, ok: boolean, detail = '', dryRun =
|
|
|
76
77
|
// ── 阶段 2:构建与制品 ──
|
|
77
78
|
{
|
|
78
79
|
const stage = 'S2-构建与制品'
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
writeFileSync(join(DIST, 'sbom.json'), JSON.stringify(sbom, null, 2))
|
|
88
|
-
record(stage, 'G-05 SBOM 生成', true, `${files.length} 个组件(运行时依赖 0 项)`)
|
|
80
|
+
// 1.3-S2(W4-1):制品构建抽至 release-artifacts.ts(本地流水线与 CI j16 双端单一事实源——
|
|
81
|
+
// 同仓树逐字节同制品);此处产出 tgz + SHA256SUMS + sbom,G-11 在 S6 独立复核
|
|
82
|
+
const built = buildReleaseArtifacts({
|
|
83
|
+
version: VERSION,
|
|
84
|
+
onRecord: (s, detail) => auditLog(stage, s.toUpperCase(), 'PASS', { detail }),
|
|
85
|
+
})
|
|
86
|
+
record(stage, 'G-05 SBOM 生成', true, `${built.components} 个组件(运行时依赖 0 项;tar.gz ${built.bytes} bytes 已构建——W4 单一事实源)`)
|
|
89
87
|
// 版本一致性:精确 semver(含 prerelease——1.0-rc.x 是 rc 渠道合法形态;禁止 build 元数据与 loose 版本)
|
|
90
88
|
const ok = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(VERSION) && !/\+/.test(VERSION)
|
|
91
89
|
record(stage, 'G-06 版本一致性', ok, `version=${VERSION}(精确 semver 含 prerelease——rc 渠道合法形态;npm 不可变语义:禁止覆盖已发布版本)`)
|
|
@@ -117,7 +115,7 @@ const record = (stage: string, gate: string, ok: boolean, detail = '', dryRun =
|
|
|
117
115
|
}
|
|
118
116
|
|
|
119
117
|
// ── 阶段 4-5:provenance 与远端归档(远端依赖,DRY-RUN)──
|
|
120
|
-
// G-08
|
|
118
|
+
// G-08 双轨语义(1.3 D-15):本地 keypair no-tlog = 预演态(S6b);CI j16 keyless OIDC+Rekor = 正式态
|
|
121
119
|
// G-10:REMOTE_CHECK=1 且具备 GH_TOKEN 时实查 Release 资产齐备性,否则 DRY-RUN
|
|
122
120
|
{
|
|
123
121
|
const stage = 'S4-provenance与远端归档'
|
|
@@ -127,14 +125,21 @@ const record = (stage: string, gate: string, ok: boolean, detail = '', dryRun =
|
|
|
127
125
|
if (process.env.GH_TOKEN && process.env.REMOTE_CHECK === '1') {
|
|
128
126
|
try {
|
|
129
127
|
const ghBin = 'D:/WorkBuddy/agent/tools/bin/gh.exe'
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
128
|
+
// tag 形态双兼容:v 前缀(v1.0.0 惯例)与无前缀(1.1.0 实挂形态)都查,并集去重——
|
|
129
|
+
// 硬编码单形态会把真实归档误判 FAIL(4c84b17 G-09 期望 tag 动态化同类教训,2026-09-27)
|
|
130
|
+
const fetchAssets = (tag: string): string[] => {
|
|
131
|
+
try {
|
|
132
|
+
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)
|
|
133
|
+
} catch { return [] }
|
|
134
|
+
}
|
|
135
|
+
const assets = [...new Set([...fetchAssets(`v${VERSION}`), ...fetchAssets(VERSION)])]
|
|
136
|
+
// 资产清单单一事实源(1.3 W4-4/R-3):releaseAssetNames(五件套,无 car-release.pub)
|
|
137
|
+
const need = releaseAssetNames(VERSION)
|
|
133
138
|
const missing = need.filter(n => !assets.includes(n))
|
|
134
139
|
g10Ok = missing.length === 0
|
|
135
140
|
g10Dry = false
|
|
136
141
|
g10Detail = g10Ok
|
|
137
|
-
? `Release v${VERSION} 归档齐备(${assets.length} assets:tgz+签名bundle+SHA256SUMS+SBOM
|
|
142
|
+
? `Release v${VERSION} 归档齐备(${assets.length} assets:tgz+签名bundle+SHA256SUMS+SBOM+审计——D-14 五件套)`
|
|
138
143
|
: `Release 资产缺失:${missing.join(', ')}`
|
|
139
144
|
} catch { /* 远端不可达或 Release 不存在 → 保持 DRY-RUN */ }
|
|
140
145
|
}
|
|
@@ -165,22 +170,18 @@ const record = (stage: string, gate: string, ok: boolean, detail = '', dryRun =
|
|
|
165
170
|
record(stage, 'G-09 npm provenance(OIDC 可信发布)', g9Ok, g9Detail, g9Dry)
|
|
166
171
|
}
|
|
167
172
|
|
|
168
|
-
// ── 阶段 6
|
|
173
|
+
// ── 阶段 6:制品校验(G-11:对 S2 产出的制品做独立复核,不重建不写死) ──
|
|
169
174
|
const TARBALL = join(DIST, `car-runtime-${VERSION}.tgz`)
|
|
170
175
|
{
|
|
171
176
|
const stage = 'S6-制品封装'
|
|
172
|
-
execSync(`git archive --format=tar.gz -o "${TARBALL}" HEAD`, { cwd: ROOT })
|
|
173
|
-
const bytes = readFileSync(TARBALL)
|
|
174
177
|
// 校验和必须对「裸字节」求哈希——曾误写为 sha256(bytes.toString('base64')),
|
|
175
178
|
// 记录值成为 base64 字符串的哈希,外部 sha256sum 校验必然失败(rc.1 已发生)。
|
|
176
|
-
const
|
|
177
|
-
writeFileSync(sumsPath, `${sha256(bytes)} car-runtime-${VERSION}.tgz\n`)
|
|
178
|
-
// 自检:从落盘文件读回,复核其确实等于制品裸字节哈希(门禁不可写死为 true)
|
|
179
|
-
const [recorded, recordedName] = readFileSync(sumsPath, 'utf-8').trim().split(/\s+/)
|
|
179
|
+
const bytes = readFileSync(TARBALL)
|
|
180
180
|
const actual = sha256(bytes)
|
|
181
|
+
const [recorded, recordedName] = readFileSync(join(DIST, 'SHA256SUMS'), 'utf-8').trim().split(/\s+/)
|
|
181
182
|
const ok = recorded === actual && recordedName === `car-runtime-${VERSION}.tgz`
|
|
182
183
|
record(stage, 'G-11 制品校验和(SHA-256)', ok, ok
|
|
183
|
-
? `car-runtime-${VERSION}.tgz (${bytes.length} bytes) → SHA256SUMS ${recorded
|
|
184
|
+
? `car-runtime-${VERSION}.tgz (${bytes.length} bytes) → SHA256SUMS ${recorded!.slice(0, 16)}…(S2 产出独立复核,外部 sha256sum 可验证)`
|
|
184
185
|
: `校验和自检失败:SHA256SUMS 记录 ${recorded} ≠ 制品裸字节哈希 ${actual}`)
|
|
185
186
|
}
|
|
186
187
|
|
|
@@ -196,7 +197,7 @@ const TARBALL = join(DIST, `car-runtime-${VERSION}.tgz`)
|
|
|
196
197
|
'--signing-config', sigConfig, '--bundle', bundle, TARBALL], { env })
|
|
197
198
|
execFileSync(cosignBin, ['verify-blob', '--key', join(keysDir, 'car-release.pub'),
|
|
198
199
|
'--insecure-ignore-tlog', '--bundle', bundle, TARBALL], { env })
|
|
199
|
-
record(stage, 'G-08 cosign 签名', existsSync(bundle), `本地 keypair 实签+验签 PASS(no-tlog 模式,bundle: car-runtime-${VERSION}.tgz.sig.bundle
|
|
200
|
+
record(stage, 'G-08 cosign 签名', existsSync(bundle), `本地 keypair 实签+验签 PASS(no-tlog 模式,bundle: car-runtime-${VERSION}.tgz.sig.bundle)——D-15 预演轨;正式轨 = CI j16 keyless OIDC+Rekor`)
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
// ── 阶段 7:dist-tag 晋级模拟 + 回滚演练(发布渠道语义)──
|
|
@@ -223,5 +224,5 @@ console.log('\n===== 发布工程预演结果 =====')
|
|
|
223
224
|
for (const r of results) console.log(` [${r.verdict.padEnd(7)}] ${r.stage} / ${r.gate}${r.detail ? ' — ' + r.detail : ''}`)
|
|
224
225
|
const pass = results.filter(r => r.verdict === 'PASS').length
|
|
225
226
|
const dry = results.filter(r => r.verdict === 'DRY-RUN').length
|
|
226
|
-
console.log(`\n合计:${pass} PASS / ${dry} DRY-RUN / 0 FAIL(制品:dist
|
|
227
|
-
console.log('DRY-RUN 项正式执行前置:GitHub 仓库 + OIDC + npm publish 凭据 + cosign + Linux CI runner(逃逸矩阵 G-07)')
|
|
227
|
+
console.log(`\n合计:${pass} PASS / ${dry} DRY-RUN / 0 FAIL(制品:dist/ 五件套——tar.gz + 签名bundle + SHA256SUMS + sbom + release-audit)`)
|
|
228
|
+
console.log('DRY-RUN 项正式执行前置:GitHub 仓库 + OIDC + npm publish 凭据 + cosign + Linux CI runner(逃逸矩阵 G-07);正式签名轨 = CI j16 keyless(D-15)')
|
package/src/cli.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { SessionLog, loadSessionLog, type SessionEvent } from './session/log.ts'
|
|
|
19
19
|
import { SessionFileStore } from './session/store.ts'
|
|
20
20
|
import { compressJsonlZstd, decodeLogBuffer, zstdAvailable } from './session/format.ts'
|
|
21
21
|
import { exportForensicsBundle, verifyBundle, type ExportMeta } from './session/export.ts'
|
|
22
|
+
import { lookupSession, IndexUpdater, type SessionIndexRow } from './session/indexStore.ts'
|
|
22
23
|
import { runTurn } from './loop/stop.ts'
|
|
23
24
|
import { Context } from './kernel/context.ts'
|
|
24
25
|
import { mountPlugin } from './load/loader.ts'
|
|
@@ -26,6 +27,8 @@ import { verifyPluginFile } from './load/sigGate.ts'
|
|
|
26
27
|
import { loadCarConfig, mergeSignatureGate } from './load/config.ts'
|
|
27
28
|
import { generateSigningKeypair, signPluginFile, resolveTrustRoot } from './load/sign.ts'
|
|
28
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'
|
|
29
32
|
|
|
30
33
|
const HELP = `用法: car <command> [args]
|
|
31
34
|
run <plugin.ts> [--sig-enforce] [--config <path>]
|
|
@@ -38,6 +41,8 @@ const HELP = `用法: car <command> [args]
|
|
|
38
41
|
取证包导出(断链中止;落盘读回重验,离线自证)
|
|
39
42
|
session rebuild-index <root> [--db <path>]
|
|
40
43
|
从 JSONL 重建 SQLite 会话索引(幂等)
|
|
44
|
+
session verify/replay/export 均支持 --session <id> [--db <path>]
|
|
45
|
+
经 SQLite 索引定位会话(1.2;缺省 db = cwd sessions-index.db;索引未收录 = 显式报错)
|
|
41
46
|
doctor 环境自检
|
|
42
47
|
plugin-sign keygen [--out <前缀>] [--force]
|
|
43
48
|
生成 ed25519 签名密钥对(<前缀>.priv PKCS8 DER / <前缀>.pub SPKI base64;缺省前缀 car-release)
|
|
@@ -109,7 +114,10 @@ async function main(): Promise<number> {
|
|
|
109
114
|
const log = new SessionLog('S-' + Date.now().toString(36))
|
|
110
115
|
// §3.2.M7.5「先落日志后放行」:run 流全程逐事件 fsync 落盘(append 返回 = 已过掉电窗口)
|
|
111
116
|
const store = new SessionFileStore(`session-${log.sessionId}.jsonl`)
|
|
112
|
-
|
|
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) })
|
|
113
121
|
const hostTools: any[] = []
|
|
114
122
|
ctx.plugin({ name: plugin.manifest.name, apply: (c) => {
|
|
115
123
|
plugin.bindCore({
|
|
@@ -133,8 +141,9 @@ async function main(): Promise<number> {
|
|
|
133
141
|
model: async () => ({ stopReason: 'toolUse' as const, toolCalls: [{ id: 'c1', tool: 'demo_tool', args: {} }] }),
|
|
134
142
|
})
|
|
135
143
|
console.log(`[3/5 收口] turnEnd=${r.reason}(steps=${r.steps})`)
|
|
136
|
-
// 环节 4:落盘收口(全程逐事件 fsync
|
|
144
|
+
// 环节 4:落盘收口(全程逐事件 fsync 已发生,此处仅关句柄核对 + 索引 pending 落盘)
|
|
137
145
|
store.close()
|
|
146
|
+
await indexUpdater.close()
|
|
138
147
|
console.log(`[4/5 落盘] ${store.path}(${log.events.length} 事件逐事件 fsync,哈希链 ${log.verifyChain() === null ? '完整' : '断链!'})`)
|
|
139
148
|
// 环节 5:审计回放
|
|
140
149
|
const replay = log.deriveMessages()
|
|
@@ -182,7 +191,7 @@ async function main(): Promise<number> {
|
|
|
182
191
|
for (let i = 0; i < args.length; i++) {
|
|
183
192
|
const a = args[i]!
|
|
184
193
|
if (a === '--zstd') flags.zstd = true
|
|
185
|
-
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] ?? ''
|
|
186
195
|
else positional.push(a)
|
|
187
196
|
}
|
|
188
197
|
// C-02 索引重建(§3.2.M7.5 Step 3「异步可容忍,丢失可 rebuild」;幂等单事务)
|
|
@@ -200,7 +209,20 @@ async function main(): Promise<number> {
|
|
|
200
209
|
return 0
|
|
201
210
|
} catch (e) { console.error((e as Error).message); return 2 }
|
|
202
211
|
}
|
|
203
|
-
|
|
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
|
+
}
|
|
204
226
|
if (!file) { console.error('缺少文件参数'); return 2 }
|
|
205
227
|
if (sub === 'export' && !flags.out) { console.error('缺少 --out 目录参数'); return 2 }
|
|
206
228
|
let loaded: ReturnType<typeof loadSessionLog>
|
|
@@ -293,14 +315,22 @@ async function main(): Promise<number> {
|
|
|
293
315
|
const pluginsArg = collectPluginSources(rest)
|
|
294
316
|
if (pluginsArg.error) { console.error(pluginsArg.error); return 2 }
|
|
295
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())
|
|
296
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
|
+
}
|
|
297
327
|
if (pluginsArg.sources.length) {
|
|
298
328
|
const { loadPlugins, formatLoadReport } = await import('./load/report.ts')
|
|
299
329
|
const { Context } = await import('./kernel/context.ts')
|
|
300
330
|
const ctx = new Context()
|
|
301
331
|
const hostTools: any[] = []
|
|
302
332
|
for (const src of pluginsArg.sources) {
|
|
303
|
-
const result = await loadPlugins({ source: src, signature: { ...sig.options, onCount: (n, l) =>
|
|
333
|
+
const result = await loadPlugins({ source: src, signature: { ...sig.options, onCount: (n, l) => onCount(n, l) } })
|
|
304
334
|
if (result.report.stages.some(s => s.status === 'FAIL')) {
|
|
305
335
|
// 加载期显式失败红线(两模式一致):FAIL = 插件未装上,服务不得静默缺插件启动。
|
|
306
336
|
// DEC-1 warn/enforce 之分仅在签名门缺签路径(warn=横幅放行,enforce=verify FAIL 走此分支)
|
|
@@ -326,13 +356,18 @@ async function main(): Promise<number> {
|
|
|
326
356
|
const { createStdioServer } = await import('./host/stdio.ts')
|
|
327
357
|
const profiles = new Map(HOST_MAPPINGS.map(h => [h.hostId, h]))
|
|
328
358
|
const facade = createRuntimeFacade({ profiles })
|
|
329
|
-
const gw = new HostGateway({ facade, audit: e =>
|
|
359
|
+
const gw = new HostGateway({ facade, audit: e => onCount('car_registry_decision', { source: 'registry' }) })
|
|
330
360
|
for (const h of HOST_MAPPINGS) gw.registerHost({ hostId: h.hostId, profile: h, transport: {} as never })
|
|
331
361
|
const server = createStdioServer((tool, args) => gw.handle(hostId, tool, args))
|
|
332
362
|
const n = await server.serve(process.stdin, process.stdout)
|
|
333
363
|
// 采集窗口收口(登记表兜底通道):快照写 stderr(stdout 为协议通道不可污染)
|
|
334
364
|
const snap = counters.snapshot()
|
|
335
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
|
+
}
|
|
336
371
|
return 0
|
|
337
372
|
}
|
|
338
373
|
case 'plugin-sign': {
|