@lqc123qwe/car-runtime 1.2.0 → 1.4.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 +5 -1
- package/package.json +1 -1
- package/scripts/release-artifacts.ts +111 -0
- package/scripts/release-pipeline.ts +19 -24
- package/src/cli.ts +158 -12
- package/src/dx/doctor.ts +49 -2
- package/src/load/config.ts +118 -2
- package/src/load/loader.ts +21 -1
- package/src/loop/stop.ts +12 -1
- package/src/mcp/stdioClient.ts +75 -0
- package/src/ptc/runCode.ts +10 -0
- package/src/runtime-core/chatStep.ts +10 -2
- package/src/runtime-core/llm.ts +17 -1
- package/src/runtime-core/telemetry.ts +42 -6
- package/src/runtime-core/tools.ts +27 -0
- package/src/runtime-core/trace.ts +63 -0
- package/src/runtime-core/types.ts +4 -1
- package/src/session/log.ts +3 -2
- package/test/contract/eventmap.golden.json +1 -1
- package/test/s26-signature-closeout.spec.ts +1 -1
- package/test/s28-session-index-followup.spec.ts +1 -1
- package/test/s30-release-artifacts.spec.ts +127 -0
- package/test/s31-real-model-wiring.spec.ts +329 -0
- package/test/s32-telemetry-mcp-ptc.spec.ts +218 -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
|
@@ -179,10 +179,14 @@ node --experimental-transform-types --test test/*.spec.ts
|
|
|
179
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
180
|
- **边界口径(防外推)**:OTel span 面(turn/step span)**仍无生产调用点**(随 W1 登记后续);trace 传播(parentSpanId/共享 trace)随 W1 设计;mcp-serve 出站现仅 metrics(counter 桥)。
|
|
181
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 结构与步骤序红线/**name 行冒号守卫**〔1.3-BUG-2 回归钉〕)。全量 **270(267 PASS + 3 skip / 0 fail)**。W1 三度延后登记 **1.4 头号候选**(设计账本 D-12a loopback 豁免 + D-13 --demo 保留)。规划:`car-docs/10-内核v1/1.3-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.3-收口终验报告.md`。
|
|
185
|
+
- **1.3.0 正式发布(2026-09-27,S5 收官,W4 全自动首战兑现)**:Release `1.3.0`(tag 32dad29)→ **j16 run 36320604746 全步 success**(构建 → keyless 签名 → 自动上传五件套〔零人工拖拽〕→ npm publish)→ npm `@lqc123qwe/car-runtime@1.3.0` 在架,**dist-tags latest → 1.3.0、rc 保留 1.0.0-rc.1** + provenance。**post-publish 12 PASS / 1 DRY-RUN / 0 FAIL(G-09 实查)**;外部等价检查 **SHA256 MATCH(`70767c8d…`)+ cosign keyless Verified OK**(证书身份 + Rekor——D-15 正式轨兑现,pub 退役)。发布工程事件:首发受挫 1.3-BUG-2(workflow 步骤名未引号冒号 → YAML 解析失败 → startup failure 且 release 事件静默失效;**改 workflow 必须本地 PyYAML 复验**,post-publish 必须核对 release 事件 run 真实触发)。
|
|
182
186
|
|
|
183
187
|
## 状态
|
|
184
188
|
|
|
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
|
|
189
|
+
- **版本主线**: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 发布资产自动化 全量 270(267 PASS + 3 skip,2026-09-27 收口,1.3.0 已发布,全链零人工拖拽兑现)**
|
|
186
190
|
- **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`)在真实发布兑现价值。
|
|
187
191
|
- **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 清单已全绿收口。
|
|
188
192
|
- **轨道 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.4.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与远端归档'
|
|
@@ -135,12 +133,13 @@ const record = (stage: string, gate: string, ok: boolean, detail = '', dryRun =
|
|
|
135
133
|
} catch { return [] }
|
|
136
134
|
}
|
|
137
135
|
const assets = [...new Set([...fetchAssets(`v${VERSION}`), ...fetchAssets(VERSION)])]
|
|
138
|
-
|
|
136
|
+
// 资产清单单一事实源(1.3 W4-4/R-3):releaseAssetNames(五件套,无 car-release.pub)
|
|
137
|
+
const need = releaseAssetNames(VERSION)
|
|
139
138
|
const missing = need.filter(n => !assets.includes(n))
|
|
140
139
|
g10Ok = missing.length === 0
|
|
141
140
|
g10Dry = false
|
|
142
141
|
g10Detail = g10Ok
|
|
143
|
-
? `Release v${VERSION} 归档齐备(${assets.length} assets:tgz+签名bundle+SHA256SUMS+SBOM
|
|
142
|
+
? `Release v${VERSION} 归档齐备(${assets.length} assets:tgz+签名bundle+SHA256SUMS+SBOM+审计——D-14 五件套)`
|
|
144
143
|
: `Release 资产缺失:${missing.join(', ')}`
|
|
145
144
|
} catch { /* 远端不可达或 Release 不存在 → 保持 DRY-RUN */ }
|
|
146
145
|
}
|
|
@@ -171,22 +170,18 @@ const record = (stage: string, gate: string, ok: boolean, detail = '', dryRun =
|
|
|
171
170
|
record(stage, 'G-09 npm provenance(OIDC 可信发布)', g9Ok, g9Detail, g9Dry)
|
|
172
171
|
}
|
|
173
172
|
|
|
174
|
-
// ── 阶段 6
|
|
173
|
+
// ── 阶段 6:制品校验(G-11:对 S2 产出的制品做独立复核,不重建不写死) ──
|
|
175
174
|
const TARBALL = join(DIST, `car-runtime-${VERSION}.tgz`)
|
|
176
175
|
{
|
|
177
176
|
const stage = 'S6-制品封装'
|
|
178
|
-
execSync(`git archive --format=tar.gz -o "${TARBALL}" HEAD`, { cwd: ROOT })
|
|
179
|
-
const bytes = readFileSync(TARBALL)
|
|
180
177
|
// 校验和必须对「裸字节」求哈希——曾误写为 sha256(bytes.toString('base64')),
|
|
181
178
|
// 记录值成为 base64 字符串的哈希,外部 sha256sum 校验必然失败(rc.1 已发生)。
|
|
182
|
-
const
|
|
183
|
-
writeFileSync(sumsPath, `${sha256(bytes)} car-runtime-${VERSION}.tgz\n`)
|
|
184
|
-
// 自检:从落盘文件读回,复核其确实等于制品裸字节哈希(门禁不可写死为 true)
|
|
185
|
-
const [recorded, recordedName] = readFileSync(sumsPath, 'utf-8').trim().split(/\s+/)
|
|
179
|
+
const bytes = readFileSync(TARBALL)
|
|
186
180
|
const actual = sha256(bytes)
|
|
181
|
+
const [recorded, recordedName] = readFileSync(join(DIST, 'SHA256SUMS'), 'utf-8').trim().split(/\s+/)
|
|
187
182
|
const ok = recorded === actual && recordedName === `car-runtime-${VERSION}.tgz`
|
|
188
183
|
record(stage, 'G-11 制品校验和(SHA-256)', ok, ok
|
|
189
|
-
? `car-runtime-${VERSION}.tgz (${bytes.length} bytes) → SHA256SUMS ${recorded
|
|
184
|
+
? `car-runtime-${VERSION}.tgz (${bytes.length} bytes) → SHA256SUMS ${recorded!.slice(0, 16)}…(S2 产出独立复核,外部 sha256sum 可验证)`
|
|
190
185
|
: `校验和自检失败:SHA256SUMS 记录 ${recorded} ≠ 制品裸字节哈希 ${actual}`)
|
|
191
186
|
}
|
|
192
187
|
|
|
@@ -202,7 +197,7 @@ const TARBALL = join(DIST, `car-runtime-${VERSION}.tgz`)
|
|
|
202
197
|
'--signing-config', sigConfig, '--bundle', bundle, TARBALL], { env })
|
|
203
198
|
execFileSync(cosignBin, ['verify-blob', '--key', join(keysDir, 'car-release.pub'),
|
|
204
199
|
'--insecure-ignore-tlog', '--bundle', bundle, TARBALL], { env })
|
|
205
|
-
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`)
|
|
206
201
|
}
|
|
207
202
|
|
|
208
203
|
// ── 阶段 7:dist-tag 晋级模拟 + 回滚演练(发布渠道语义)──
|
|
@@ -229,5 +224,5 @@ console.log('\n===== 发布工程预演结果 =====')
|
|
|
229
224
|
for (const r of results) console.log(` [${r.verdict.padEnd(7)}] ${r.stage} / ${r.gate}${r.detail ? ' — ' + r.detail : ''}`)
|
|
230
225
|
const pass = results.filter(r => r.verdict === 'PASS').length
|
|
231
226
|
const dry = results.filter(r => r.verdict === 'DRY-RUN').length
|
|
232
|
-
console.log(`\n合计:${pass} PASS / ${dry} DRY-RUN / 0 FAIL(制品:dist
|
|
233
|
-
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
|
@@ -24,10 +24,15 @@ import { runTurn } from './loop/stop.ts'
|
|
|
24
24
|
import { Context } from './kernel/context.ts'
|
|
25
25
|
import { mountPlugin } from './load/loader.ts'
|
|
26
26
|
import { verifyPluginFile } from './load/sigGate.ts'
|
|
27
|
-
import { loadCarConfig, mergeSignatureGate } from './load/config.ts'
|
|
27
|
+
import { loadCarConfig, mergeSignatureGate, mergeLlmConfig } from './load/config.ts'
|
|
28
28
|
import { generateSigningKeypair, signPluginFile, resolveTrustRoot } from './load/sign.ts'
|
|
29
|
-
import { doctorCredentials, doctorConnectivity, doctorKeychain, doctorSignature } from './dx/doctor.ts'
|
|
29
|
+
import { doctorCredentials, doctorConnectivity, doctorKeychain, doctorSignature, doctorModelReadiness } from './dx/doctor.ts'
|
|
30
|
+
import { RuntimeCore, createOpenAICompatAdapter } from './runtime-core/llm.ts'
|
|
31
|
+
import { CredentialService } from './runtime-core/credentials.ts'
|
|
32
|
+
import { chatStep } from './runtime-core/chatStep.ts'
|
|
33
|
+
import { toToolDefinitions } from './runtime-core/tools.ts'
|
|
30
34
|
import { createTelemetryFacade, telemetryConfigFromEnv } from './runtime-core/telemetry.ts'
|
|
35
|
+
import { TurnTracer } from './runtime-core/trace.ts'
|
|
31
36
|
import type { CounterName, AllowedLabels } from './telemetry/metrics.ts'
|
|
32
37
|
|
|
33
38
|
const HELP = `用法: car <command> [args]
|
|
@@ -98,8 +103,11 @@ async function main(): Promise<number> {
|
|
|
98
103
|
const [, , cmd, ...rest] = process.argv
|
|
99
104
|
switch (cmd) {
|
|
100
105
|
case 'run': {
|
|
106
|
+
// 1.4-S5(D-13):缺省 = 真实模型路径(chatStep 经 openai-compat 适配器);--demo 保留 stub 演示流。
|
|
107
|
+
// 无模型配置 exit 2 显式报错引导(禁静默降级)。会话/索引/签名门两路径共用。
|
|
101
108
|
const file = rest[0]
|
|
102
109
|
if (!file) { console.error('缺少插件文件参数'); return 2 }
|
|
110
|
+
const isDemo = rest.includes('--demo')
|
|
103
111
|
const t0 = Date.now()
|
|
104
112
|
// 环节 0:装载前签名门(M5-S28 装载接线 + 1.1-S3 配置通道:flag > env > car.config.json > 缺省 warn)
|
|
105
113
|
const sig = resolveSigGate(rest)
|
|
@@ -118,29 +126,164 @@ async function main(): Promise<number> {
|
|
|
118
126
|
// 失败非致命——索引为派生缓存,rebuild-index 兜底语义不变;SQLite 能力缺席 = 显式登记一次后停用)
|
|
119
127
|
const indexUpdater = new IndexUpdater({ dbPath: join(process.cwd(), 'sessions-index.db'), onError: e => console.error(`[index-updater] ${e.message}`) })
|
|
120
128
|
log.attachSink(line => { store.append(line); indexUpdater.record(log.sessionId, line, store.path) })
|
|
129
|
+
// 1.4-S5:插件工具收集(ToolReg 全量进 Map——执行面与声明面同一对象,声明面=授权面红线);
|
|
130
|
+
// setSystemPrompt 收集(last-wins,D-17)
|
|
121
131
|
const hostTools: any[] = []
|
|
132
|
+
let pluginSystemPrompt: string | null = null
|
|
122
133
|
ctx.plugin({ name: plugin.manifest.name, apply: (c) => {
|
|
123
134
|
plugin.bindCore({
|
|
124
135
|
registerTool: (t) => { hostTools.push(t); c.provide('tool:' + t.name, t) },
|
|
125
136
|
getRegisteredTools: () => hostTools,
|
|
137
|
+
setSystemPrompt: (text) => { pluginSystemPrompt = text },
|
|
126
138
|
})
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
139
|
+
if (isDemo) {
|
|
140
|
+
// --demo 演示流专属脚手架(真路径不注入——1.1 规划登记的 demo_tool 撞名边缘就此出清)
|
|
141
|
+
plugin.api.registerTool({
|
|
142
|
+
name: 'demo_tool', run: async () => 'demo-ok',
|
|
143
|
+
})
|
|
144
|
+
}
|
|
130
145
|
} })
|
|
131
146
|
console.log(`[1/5 装配] OK(${Date.now() - t0}ms,工具: ${hostTools.map(t => t.name).join(', ') || '无'})`)
|
|
132
147
|
// 环节 2-3:沙箱探测 + 事件运行 + 停止收口
|
|
133
148
|
const probe = await probeCapabilities()
|
|
134
149
|
const sandbox = new SandboxExecutor({ probe, audit: () => {}, workspace: process.cwd() })
|
|
135
150
|
console.log(`[2/5 沙箱] ${probe.degraded ? `降级(${probe.reason})——Q-04 约束生效` : 'Landlock 就绪'}`)
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
})
|
|
151
|
+
// 工具执行面 Map(T-22 收口:declaredSideEffect 缺省 write)
|
|
152
|
+
const toolsMap = new Map<string, any>(hostTools.map(t => [t.name, {
|
|
153
|
+
declaredSideEffect: t.declaredSideEffect ?? 'write',
|
|
154
|
+
run: t.run,
|
|
155
|
+
...(t.description !== undefined ? { description: t.description } : {}),
|
|
156
|
+
...(t.parameters !== undefined ? { parameters: t.parameters } : {}),
|
|
157
|
+
}]))
|
|
158
|
+
// 取消信号(SIGINT → 中断在途流;runTurn 既有收口落 turnEnd aborted)
|
|
159
|
+
const signal = { aborted: false }
|
|
160
|
+
const onSigint = () => { signal.aborted = true; console.error('\n[取消] SIGINT——正在中断当前 turn…') }
|
|
161
|
+
process.once('SIGINT', onSigint)
|
|
162
|
+
// 1.4-S6:遥测门面(CAR_OTEL_ENDPOINT env 通道;缺省关 = noop 零出站)——turn/step span 挂装配层
|
|
163
|
+
const otel = createTelemetryFacade(telemetryConfigFromEnv())
|
|
164
|
+
const tracer = new TurnTracer(otel)
|
|
165
|
+
let r: Awaited<ReturnType<typeof runTurn>>
|
|
166
|
+
if (isDemo) {
|
|
167
|
+
// --demo:stub 演示流(无模型可跑——五环节叙事与既有测试/CI 承载)
|
|
168
|
+
log.append('user', 'user', 'T0', '请调用 demo_tool 并汇报')
|
|
169
|
+
r = await runTurn({
|
|
170
|
+
log, turnId: 'T0',
|
|
171
|
+
preset: { mode: 'confirm', authorize: async () => { console.log('[授权] 确认模式:放行 demo_tool'); return true } },
|
|
172
|
+
tools: new Map([['demo_tool', { declaredSideEffect: 'write', run: async () => 'demo-ok' } as any]]),
|
|
173
|
+
model: async () => ({ stopReason: 'toolUse' as const, toolCalls: [{ id: 'c1', tool: 'demo_tool', args: {} }] }),
|
|
174
|
+
})
|
|
175
|
+
} else {
|
|
176
|
+
// 真路径:模型配置解析(D-16 优先级 flag > env > config)+ 装配 RuntimeCore
|
|
177
|
+
const cfgLlm = loadCarConfig({ explicitPath: flagValue(rest, '--config') })
|
|
178
|
+
if (cfgLlm.error) { console.error(cfgLlm.error); return 2 }
|
|
179
|
+
const mtFlag = flagValue(rest, '--max-tokens')
|
|
180
|
+
const llm = mergeLlmConfig(process.env, cfgLlm.config, {
|
|
181
|
+
baseUrl: flagValue(rest, '--base-url'),
|
|
182
|
+
model: flagValue(rest, '--model'),
|
|
183
|
+
adapterId: flagValue(rest, '--adapter-id'),
|
|
184
|
+
...(mtFlag !== undefined ? { maxTokens: Number(mtFlag) } : {}),
|
|
185
|
+
})
|
|
186
|
+
const prompt = flagValue(rest, '--prompt')
|
|
187
|
+
if (!llm.baseUrl || !llm.model || prompt === undefined) {
|
|
188
|
+
console.error('CAR-E-LLM-CONFIG: 真实模型路径需要 baseUrl + model + --prompt——')
|
|
189
|
+
console.error(' 配置:car.config.json {"llm":{"baseUrl":"https://…","model":"…"}} / env CAR_LLM_BASE_URL+CAR_LLM_MODEL / flag --base-url --model --prompt')
|
|
190
|
+
console.error(' 凭据:OPENAI_API_KEY(env fallback 需 CAR_ALLOW_ENV_CREDENTIALS=1 或 llm.allowEnvFallback)或 keychain;诊断:car doctor')
|
|
191
|
+
console.error(' (无模型演示流:--demo)')
|
|
192
|
+
return 2
|
|
193
|
+
}
|
|
194
|
+
const modelName = llm.model
|
|
195
|
+
const credService = new CredentialService()
|
|
196
|
+
const core = new RuntimeCore(credService)
|
|
197
|
+
core.bindContext(ctx)
|
|
198
|
+
// 适配器经根作用域 effect 可逆注册(bindContext 必须先于 register——否则走非可逆直注册分支)
|
|
199
|
+
core.registerLlmAdapter(createOpenAICompatAdapter({
|
|
200
|
+
baseUrl: llm.baseUrl,
|
|
201
|
+
credentials: credService,
|
|
202
|
+
provider: 'openai-compat',
|
|
203
|
+
}), { default: true })
|
|
204
|
+
// MCP 工具进 turn(D-18):--mcp <serverId>(可重复;server 启动规格在 car.config.json mcp.servers 登记)
|
|
205
|
+
const mcpIds: string[] = []
|
|
206
|
+
for (let i = 0; i < rest.length; i++) {
|
|
207
|
+
if (rest[i] === '--mcp') {
|
|
208
|
+
const id = rest[++i]
|
|
209
|
+
if (!id) { console.error('缺少 --mcp serverId'); return 2 }
|
|
210
|
+
mcpIds.push(id)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const mcpTransports: import('./mcp/gateway.ts').ClientTransport[] = []
|
|
214
|
+
if (mcpIds.length) {
|
|
215
|
+
const { McpGateway } = await import('./mcp/gateway.ts')
|
|
216
|
+
const { createStdioClientTransport } = await import('./mcp/stdioClient.ts')
|
|
217
|
+
const gw = new McpGateway()
|
|
218
|
+
for (const id of mcpIds) {
|
|
219
|
+
const spec = cfgLlm.config.mcp?.servers?.[id]
|
|
220
|
+
if (!spec) { console.error(`CAR-E-MCP: server "${id}" 未在 car.config.json mcp.servers 登记`); return 2 }
|
|
221
|
+
const transport = createStdioClientTransport({ command: spec.command, args: spec.args })
|
|
222
|
+
mcpTransports.push(transport)
|
|
223
|
+
const mcpTools = await gw.register({ serverId: id, transport, env: spec.env })
|
|
224
|
+
for (const t of mcpTools) {
|
|
225
|
+
const modelName = `mcp_${id}_${t.name}`
|
|
226
|
+
if (toolsMap.has(modelName)) { console.error(`CAR-E-MCP: 工具名冲突 ${modelName}(跨源同名禁静默覆盖)`); return 1 }
|
|
227
|
+
toolsMap.set(modelName, {
|
|
228
|
+
declaredSideEffect: t.declaredSideEffect ?? 'write',
|
|
229
|
+
run: async (args: any) => { const r = await gw.callTool(id, t.name, args); if (!r.ok) throw new Error(r.error ?? 'mcp call failed'); return r.result },
|
|
230
|
+
...(t.description !== undefined ? { description: t.description } : {}),
|
|
231
|
+
...(t.inputSchema !== undefined ? { parameters: t.inputSchema } : {}),
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
console.log(`[MCP] ${id}: ${mcpTools.length} 工具入声明面`)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// PTC 进真实 turn(--ptc):插件工具 Map 即 ToolBridge(声明面=授权面同源快照);
|
|
238
|
+
// run_code 进工具 Map(强制 write);SDK 声明块独立 system 事件(D-17)
|
|
239
|
+
let sdkBlock: string | null = null
|
|
240
|
+
if (rest.includes('--ptc')) {
|
|
241
|
+
const { makePtcToolDefinition, runCode } = await import('./ptc/runCode.ts')
|
|
242
|
+
void runCode
|
|
243
|
+
const { renderSdkFromRegistry } = await import('./ptc/sdk.ts')
|
|
244
|
+
const registry = new Map([...toolsMap].map(([name, t]) => [name, { run: t.run, description: t.description, inputSchema: t.parameters }]))
|
|
245
|
+
sdkBlock = renderSdkFromRegistry(registry)
|
|
246
|
+
const ptc = makePtcToolDefinition({ tools: registry, audit: () => {} })
|
|
247
|
+
toolsMap.set(ptc.name, ptc)
|
|
248
|
+
console.log(`[PTC] run_code 入工具面(桥接 ${registry.size} 工具;SDK 声明块进 system)`)
|
|
249
|
+
}
|
|
250
|
+
// system prompt 落链(D-17):--system flag > 插件 setSystemPrompt(last-wins);SDK 块独立追加
|
|
251
|
+
const systemText = flagValue(rest, '--system') ?? pluginSystemPrompt
|
|
252
|
+
if (systemText) log.append('runtime', 'system', 'system', systemText)
|
|
253
|
+
if (sdkBlock) log.append('runtime', 'system', 'system', sdkBlock)
|
|
254
|
+
log.append('user', 'user', 'T0', prompt)
|
|
255
|
+
const turn = tracer.startTurn('T0')
|
|
256
|
+
let stepNo = 0
|
|
257
|
+
r = await runTurn({
|
|
258
|
+
log, turnId: 'T0',
|
|
259
|
+
preset: { mode: 'confirm', authorize: async (call) => { console.log(`[授权] 确认模式:放行 ${call.tool}`); return true } },
|
|
260
|
+
tools: toolsMap,
|
|
261
|
+
signal,
|
|
262
|
+
model: () => {
|
|
263
|
+
// 1.4-S6:step span 挂每次 model() 调用(parent = turn span;同 turn 共享 traceId)
|
|
264
|
+
const step = turn.stepSpan(++stepNo)
|
|
265
|
+
return chatStep({
|
|
266
|
+
core, log, turnId: 'T0',
|
|
267
|
+
model: modelName,
|
|
268
|
+
tools: toToolDefinitions(toolsMap),
|
|
269
|
+
...(llm.maxTokens !== undefined ? { maxTokens: llm.maxTokens } : {}),
|
|
270
|
+
...(llm.adapterId ? { adapterId: llm.adapterId } : {}),
|
|
271
|
+
signal,
|
|
272
|
+
}).then(
|
|
273
|
+
res => { step.end(res.stopReason); return res },
|
|
274
|
+
e => { step.recordException(e); step.end('error'); throw e },
|
|
275
|
+
)
|
|
276
|
+
},
|
|
277
|
+
})
|
|
278
|
+
turn.end(r.reason)
|
|
279
|
+
}
|
|
280
|
+
process.removeListener('SIGINT', onSigint)
|
|
143
281
|
console.log(`[3/5 收口] turnEnd=${r.reason}(steps=${r.steps})`)
|
|
282
|
+
// 1.4-S6:遥测收口 flush(显式开路径;缺省关 = 空转)
|
|
283
|
+
if (otel.enabled) {
|
|
284
|
+
await otel.shutdown()
|
|
285
|
+
console.error(`[otel] stats=${JSON.stringify(otel.stats())}`)
|
|
286
|
+
}
|
|
144
287
|
// 环节 4:落盘收口(全程逐事件 fsync 已发生,此处仅关句柄核对 + 索引 pending 落盘)
|
|
145
288
|
store.close()
|
|
146
289
|
await indexUpdater.close()
|
|
@@ -179,6 +322,9 @@ async function main(): Promise<number> {
|
|
|
179
322
|
// 1.1-S2 增强:签名就绪行(mode 生效值 / 信任根可解析性 / 配置文件通道发现态;诊断只读)
|
|
180
323
|
const sig = doctorSignature({ cwd: process.cwd(), configPath: flagValue(rest, '--config') })
|
|
181
324
|
console.log(`signature: ${sig.detail}`)
|
|
325
|
+
// 1.4-S3 增强:模型就绪行(baseUrl/model / 凭据通道 / envFallback;值不打印)
|
|
326
|
+
const mr = doctorModelReadiness({ cwd: process.cwd(), configPath: flagValue(rest, '--config') })
|
|
327
|
+
console.log(`model: ${mr.detail}`)
|
|
182
328
|
// M6 增强:registry 连通性(不可达 = SKIPPED 显式跳过,离线不失败)
|
|
183
329
|
const conn = await doctorConnectivity()
|
|
184
330
|
console.log(`connectivity: ${conn.status} — ${conn.detail}${conn.status === 'PASS' ? `(${conn.latencyMs}ms)` : ''}`)
|
package/src/dx/doctor.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { createPublicKey } from 'node:crypto'
|
|
11
11
|
import { CredentialService } from '../runtime-core/credentials.ts'
|
|
12
|
-
import { loadCarConfig, mergeSignatureGate } from '../load/config.ts'
|
|
12
|
+
import { loadCarConfig, mergeSignatureGate, mergeLlmConfig } from '../load/config.ts'
|
|
13
13
|
|
|
14
14
|
export interface CredentialCheck {
|
|
15
15
|
name: string
|
|
@@ -90,6 +90,47 @@ export function doctorSignature(opts: { env?: NodeJS.ProcessEnv; cwd?: string; c
|
|
|
90
90
|
return { mode, trustRoot, configFile, detail: parts.join(';') }
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
export interface ModelReadinessCheck {
|
|
94
|
+
/** baseUrl + model 均在场 = true(可发起真实模型请求的最低配置面) */
|
|
95
|
+
configured: boolean
|
|
96
|
+
baseUrl: string
|
|
97
|
+
model: string
|
|
98
|
+
/** 凭据通道探测(存在性,值不打印):keychain 命中 / env 命中(显式开启时)/ 全落空 */
|
|
99
|
+
credential: 'keychain' | 'env' | 'not-found'
|
|
100
|
+
/** env fallback 开关态(CAR_ALLOW_ENV_CREDENTIALS=1 或 llm.allowEnvFallback) */
|
|
101
|
+
envFallback: 'on' | 'off'
|
|
102
|
+
detail: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 1.4-S3 模型就绪检查(D-16):baseUrl/model 在场性 + 凭据通道存在性探测(离线可过;值不打印) */
|
|
106
|
+
export function doctorModelReadiness(opts: { env?: NodeJS.ProcessEnv; cwd?: string; configPath?: string } = {}): ModelReadinessCheck {
|
|
107
|
+
const env = opts.env ?? process.env
|
|
108
|
+
const cfg = loadCarConfig({ explicitPath: opts.configPath, cwd: opts.cwd })
|
|
109
|
+
const { baseUrl, model, allowEnvFallback } = mergeLlmConfig(env, cfg.error ? {} : cfg.config)
|
|
110
|
+
const envFallback: ModelReadinessCheck['envFallback'] = allowEnvFallback ? 'on' : 'off'
|
|
111
|
+
let credential: ModelReadinessCheck['credential'] = 'not-found'
|
|
112
|
+
if (allowEnvFallback) {
|
|
113
|
+
// env fallback 显式开启:env 变量存在性(OPENAI_API_KEY / CAR_LLM_API_KEY)——只探存在不取值
|
|
114
|
+
if (env.OPENAI_API_KEY || env.CAR_LLM_API_KEY) credential = 'env'
|
|
115
|
+
}
|
|
116
|
+
if (credential === 'not-found') {
|
|
117
|
+
// keychain 通道存在性(resolve 探测,A080001 全落空即 not-found;值不打印)
|
|
118
|
+
try {
|
|
119
|
+
const ref = new CredentialService().resolve('openai-compat', { env, allowEnvFallback })
|
|
120
|
+
credential = ref.source
|
|
121
|
+
} catch { /* 全落空 = not-found(登记口径,非错误) */ }
|
|
122
|
+
}
|
|
123
|
+
const configured = !!baseUrl && !!model
|
|
124
|
+
const parts = [
|
|
125
|
+
configured ? `baseUrl=${baseUrl} model=${model}` : 'baseUrl/model 未配置(car run 真路径需 llm.* 配置或 CAR_LLM_BASE_URL/CAR_LLM_MODEL——见 car doctor / 配置指南)',
|
|
126
|
+
credential === 'keychain' ? '凭据=keychain 命中(car-runtime/openai-compat,值不打印)'
|
|
127
|
+
: credential === 'env' ? '凭据=env 命中(显式开启 fallback,值不打印)'
|
|
128
|
+
: '凭据未就绪(keychain 未命中且 env fallback 未开或变量缺席——A080001 会引导至此)',
|
|
129
|
+
`envFallback=${envFallback}`,
|
|
130
|
+
]
|
|
131
|
+
return { configured, baseUrl: baseUrl ?? '', model: model ?? '', credential, envFallback, detail: parts.join(';') }
|
|
132
|
+
}
|
|
133
|
+
|
|
93
134
|
/**
|
|
94
135
|
* registry 连通性检查:可达 = PASS(含延迟);不可达/显式跳过 = SKIPPED(不失败)。
|
|
95
136
|
* 默认 registry 取 CAR_REGISTRY_URL,缺省 npmjs。
|
|
@@ -105,14 +146,20 @@ export async function doctorConnectivity(opts: {
|
|
|
105
146
|
return { status: 'SKIPPED', detail: `显式离线(CAR_OFFLINE=1 或测试注入)——跳过 ${url}`, latencyMs: 0 }
|
|
106
147
|
}
|
|
107
148
|
const t0 = Date.now()
|
|
149
|
+
// 同 1.4-BUG-2:AbortSignal.timeout → 手动 controller(进程退出 libuv win32 断言预防)
|
|
150
|
+
const controller = new AbortController()
|
|
151
|
+
const abortTimer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 2000)
|
|
108
152
|
try {
|
|
109
|
-
await fetch(url, { signal:
|
|
153
|
+
await fetch(url, { signal: controller.signal })
|
|
110
154
|
return { status: 'PASS', detail: url, latencyMs: Date.now() - t0 }
|
|
111
155
|
} catch (e) {
|
|
156
|
+
clearTimeout(abortTimer)
|
|
112
157
|
return {
|
|
113
158
|
status: 'SKIPPED',
|
|
114
159
|
detail: `registry 不可达(${(e as Error).name}: ${(e as Error).message})——离线环境显式跳过,不判失败`,
|
|
115
160
|
latencyMs: Date.now() - t0,
|
|
116
161
|
}
|
|
162
|
+
} finally {
|
|
163
|
+
clearTimeout(abortTimer)
|
|
117
164
|
}
|
|
118
165
|
}
|