@lqc123qwe/car-runtime 1.3.0 → 1.5.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.
@@ -66,6 +66,8 @@ jobs:
66
66
  - uses: actions/checkout@v4
67
67
  - uses: actions/setup-node@v4
68
68
  with: { node-version: '22.19' }
69
+ # 1.5 起 zod 为常规运行时依赖(D-22)——CLI/doctor 传递加载,测试 job 需 node_modules
70
+ - run: npm install
69
71
  - run: node -p "'G-06 node ' + process.version"
70
72
  - run: node --experimental-sqlite --experimental-transform-types --test "test/*.spec.ts"
71
73
 
@@ -77,6 +79,8 @@ jobs:
77
79
  - uses: actions/checkout@v4
78
80
  - uses: actions/setup-node@v4
79
81
  with: { node-version: '22.19' }
82
+ # 1.5 起 zod 为常规运行时依赖(D-22)——CLI/doctor 传递加载,测试 job 需 node_modules
83
+ - run: npm install
80
84
  - run: node -p "'G-06 node ' + process.version"
81
85
  - run: node --experimental-sqlite --experimental-transform-types --test "test/*.spec.ts"
82
86
 
@@ -88,6 +92,8 @@ jobs:
88
92
  - uses: actions/checkout@v4
89
93
  - uses: actions/setup-node@v4
90
94
  with: { node-version: '22.19' }
95
+ # 1.5 起 zod 为常规运行时依赖(D-22)——CLI/doctor 传递加载,测试 job 需 node_modules
96
+ - run: npm install
91
97
  - run: node -p "'G-06 node ' + process.version"
92
98
  - run: node --experimental-sqlite --experimental-transform-types --test "test/*.spec.ts"
93
99
 
@@ -163,6 +169,8 @@ jobs:
163
169
  - uses: actions/checkout@v4
164
170
  - uses: actions/setup-node@v4
165
171
  with: { node-version: '22.19' }
172
+ # 1.5 起 zod 为常规运行时依赖(D-22)——cli.ts 传递加载,dx-smoke 需 node_modules
173
+ - run: npm install
166
174
  - run: node --experimental-transform-types src/cli.ts doctor
167
175
  - run: node --experimental-transform-types src/cli.ts reload test/fixtures/hello-plugin.ts
168
176
 
@@ -174,6 +182,8 @@ jobs:
174
182
  - uses: actions/checkout@v4
175
183
  - uses: actions/setup-node@v4
176
184
  with: { node-version: '22.19' }
185
+ # 1.5 起 zod 为常规运行时依赖(D-22)——cli.ts 传递加载,dx-smoke 需 node_modules
186
+ - run: npm install
177
187
  - run: node --experimental-transform-types src/cli.ts doctor
178
188
  - run: node --experimental-transform-types src/cli.ts reload test/fixtures/hello-plugin.ts
179
189
 
@@ -185,6 +195,8 @@ jobs:
185
195
  - uses: actions/checkout@v4
186
196
  - uses: actions/setup-node@v4
187
197
  with: { node-version: '22.19' }
198
+ # 1.5 起 zod 为常规运行时依赖(D-22)——cli.ts 传递加载,dx-smoke 需 node_modules
199
+ - run: npm install
188
200
  - run: node --experimental-transform-types src/cli.ts doctor
189
201
  - run: node --experimental-transform-types src/cli.ts reload test/fixtures/hello-plugin.ts
190
202
 
package/README.md CHANGED
@@ -181,11 +181,27 @@ node --experimental-transform-types --test test/*.spec.ts
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
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
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
-
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 真实触发)。
186
+
187
+ ## 1.4 状态(真实模型接入 W1 完整)
188
+
189
+ | `src/cli.ts` | 1.4-S5(D-13)car run 双路径:真路径(Context+RuntimeCore+bindContext→registerLlmAdapter 根作用域 effect → runTurn model 回调=chatStep;SIGINT 取消;SIGINT→signal)+ `--demo` 保留 stub(demo_tool 撞名边缘出清);无模型配置 exit 2 显式引导(CAR-E-LLM-CONFIG) | M8 §1「runTurn 消费接线」出清 |
190
+ | `src/load/config.ts` +2 | 1.4-S3(D-16)`llm.*` 键位(baseUrl/model/adapterId/maxTokens/allowEnvFallback——fail-visible)+ `mcp.servers.*` 键位(D-18;env 禁明文凭据由 gateway 强制检查)+ mergeLlmConfig(flag > env > config) | 1.4 W1-2 |
191
+ | `src/runtime-core/tools.ts` | 1.4-S1 toToolDefinitions(执行面 Map → 模型声明面;T-22 缺省 write 收口;执行面=声明面同一对象红线) | 1.4 W1-3 |
192
+ | `src/load/loader.ts` + `src/loop/stop.ts` | 1.4-S1 ToolReg/ToolDef 加 description/parameters/declaredSideEffect(加法);PluginApi.setSystemPrompt(pending 收集 → bindCore 冲刷,last-wins) | 1.4 W1-3/W1-4 |
193
+ | `src/session/log.ts` + `src/runtime-core/chatStep.ts` | 1.4-S2(D-17)EventKind 加 'system' + deriveMessages 投影 role:system + chatStep 角色映射 + golden 登记(contract-check 4/4);1.4-S4(D-19)**TurnAborted 全库首次实抛**(chunk 间隙 + finish 检查前序——assistant 未落链无半截) | §3.5.5 红线兑现 |
194
+ | `src/runtime-core/llm.ts` + `types.ts` | 1.4-S4:D-12a TLS loopback 豁免(http 仅 127.0.0.1/localhost/[::1]——1.4-BUG-3:URL.hostname IPv6 带方括号)+ LlmRequest.signal 透传三检查点(取消 ≠ 失败:不 retry 不 error chunk);**1.4-BUG-1 修复:出站 fetch 缺 method:'POST'(M8 潜伏——注入式 fetch 单测忽略 method,真 undici 暴露)** | 1.4 W1-5/W1-7 |
195
+ | `src/runtime-core/telemetry.ts` + `trace.ts` | 1.4-S6:startSpan trace 透传(traceId 复用 + parentSpanId)+ TelemetrySpan.spanId + TurnTracer(turn 根 → step 子层级;**挂点装配层**——loop 层零改动微调登记);**1.4-BUG-2 修复:出站默认 node:http(s)(win32 全局 fetch/undici + 进程退出 → libuv 断言崩溃 0xC0000409,Node 23.5 实测/22 不复现)** | M8 §4.4 遥测未入链出清 |
196
+ | `src/mcp/stdioClient.ts` + `src/ptc/runCode.ts` +2 | 1.4-S7(D-18)MCP stdio 客户端传输(spawn + JSON-RPC 行协议 + 30s 超时)+ `--mcp <serverId>` → gateway → 模型名 `mcp_<serverId>_<name>` + callTool 桥;makePtcToolDefinition 补 parameters + `--ptc` 桥(声明面=授权面同源快照)+ **renderSdkFromRegistry 首次生产调用点**(SDK 声明块独立 system 事件) | 1.4 MCP/PTC 块 |
197
+ | `test/s31-real-model-wiring.spec.ts` + `test/s32-telemetry-mcp-ptc.spec.ts` | 1.4 测试:s31 9 测(声明面/system/配置/loopback/取消/CarM8Error/多步真 E2E/spawn 全链/无配置 exit 2)+ s32 3 测(trace 透传/TurnTracer/**car run 全链:MCP 真进程+PTC worker 桥+SDK 出站+trace 父子链**) | 1.4-GO |
198
+
199
+ - **1.4 工程收口(2026-09-27)**:W1 完整四块兑现——**car run 首次真实调模型**(多步工具循环 E2E 全链),模型配置通道(llm.* + env + flags)、system prompt 落链(§3.5.5 红线兑现)、TurnAborted 首次实抛、CarM8Error 保形(A080001 引导文案可达)、TLS loopback 豁免(D-12a 兑现)、MCP/PTC 进真实 turn、遥测 turn/step span 出站。全量 **282(279 PASS + 3 能力门控 skip / 0 fail)** + tsc strict 0 + secrets-scan 105 文件 0 命中。**1.4-BUG-1(fetch 缺 POST)/BUG-2(win32 退出崩溃)/BUG-3(IPv6 方括号)均为生产接线暴露的真缺陷**。
200
+ - **1.4.0 正式发布(2026-09-27,S9 收官,全自动第二次实战)**:npm `@lqc123qwe/car-runtime@1.4.0` 在架——**dist-tags latest → 1.4.0、rc 保留指向 1.0.0-rc.1** + provenance。Release `1.4.0`(agent 经 GCM 凭据 API 创建)→ **j16 run 36328295548 全步 success**(构建/keyless 签名/自动上传五件套/npm publish)→ **post-publish 12 PASS / 1 DRY-RUN / 0 FAIL(G-09 实查)**;外部等价检查 **SHA256 MATCH(`55ce08fd…`)+ cosign keyless Verified OK**。
201
+ - **遗留登记**:anthropic 适配器(冻结);远程 MCP 传输(SSE/HTTP);写类工具双 toolResult 投影观察项;car.config.ts+zod;jiti。规划:`car-docs/10-内核v1/1.4-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.4-收口终验报告.md`。
186
202
  ## 状态
187
203
 
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 收口,发布全链去人工拖拽)**
204
+ - **版本主线**: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 已发布,全链零人工拖拽兑现)** | **1.4 真实模型接入 全量 282(279 PASS + 3 skip,2026-09-27 收口,1.4.0 已发布)**
189
205
  - **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`)在真实发布兑现价值。
190
206
  - **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 清单已全绿收口。
191
207
  - **轨道 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.0",
3
+ "version": "1.5.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",
@@ -24,5 +24,8 @@
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^26.6.1"
27
+ },
28
+ "dependencies": {
29
+ "zod": "^3.25.76"
27
30
  }
28
31
  }
package/src/cli.ts CHANGED
@@ -24,15 +24,23 @@ 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, warmCarConfig } 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, createAnthropicAdapter } 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]
34
- run <plugin.ts> [--sig-enforce] [--config <path>]
39
+ run <plugin.ts> [--sig-enforce] [--config <path>] [--adapter-id <id>]
35
40
  装配并运行插件(快速上手流;装载前签名门 warn 缺省)
41
+ 真路径:--prompt <text> + --base-url/--model(或 llm.* 配置);
42
+ --adapter-id anthropic = Messages API(凭据 ANTHROPIC_API_KEY);
43
+ --demo = stub 演示流
36
44
  reload <file|dir> [--sig-enforce] [--config <path>]
37
45
  热重载插件并打印六阶段加载报告(含 verify 签名门)
38
46
  session verify <file> 哈希链完整性校验(撕裂尾显式报告)
@@ -44,6 +52,8 @@ const HELP = `用法: car <command> [args]
44
52
  session verify/replay/export 均支持 --session <id> [--db <path>]
45
53
  经 SQLite 索引定位会话(1.2;缺省 db = cwd sessions-index.db;索引未收录 = 显式报错)
46
54
  doctor 环境自检
55
+ mcp-serve [--host <id>] [--plugin <file|dir>...] [--config <path>]
56
+ CAR-as-MCP-Server(宿主经 stdio JSON-RPC 接入)
47
57
  plugin-sign keygen [--out <前缀>] [--force]
48
58
  生成 ed25519 签名密钥对(<前缀>.priv PKCS8 DER / <前缀>.pub SPKI base64;缺省前缀 car-release)
49
59
  plugin-sign sign <file...> [--key <priv>]
@@ -52,8 +62,10 @@ const HELP = `用法: car <command> [args]
52
62
  验证签名(信任根序:--trust-root > CAR_TRUST_ROOT > ./car-release.pub;缺签/坏签均 FAIL)
53
63
  签名门环境变量:CAR_SIG_ENFORCE=1 切 enforce;CAR_TRUST_ROOT=<ed25519 spki base64>;
54
64
  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`
65
+ 配置文件通道(1.5 起双载体):cwd car.config.ts(export default {…};发现序 ts > json)或
66
+ car.config.json 或 --config <path>;键位 sandbox.unsigned.allow / sandbox.sig.enforce /
67
+ sandbox.sig.trustRoot / llm.* / mcp.servers.*(stdio: command/args/env;远程: url/headers,
68
+ \${ENV_VAR} 引用注入凭据);优先级 flag > env > 配置 > 缺省;未知键 fail-visible`
57
69
 
58
70
  // 进程内热重载会话(ReloadManager 持有 epoch 与实例注册表;同进程连续 reload 语义完整)
59
71
  let reloadMgrPromise: Promise<import('./load/report.ts').ReloadManager> | undefined
@@ -96,10 +108,15 @@ function resolveSigGate(argv: string[]): { options: import('./load/sigGate.ts').
96
108
 
97
109
  async function main(): Promise<number> {
98
110
  const [, , cmd, ...rest] = process.argv
111
+ // 1.5-S3(D-22):car.config.ts 载体统一预热(--config *.ts 或 cwd 发现序 ts>json;无 TS 载体 = no-op)
112
+ await warmCarConfig({ explicitPath: flagValue(rest, '--config') })
99
113
  switch (cmd) {
100
114
  case 'run': {
115
+ // 1.4-S5(D-13):缺省 = 真实模型路径(chatStep 经 openai-compat 适配器);--demo 保留 stub 演示流。
116
+ // 无模型配置 exit 2 显式报错引导(禁静默降级)。会话/索引/签名门两路径共用。
101
117
  const file = rest[0]
102
118
  if (!file) { console.error('缺少插件文件参数'); return 2 }
119
+ const isDemo = rest.includes('--demo')
103
120
  const t0 = Date.now()
104
121
  // 环节 0:装载前签名门(M5-S28 装载接线 + 1.1-S3 配置通道:flag > env > car.config.json > 缺省 warn)
105
122
  const sig = resolveSigGate(rest)
@@ -118,29 +135,172 @@ async function main(): Promise<number> {
118
135
  // 失败非致命——索引为派生缓存,rebuild-index 兜底语义不变;SQLite 能力缺席 = 显式登记一次后停用)
119
136
  const indexUpdater = new IndexUpdater({ dbPath: join(process.cwd(), 'sessions-index.db'), onError: e => console.error(`[index-updater] ${e.message}`) })
120
137
  log.attachSink(line => { store.append(line); indexUpdater.record(log.sessionId, line, store.path) })
138
+ // 1.4-S5:插件工具收集(ToolReg 全量进 Map——执行面与声明面同一对象,声明面=授权面红线);
139
+ // setSystemPrompt 收集(last-wins,D-17)
121
140
  const hostTools: any[] = []
141
+ let pluginSystemPrompt: string | null = null
122
142
  ctx.plugin({ name: plugin.manifest.name, apply: (c) => {
123
143
  plugin.bindCore({
124
144
  registerTool: (t) => { hostTools.push(t); c.provide('tool:' + t.name, t) },
125
145
  getRegisteredTools: () => hostTools,
146
+ setSystemPrompt: (text) => { pluginSystemPrompt = text },
126
147
  })
127
- plugin.api.registerTool({
128
- name: 'demo_tool', run: async () => 'demo-ok',
129
- })
148
+ if (isDemo) {
149
+ // --demo 演示流专属脚手架(真路径不注入——1.1 规划登记的 demo_tool 撞名边缘就此出清)
150
+ plugin.api.registerTool({
151
+ name: 'demo_tool', run: async () => 'demo-ok',
152
+ })
153
+ }
130
154
  } })
131
155
  console.log(`[1/5 装配] OK(${Date.now() - t0}ms,工具: ${hostTools.map(t => t.name).join(', ') || '无'})`)
132
156
  // 环节 2-3:沙箱探测 + 事件运行 + 停止收口
133
157
  const probe = await probeCapabilities()
134
158
  const sandbox = new SandboxExecutor({ probe, audit: () => {}, workspace: process.cwd() })
135
159
  console.log(`[2/5 沙箱] ${probe.degraded ? `降级(${probe.reason})——Q-04 约束生效` : 'Landlock 就绪'}`)
136
- log.append('user', 'user', 'T0', '请调用 demo_tool 并汇报')
137
- const r = await runTurn({
138
- log, turnId: 'T0',
139
- preset: { mode: 'confirm', authorize: async () => { console.log('[授权] 确认模式:放行 demo_tool'); return true } },
140
- tools: new Map([['demo_tool', { declaredSideEffect: 'write', run: async () => 'demo-ok' } as any]]),
141
- model: async () => ({ stopReason: 'toolUse' as const, toolCalls: [{ id: 'c1', tool: 'demo_tool', args: {} }] }),
142
- })
160
+ // 工具执行面 Map(T-22 收口:declaredSideEffect 缺省 write)
161
+ const toolsMap = new Map<string, any>(hostTools.map(t => [t.name, {
162
+ declaredSideEffect: t.declaredSideEffect ?? 'write',
163
+ run: t.run,
164
+ ...(t.description !== undefined ? { description: t.description } : {}),
165
+ ...(t.parameters !== undefined ? { parameters: t.parameters } : {}),
166
+ }]))
167
+ // 取消信号(SIGINT → 中断在途流;runTurn 既有收口落 turnEnd aborted)
168
+ const signal = { aborted: false }
169
+ const onSigint = () => { signal.aborted = true; console.error('\n[取消] SIGINT——正在中断当前 turn…') }
170
+ process.once('SIGINT', onSigint)
171
+ // 1.4-S6:遥测门面(CAR_OTEL_ENDPOINT env 通道;缺省关 = noop 零出站)——turn/step span 挂装配层
172
+ const otel = createTelemetryFacade(telemetryConfigFromEnv())
173
+ const tracer = new TurnTracer(otel)
174
+ let r: Awaited<ReturnType<typeof runTurn>>
175
+ if (isDemo) {
176
+ // --demo:stub 演示流(无模型可跑——五环节叙事与既有测试/CI 承载)
177
+ log.append('user', 'user', 'T0', '请调用 demo_tool 并汇报')
178
+ r = await runTurn({
179
+ log, turnId: 'T0',
180
+ preset: { mode: 'confirm', authorize: async () => { console.log('[授权] 确认模式:放行 demo_tool'); return true } },
181
+ tools: new Map([['demo_tool', { declaredSideEffect: 'write', run: async () => 'demo-ok' } as any]]),
182
+ model: async () => ({ stopReason: 'toolUse' as const, toolCalls: [{ id: 'c1', tool: 'demo_tool', args: {} }] }),
183
+ })
184
+ } else {
185
+ // 真路径:模型配置解析(D-16 优先级 flag > env > config)+ 装配 RuntimeCore
186
+ const cfgLlm = loadCarConfig({ explicitPath: flagValue(rest, '--config') })
187
+ if (cfgLlm.error) { console.error(cfgLlm.error); return 2 }
188
+ const mtFlag = flagValue(rest, '--max-tokens')
189
+ const llm = mergeLlmConfig(process.env, cfgLlm.config, {
190
+ baseUrl: flagValue(rest, '--base-url'),
191
+ model: flagValue(rest, '--model'),
192
+ adapterId: flagValue(rest, '--adapter-id'),
193
+ ...(mtFlag !== undefined ? { maxTokens: Number(mtFlag) } : {}),
194
+ })
195
+ const prompt = flagValue(rest, '--prompt')
196
+ if (!llm.baseUrl || !llm.model || prompt === undefined) {
197
+ console.error('CAR-E-LLM-CONFIG: 真实模型路径需要 baseUrl + model + --prompt——')
198
+ console.error(' 配置:car.config.ts(export default)或 car.config.json {"llm":{"baseUrl":"https://…","model":"…"}} / env CAR_LLM_BASE_URL+CAR_LLM_MODEL / flag --base-url --model --prompt')
199
+ console.error(' anthropic:llm.adapterId "anthropic"(或 --adapter-id anthropic)+ baseUrl https://api.anthropic.com;凭据 ANTHROPIC_API_KEY')
200
+ console.error(' 凭据:OPENAI_API_KEY / ANTHROPIC_API_KEY(env fallback 需 CAR_ALLOW_ENV_CREDENTIALS=1 或 llm.allowEnvFallback)或 keychain;诊断:car doctor')
201
+ console.error(' (无模型演示流:--demo)')
202
+ return 2
203
+ }
204
+ const modelName = llm.model
205
+ const credService = new CredentialService()
206
+ const core = new RuntimeCore(credService)
207
+ core.bindContext(ctx)
208
+ // 适配器经根作用域 effect 可逆注册(bindContext 必须先于 register——否则走非可逆直注册分支);
209
+ // 1.5-S1(D-20):adapterId='anthropic' → anthropic 适配器(Messages API);缺省 openai-compat 不变
210
+ core.registerLlmAdapter(llm.adapterId === 'anthropic'
211
+ ? createAnthropicAdapter({ baseUrl: llm.baseUrl, credentials: credService, provider: 'anthropic' })
212
+ : createOpenAICompatAdapter({
213
+ baseUrl: llm.baseUrl,
214
+ credentials: credService,
215
+ provider: 'openai-compat',
216
+ }), { default: true })
217
+ // MCP 工具进 turn(D-18):--mcp <serverId>(可重复;server 启动规格在 car.config.json mcp.servers 登记)
218
+ const mcpIds: string[] = []
219
+ for (let i = 0; i < rest.length; i++) {
220
+ if (rest[i] === '--mcp') {
221
+ const id = rest[++i]
222
+ if (!id) { console.error('缺少 --mcp serverId'); return 2 }
223
+ mcpIds.push(id)
224
+ }
225
+ }
226
+ const mcpTransports: import('./mcp/gateway.ts').ClientTransport[] = []
227
+ if (mcpIds.length) {
228
+ const { McpGateway } = await import('./mcp/gateway.ts')
229
+ const { createStdioClientTransport } = await import('./mcp/stdioClient.ts')
230
+ const { createHttpMcpTransport, resolveMcpHeaders } = await import('./mcp/httpClient.ts')
231
+ const gw = new McpGateway()
232
+ for (const id of mcpIds) {
233
+ const spec = cfgLlm.config.mcp?.servers?.[id]
234
+ if (!spec) { console.error(`CAR-E-MCP: server "${id}" 未在配置 mcp.servers 登记`); return 2 }
235
+ // 1.5-S2(D-21):url = 远程 Streamable HTTP(懒 initialize + 会话头);command = stdio 本地进程
236
+ const transport = spec.url
237
+ ? createHttpMcpTransport({ url: spec.url, headers: resolveMcpHeaders(spec.headers ?? {}) })
238
+ : createStdioClientTransport({ command: spec.command!, args: spec.args })
239
+ mcpTransports.push(transport)
240
+ const mcpTools = await gw.register({ serverId: id, transport, env: spec.env })
241
+ for (const t of mcpTools) {
242
+ const modelName = `mcp_${id}_${t.name}`
243
+ if (toolsMap.has(modelName)) { console.error(`CAR-E-MCP: 工具名冲突 ${modelName}(跨源同名禁静默覆盖)`); return 1 }
244
+ toolsMap.set(modelName, {
245
+ declaredSideEffect: t.declaredSideEffect ?? 'write',
246
+ 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 },
247
+ ...(t.description !== undefined ? { description: t.description } : {}),
248
+ ...(t.inputSchema !== undefined ? { parameters: t.inputSchema } : {}),
249
+ })
250
+ }
251
+ console.log(`[MCP] ${id}: ${mcpTools.length} 工具入声明面`)
252
+ }
253
+ }
254
+ // PTC 进真实 turn(--ptc):插件工具 Map 即 ToolBridge(声明面=授权面同源快照);
255
+ // run_code 进工具 Map(强制 write);SDK 声明块独立 system 事件(D-17)
256
+ let sdkBlock: string | null = null
257
+ if (rest.includes('--ptc')) {
258
+ const { makePtcToolDefinition, runCode } = await import('./ptc/runCode.ts')
259
+ void runCode
260
+ const { renderSdkFromRegistry } = await import('./ptc/sdk.ts')
261
+ const registry = new Map([...toolsMap].map(([name, t]) => [name, { run: t.run, description: t.description, inputSchema: t.parameters }]))
262
+ sdkBlock = renderSdkFromRegistry(registry)
263
+ const ptc = makePtcToolDefinition({ tools: registry, audit: () => {} })
264
+ toolsMap.set(ptc.name, ptc)
265
+ console.log(`[PTC] run_code 入工具面(桥接 ${registry.size} 工具;SDK 声明块进 system)`)
266
+ }
267
+ // system prompt 落链(D-17):--system flag > 插件 setSystemPrompt(last-wins);SDK 块独立追加
268
+ const systemText = flagValue(rest, '--system') ?? pluginSystemPrompt
269
+ if (systemText) log.append('runtime', 'system', 'system', systemText)
270
+ if (sdkBlock) log.append('runtime', 'system', 'system', sdkBlock)
271
+ log.append('user', 'user', 'T0', prompt)
272
+ const turn = tracer.startTurn('T0')
273
+ let stepNo = 0
274
+ r = await runTurn({
275
+ log, turnId: 'T0',
276
+ preset: { mode: 'confirm', authorize: async (call) => { console.log(`[授权] 确认模式:放行 ${call.tool}`); return true } },
277
+ tools: toolsMap,
278
+ signal,
279
+ model: () => {
280
+ // 1.4-S6:step span 挂每次 model() 调用(parent = turn span;同 turn 共享 traceId)
281
+ const step = turn.stepSpan(++stepNo)
282
+ return chatStep({
283
+ core, log, turnId: 'T0',
284
+ model: modelName,
285
+ tools: toToolDefinitions(toolsMap),
286
+ ...(llm.maxTokens !== undefined ? { maxTokens: llm.maxTokens } : {}),
287
+ ...(llm.adapterId ? { adapterId: llm.adapterId } : {}),
288
+ signal,
289
+ }).then(
290
+ res => { step.end(res.stopReason); return res },
291
+ e => { step.recordException(e); step.end('error'); throw e },
292
+ )
293
+ },
294
+ })
295
+ turn.end(r.reason)
296
+ }
297
+ process.removeListener('SIGINT', onSigint)
143
298
  console.log(`[3/5 收口] turnEnd=${r.reason}(steps=${r.steps})`)
299
+ // 1.4-S6:遥测收口 flush(显式开路径;缺省关 = 空转)
300
+ if (otel.enabled) {
301
+ await otel.shutdown()
302
+ console.error(`[otel] stats=${JSON.stringify(otel.stats())}`)
303
+ }
144
304
  // 环节 4:落盘收口(全程逐事件 fsync 已发生,此处仅关句柄核对 + 索引 pending 落盘)
145
305
  store.close()
146
306
  await indexUpdater.close()
@@ -179,6 +339,9 @@ async function main(): Promise<number> {
179
339
  // 1.1-S2 增强:签名就绪行(mode 生效值 / 信任根可解析性 / 配置文件通道发现态;诊断只读)
180
340
  const sig = doctorSignature({ cwd: process.cwd(), configPath: flagValue(rest, '--config') })
181
341
  console.log(`signature: ${sig.detail}`)
342
+ // 1.4-S3 增强:模型就绪行(baseUrl/model / 凭据通道 / envFallback;值不打印)
343
+ const mr = doctorModelReadiness({ cwd: process.cwd(), configPath: flagValue(rest, '--config') })
344
+ console.log(`model: ${mr.detail}`)
182
345
  // M6 增强:registry 连通性(不可达 = SKIPPED 显式跳过,离线不失败)
183
346
  const conn = await doctorConnectivity()
184
347
  console.log(`connectivity: ${conn.status} — ${conn.detail}${conn.status === 'PASS' ? `(${conn.latencyMs}ms)` : ''}`)
package/src/dx/doctor.ts CHANGED
@@ -8,8 +8,8 @@
8
8
  * 门禁判定仍在 sigGate/verifier;公钥可打印、信任根值本身不打印
9
9
  */
10
10
  import { createPublicKey } from 'node:crypto'
11
- import { CredentialService } from '../runtime-core/credentials.ts'
12
- import { loadCarConfig, mergeSignatureGate } from '../load/config.ts'
11
+ import { CredentialService, providerEnvVars } from '../runtime-core/credentials.ts'
12
+ import { loadCarConfig, mergeSignatureGate, mergeLlmConfig } from '../load/config.ts'
13
13
 
14
14
  export interface CredentialCheck {
15
15
  name: string
@@ -90,6 +90,49 @@ 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, adapterId, allowEnvFallback } = mergeLlmConfig(env, cfg.error ? {} : cfg.config)
110
+ // 1.5-S4(D-20 配套):adapterId → 凭据 provider 感知(anthropic → ANTHROPIC_API_KEY;缺省 openai-compat)
111
+ const provider = adapterId === 'anthropic' ? 'anthropic' : 'openai-compat'
112
+ const envFallback: ModelReadinessCheck['envFallback'] = allowEnvFallback ? 'on' : 'off'
113
+ let credential: ModelReadinessCheck['credential'] = 'not-found'
114
+ if (allowEnvFallback) {
115
+ // env fallback 显式开启:provider 候选变量存在性(providerEnvVars;只探存在不取值)
116
+ if (providerEnvVars(provider).some(n => env[n])) credential = 'env'
117
+ }
118
+ if (credential === 'not-found') {
119
+ // keychain 通道存在性(resolve 探测,A080001 全落空即 not-found;值不打印)
120
+ try {
121
+ const ref = new CredentialService().resolve(provider, { env, allowEnvFallback })
122
+ credential = ref.source
123
+ } catch { /* 全落空 = not-found(登记口径,非错误) */ }
124
+ }
125
+ const configured = !!baseUrl && !!model
126
+ const parts = [
127
+ configured ? `baseUrl=${baseUrl} model=${model}${adapterId ? ` adapter=${adapterId}` : ''}` : 'baseUrl/model 未配置(car run 真路径需 llm.* 配置或 CAR_LLM_BASE_URL/CAR_LLM_MODEL——见 car doctor / 配置指南)',
128
+ credential === 'keychain' ? `凭据=keychain 命中(car-runtime/${provider},值不打印)`
129
+ : credential === 'env' ? '凭据=env 命中(显式开启 fallback,值不打印)'
130
+ : '凭据未就绪(keychain 未命中且 env fallback 未开或变量缺席——A080001 会引导至此)',
131
+ `envFallback=${envFallback}`,
132
+ ]
133
+ return { configured, baseUrl: baseUrl ?? '', model: model ?? '', credential, envFallback, detail: parts.join(';') }
134
+ }
135
+
93
136
  /**
94
137
  * registry 连通性检查:可达 = PASS(含延迟);不可达/显式跳过 = SKIPPED(不失败)。
95
138
  * 默认 registry 取 CAR_REGISTRY_URL,缺省 npmjs。
@@ -105,14 +148,20 @@ export async function doctorConnectivity(opts: {
105
148
  return { status: 'SKIPPED', detail: `显式离线(CAR_OFFLINE=1 或测试注入)——跳过 ${url}`, latencyMs: 0 }
106
149
  }
107
150
  const t0 = Date.now()
151
+ // 同 1.4-BUG-2:AbortSignal.timeout → 手动 controller(进程退出 libuv win32 断言预防)
152
+ const controller = new AbortController()
153
+ const abortTimer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 2000)
108
154
  try {
109
- await fetch(url, { signal: AbortSignal.timeout(opts.timeoutMs ?? 2000) })
155
+ await fetch(url, { signal: controller.signal })
110
156
  return { status: 'PASS', detail: url, latencyMs: Date.now() - t0 }
111
157
  } catch (e) {
158
+ clearTimeout(abortTimer)
112
159
  return {
113
160
  status: 'SKIPPED',
114
161
  detail: `registry 不可达(${(e as Error).name}: ${(e as Error).message})——离线环境显式跳过,不判失败`,
115
162
  latencyMs: Date.now() - t0,
116
163
  }
164
+ } finally {
165
+ clearTimeout(abortTimer)
117
166
  }
118
167
  }