@lqc123qwe/car-runtime 1.9.0 → 1.10.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.
Files changed (176) hide show
  1. package/README.md +17 -1
  2. package/dist/SHA256SUMS +1 -0
  3. package/dist/authz/authz.d.ts +44 -0
  4. package/dist/authz/authz.js +65 -0
  5. package/dist/car-runtime-1.10.0.tgz +0 -0
  6. package/dist/car-runtime-1.10.0.tgz.sig.bundle +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +825 -0
  9. package/dist/dx/doctor.d.ts +62 -0
  10. package/dist/dx/doctor.js +127 -0
  11. package/dist/governance/dump.d.ts +47 -0
  12. package/dist/governance/dump.js +17 -0
  13. package/dist/host/facade.d.ts +13 -0
  14. package/dist/host/facade.js +154 -0
  15. package/dist/host/hostGateway.d.ts +117 -0
  16. package/dist/host/hostGateway.js +65 -0
  17. package/dist/host/mappings.d.ts +27 -0
  18. package/dist/host/mappings.js +52 -0
  19. package/dist/host/stdio.d.ts +14 -0
  20. package/dist/host/stdio.js +83 -0
  21. package/dist/kernel/context.d.ts +76 -0
  22. package/dist/kernel/context.js +203 -0
  23. package/dist/kernel/events.d.ts +37 -0
  24. package/dist/kernel/events.js +82 -0
  25. package/dist/load/config.d.ts +79 -0
  26. package/dist/load/config.js +285 -0
  27. package/dist/load/loader.d.ts +72 -0
  28. package/dist/load/loader.js +162 -0
  29. package/dist/load/registry.d.ts +44 -0
  30. package/dist/load/registry.js +58 -0
  31. package/dist/load/report.d.ts +98 -0
  32. package/dist/load/report.js +301 -0
  33. package/dist/load/sigGate.d.ts +22 -0
  34. package/dist/load/sigGate.js +68 -0
  35. package/dist/load/sign.d.ts +25 -0
  36. package/dist/load/sign.js +47 -0
  37. package/dist/load/verifier.d.ts +35 -0
  38. package/dist/load/verifier.js +56 -0
  39. package/dist/loop/goal.d.ts +31 -0
  40. package/dist/loop/goal.js +34 -0
  41. package/dist/loop/recover.d.ts +17 -0
  42. package/dist/loop/recover.js +39 -0
  43. package/dist/loop/stop.d.ts +79 -0
  44. package/dist/loop/stop.js +151 -0
  45. package/dist/mcp/gateway.d.ts +96 -0
  46. package/dist/mcp/gateway.js +69 -0
  47. package/dist/mcp/httpClient.d.ts +27 -0
  48. package/dist/mcp/httpClient.js +196 -0
  49. package/dist/mcp/stdioClient.d.ts +8 -0
  50. package/dist/mcp/stdioClient.js +73 -0
  51. package/dist/ptc/budget.d.ts +21 -0
  52. package/dist/ptc/budget.js +36 -0
  53. package/dist/ptc/erasable.d.ts +14 -0
  54. package/dist/ptc/erasable.js +27 -0
  55. package/dist/ptc/runCode.d.ts +65 -0
  56. package/dist/ptc/runCode.js +140 -0
  57. package/dist/ptc/schemaRender.d.ts +15 -0
  58. package/dist/ptc/schemaRender.js +90 -0
  59. package/dist/ptc/sdk.d.ts +27 -0
  60. package/dist/ptc/sdk.js +23 -0
  61. package/dist/ptc/worker-entry.d.ts +1 -0
  62. package/dist/ptc/worker-entry.js +71 -0
  63. package/dist/release-audit.jsonl +4 -0
  64. package/dist/runtime-core/chatStep.d.ts +40 -0
  65. package/dist/runtime-core/chatStep.js +104 -0
  66. package/dist/runtime-core/credentials.d.ts +49 -0
  67. package/dist/runtime-core/credentials.js +118 -0
  68. package/dist/runtime-core/errors.d.ts +30 -0
  69. package/dist/runtime-core/errors.js +36 -0
  70. package/dist/runtime-core/llm.d.ts +76 -0
  71. package/dist/runtime-core/llm.js +553 -0
  72. package/dist/runtime-core/redaction.d.ts +39 -0
  73. package/dist/runtime-core/redaction.js +79 -0
  74. package/dist/runtime-core/telemetry.d.ts +88 -0
  75. package/dist/runtime-core/telemetry.js +332 -0
  76. package/dist/runtime-core/tools.d.ts +15 -0
  77. package/dist/runtime-core/tools.js +12 -0
  78. package/dist/runtime-core/trace.d.ts +55 -0
  79. package/dist/runtime-core/trace.js +137 -0
  80. package/dist/runtime-core/types.d.ts +84 -0
  81. package/dist/runtime-core/types.js +13 -0
  82. package/dist/sandbox/sandbox.d.ts +68 -0
  83. package/dist/sandbox/sandbox.js +153 -0
  84. package/dist/sbom.json +263 -0
  85. package/dist/seam.d.ts +21 -0
  86. package/dist/seam.js +20 -0
  87. package/dist/security/secrets-baseline.d.ts +43 -0
  88. package/dist/security/secrets-baseline.js +176 -0
  89. package/dist/security/secrets.d.ts +45 -0
  90. package/dist/security/secrets.js +138 -0
  91. package/dist/session/export.d.ts +55 -0
  92. package/dist/session/export.js +81 -0
  93. package/dist/session/fork.d.ts +51 -0
  94. package/dist/session/fork.js +63 -0
  95. package/dist/session/format.d.ts +21 -0
  96. package/dist/session/format.js +49 -0
  97. package/dist/session/indexStore.d.ts +44 -0
  98. package/dist/session/indexStore.js +265 -0
  99. package/dist/session/log.d.ts +79 -0
  100. package/dist/session/log.js +157 -0
  101. package/dist/session/store.d.ts +9 -0
  102. package/dist/session/store.js +37 -0
  103. package/dist/telemetry/metrics.d.ts +25 -0
  104. package/dist/telemetry/metrics.js +26 -0
  105. package/package.json +17 -1
  106. package/src/seam.ts +21 -0
  107. package/.gitattributes +0 -11
  108. package/.github/workflows/ci.yml +0 -293
  109. package/.github/workflows/release.yml +0 -54
  110. package/adr/ADR-001.md +0 -4
  111. package/adr/ADR-002.md +0 -5
  112. package/adr/ADR-003.md +0 -35
  113. package/adr/host-onboarding.md +0 -68
  114. package/adr/mlps-audit-checklist.md +0 -41
  115. package/adr/win32-spike.md +0 -45
  116. package/docs/plugin-authoring.md +0 -82
  117. package/native/landlock-run/Makefile +0 -19
  118. package/native/landlock-run/landlock-run.c +0 -162
  119. package/scripts/contract-check.ts +0 -56
  120. package/scripts/invariant-n1.ts +0 -96
  121. package/scripts/package-check.ts +0 -29
  122. package/scripts/ptc-baseline/run-baseline.ts +0 -118
  123. package/scripts/release-artifacts.ts +0 -111
  124. package/scripts/release-pipeline.ts +0 -245
  125. package/scripts/secrets-baseline.ts +0 -33
  126. package/scripts/secrets-scan.ts +0 -56
  127. package/scripts/win32-ffi-debug.ts +0 -143
  128. package/scripts/win32-koffi-spike.ts +0 -537
  129. package/test/contract/eventmap.golden.json +0 -7
  130. package/test/fixtures/hello-plugin.ts +0 -5
  131. package/test/kernel.spec.ts +0 -136
  132. package/test/registry-e2e/run-e2e.ts +0 -213
  133. package/test/s11.spec.ts +0 -129
  134. package/test/s12.spec.ts +0 -135
  135. package/test/s13.spec.ts +0 -117
  136. package/test/s14.spec.ts +0 -117
  137. package/test/s15.spec.ts +0 -80
  138. package/test/s16.spec.ts +0 -133
  139. package/test/s17.spec.ts +0 -102
  140. package/test/s18.spec.ts +0 -81
  141. package/test/s19.spec.ts +0 -138
  142. package/test/s2.spec.ts +0 -245
  143. package/test/s20.spec.ts +0 -241
  144. package/test/s21.spec.ts +0 -231
  145. package/test/s22-m7-store-format.spec.ts +0 -135
  146. package/test/s23-m7-export-index.spec.ts +0 -189
  147. package/test/s24-signature-gate.spec.ts +0 -185
  148. package/test/s25-m8-runtime-core.spec.ts +0 -600
  149. package/test/s26-signature-closeout.spec.ts +0 -349
  150. package/test/s27-host-load-wiring.spec.ts +0 -134
  151. package/test/s28-session-index-followup.spec.ts +0 -236
  152. package/test/s29-otel-producer.spec.ts +0 -252
  153. package/test/s3.spec.ts +0 -179
  154. package/test/s30-release-artifacts.spec.ts +0 -127
  155. package/test/s31-real-model-wiring.spec.ts +0 -329
  156. package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
  157. package/test/s33-anthropic-adapter.spec.ts +0 -354
  158. package/test/s34-mcp-remote-transport.spec.ts +0 -250
  159. package/test/s35-config-ts-zod.spec.ts +0 -145
  160. package/test/s36-host-tool-surface.spec.ts +0 -250
  161. package/test/s37-projection-dedupe.spec.ts +0 -161
  162. package/test/s38-config-jiti.spec.ts +0 -116
  163. package/test/s39-otlp-span-semantics.spec.ts +0 -100
  164. package/test/s4.spec.ts +0 -169
  165. package/test/s40-metrics-cumulative.spec.ts +0 -67
  166. package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
  167. package/test/s42-traceparent-propagation.spec.ts +0 -415
  168. package/test/s43-remote-http-traceparent.spec.ts +0 -262
  169. package/test/s5.spec.ts +0 -95
  170. package/test/s6.spec.ts +0 -169
  171. package/test/s7.spec.ts +0 -136
  172. package/test/s7b-baseline.spec.ts +0 -20
  173. package/test/s9.spec.ts +0 -89
  174. package/test/sandbox-escape/j17-orchestrate.sh +0 -30
  175. package/test/sandbox-escape/run-matrix.sh +0 -98
  176. package/tsconfig.json +0 -18
package/dist/cli.js ADDED
@@ -0,0 +1,825 @@
1
+ /**
2
+ * CAR CLI(N2 DX 基线 / US-1 / §6.5 审计触点)
3
+ *
4
+ * 命令(语义化退出码:0=成功,1=数据校验失败,2=用法/输入不可用):
5
+ * car run <plugin.ts> [--turns N] 快速上手流:装配插件 → 跑一轮对话 → 日志落盘 → 审计摘要
6
+ * car reload <plugin.ts|dir> 热重载:epoch 击穿缓存 → invalidate 旧实例 → 五阶段加载报告
7
+ * car session verify <events.jsonl[.zstd]> 哈希链完整性校验(撕裂尾显式报告;审计员入口)
8
+ * car session replay <events.jsonl[.zstd]> deriveMessages 投影回放(不依赖模型状态)
9
+ * car session export <file> --out <dir> [--zstd] 取证包导出(SQ-06:断链中止;落盘读回重验,离线自证)
10
+ * car session rebuild-index <root> [--db <path>] 从 JSONL 重建 SQLite 会话索引(幂等;断链/坏格式文件入 errors 跳过)
11
+ * car doctor 环境自检(Node 版本/沙箱/凭据/签名/keychain/连通性)
12
+ * car plugin-sign keygen|sign|verify 插件签名工具(1.1-S1:keygen/sign/verify;离线 minisign 轨)
13
+ */
14
+ import { createHash } from 'node:crypto';
15
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
16
+ import { dirname, join } from 'node:path';
17
+ import { probeCapabilities, SandboxExecutor } from './sandbox/sandbox.js';
18
+ import { SessionLog, loadSessionLog } from './session/log.js';
19
+ import { SessionFileStore } from './session/store.js';
20
+ import { compressJsonlZstd, decodeLogBuffer, zstdAvailable } from './session/format.js';
21
+ import { exportForensicsBundle, verifyBundle } from './session/export.js';
22
+ import { lookupSession, IndexUpdater } from './session/indexStore.js';
23
+ import { runTurn } from './loop/stop.js';
24
+ import { Context } from './kernel/context.js';
25
+ import { mountPlugin } from './load/loader.js';
26
+ import { verifyPluginFile } from './load/sigGate.js';
27
+ import { loadCarConfig, mergeSignatureGate, mergeLlmConfig, warmCarConfig } from './load/config.js';
28
+ import { generateSigningKeypair, signPluginFile, resolveTrustRoot } from './load/sign.js';
29
+ import { doctorCredentials, doctorConnectivity, doctorKeychain, doctorSignature, doctorModelReadiness } from './dx/doctor.js';
30
+ import { RuntimeCore, createOpenAICompatAdapter, createAnthropicAdapter } from './runtime-core/llm.js';
31
+ import { CredentialService } from './runtime-core/credentials.js';
32
+ import { chatStep } from './runtime-core/chatStep.js';
33
+ import { toToolDefinitions } from './runtime-core/tools.js';
34
+ import { createTelemetryFacade, telemetryConfigFromEnv } from './runtime-core/telemetry.js';
35
+ import { TurnTracer, parseTraceparent } from './runtime-core/trace.js';
36
+ const HELP = `用法: car <command> [args]
37
+ run <plugin.ts> [--sig-enforce] [--config <path>] [--adapter-id <id>]
38
+ 装配并运行插件(快速上手流;装载前签名门 warn 缺省)
39
+ 真路径:--prompt <text> + --base-url/--model(或 llm.* 配置);
40
+ --adapter-id anthropic = Messages API(凭据 ANTHROPIC_API_KEY);
41
+ --demo = stub 演示流
42
+ reload <file|dir> [--sig-enforce] [--config <path>]
43
+ 热重载插件并打印六阶段加载报告(含 verify 签名门)
44
+ session verify <file> 哈希链完整性校验(撕裂尾显式报告)
45
+ session replay <file> 会话回放(deriveMessages 投影)
46
+ session export <file> --out <dir> [--zstd]
47
+ 取证包导出(断链中止;落盘读回重验,离线自证)
48
+ session rebuild-index <root> [--db <path>]
49
+ 从 JSONL 重建 SQLite 会话索引(幂等)
50
+ session verify/replay/export 均支持 --session <id> [--db <path>]
51
+ 经 SQLite 索引定位会话(1.2;缺省 db = cwd sessions-index.db;索引未收录 = 显式报错)
52
+ doctor 环境自检
53
+ mcp-serve [--host <id>] [--plugin <file|dir>...] [--config <path>]
54
+ CAR-as-MCP-Server(宿主经 stdio JSON-RPC 接入)
55
+ plugin-sign keygen [--out <前缀>] [--force]
56
+ 生成 ed25519 签名密钥对(<前缀>.priv PKCS8 DER / <前缀>.pub SPKI base64;缺省前缀 car-release)
57
+ plugin-sign sign <file...> [--key <priv>]
58
+ 对插件文件签名,写 <file>.minisig sidecar(被签物 = 文件裸字节 sha256 hex;改动文件必须重签)
59
+ plugin-sign verify <file...> [--trust-root <spki base64>]
60
+ 验证签名(信任根序:--trust-root > CAR_TRUST_ROOT > ./car-release.pub;缺签/坏签均 FAIL)
61
+ 签名门环境变量:CAR_SIG_ENFORCE=1 切 enforce;CAR_TRUST_ROOT=<ed25519 spki base64>;
62
+ CAR_UNSIGNED_ALLOW=1 显式声明豁免(warn 缺签路径计数 confirmed=yes,横幅保留)
63
+ 配置文件通道(1.5 起双载体):cwd car.config.ts(export default {…};发现序 ts > json)或
64
+ car.config.json 或 --config <path>;键位 sandbox.unsigned.allow / sandbox.sig.enforce /
65
+ sandbox.sig.trustRoot / llm.* / mcp.servers.*(stdio: command/args/env;远程: url/headers,
66
+ \${ENV_VAR} 引用注入凭据);优先级 flag > env > 配置 > 缺省;未知键 fail-visible`;
67
+ // 进程内热重载会话(ReloadManager 持有 epoch 与实例注册表;同进程连续 reload 语义完整)
68
+ let reloadMgrPromise;
69
+ function getReloadManager() {
70
+ reloadMgrPromise ??= import('./load/report.js').then(m => new m.ReloadManager());
71
+ return reloadMgrPromise;
72
+ }
73
+ /** 取 flag 的值(--config <path> 等;缺位返回 undefined) */
74
+ function flagValue(argv, name) {
75
+ const i = argv.indexOf(name);
76
+ return i >= 0 ? argv[i + 1] : undefined;
77
+ }
78
+ /** mcp-serve 插件源收集:--plugin <file|dir> 可重复 + CAR_PLUGINS env(';' 分隔,Windows 安全) */
79
+ function collectPluginSources(argv) {
80
+ const sources = [];
81
+ for (let i = 0; i < argv.length; i++) {
82
+ if (argv[i] !== '--plugin')
83
+ continue;
84
+ const v = argv[++i];
85
+ if (!v)
86
+ return { sources, error: '缺少 --plugin 路径' };
87
+ sources.push(v);
88
+ }
89
+ for (const s of (process.env.CAR_PLUGINS ?? '').split(';')) {
90
+ const t = s.trim();
91
+ if (t)
92
+ sources.push(t);
93
+ }
94
+ return { sources };
95
+ }
96
+ /**
97
+ * 装载签名门选项装配(1.1-S3 统一入口):flag > env > car.config.json > 缺省 warn。
98
+ * 配置文件 fail-visible:显式路径不存在 / 坏 JSON / 未登记键 → 返回 error 由调用方中止(exit 2)。
99
+ */
100
+ function resolveSigGate(argv) {
101
+ const cfg = loadCarConfig({ explicitPath: flagValue(argv, '--config') });
102
+ if (cfg.error)
103
+ return { options: {}, error: cfg.error };
104
+ return { options: mergeSignatureGate(process.env, cfg.config, argv.includes('--sig-enforce')) };
105
+ }
106
+ async function main() {
107
+ const [, , cmd, ...rest] = process.argv;
108
+ // 1.5-S3(D-22):car.config.ts 载体统一预热(--config *.ts 或 cwd 发现序 ts>json;无 TS 载体 = no-op)
109
+ await warmCarConfig({ explicitPath: flagValue(rest, '--config') });
110
+ switch (cmd) {
111
+ case 'run': {
112
+ // 1.4-S5(D-13):缺省 = 真实模型路径(chatStep 经 openai-compat 适配器);--demo 保留 stub 演示流。
113
+ // 无模型配置 exit 2 显式报错引导(禁静默降级)。会话/索引/签名门两路径共用。
114
+ const file = rest[0];
115
+ if (!file) {
116
+ console.error('缺少插件文件参数');
117
+ return 2;
118
+ }
119
+ const isDemo = rest.includes('--demo');
120
+ const t0 = Date.now();
121
+ // 环节 0:装载前签名门(M5-S28 装载接线 + 1.1-S3 配置通道:flag > env > car.config.json > 缺省 warn)
122
+ const sig = resolveSigGate(rest);
123
+ if (sig.error) {
124
+ console.error(sig.error);
125
+ return 2;
126
+ }
127
+ const g = verifyPluginFile(file, sig.options);
128
+ if (!g.allowed) {
129
+ console.error(`签名门禁拒绝:${g.error ?? 'CAR-E-SIG: rejected'}`);
130
+ return 1;
131
+ }
132
+ if (g.warning)
133
+ console.log(`[0/5 签名] ⚠ ${g.warning}`);
134
+ // 环节 1:装配加载
135
+ const manifest = { name: file.replace(/.*[/\\]/, '').replace(/\.ts$/, ''), version: '0.0.1' };
136
+ const plugin = await mountPlugin({ file, manifest });
137
+ const ctx = new Context();
138
+ const log = new SessionLog('S-' + Date.now().toString(36));
139
+ // §3.2.M7.5「先落日志后放行」:run 流全程逐事件 fsync 落盘(append 返回 = 已过掉电窗口)
140
+ const store = new SessionFileStore(`session-${log.sessionId}.jsonl`);
141
+ // 1.2-S2 W3-2「更新半边」:append 后按行增量入索引(sink 先于事件入内存——行自带 seq/hash;
142
+ // 失败非致命——索引为派生缓存,rebuild-index 兜底语义不变;SQLite 能力缺席 = 显式登记一次后停用)
143
+ const indexUpdater = new IndexUpdater({ dbPath: join(process.cwd(), 'sessions-index.db'), onError: e => console.error(`[index-updater] ${e.message}`) });
144
+ log.attachSink(line => { store.append(line); indexUpdater.record(log.sessionId, line, store.path); });
145
+ // 1.4-S5:插件工具收集(ToolReg 全量进 Map——执行面与声明面同一对象,声明面=授权面红线);
146
+ // setSystemPrompt 收集(last-wins,D-17)
147
+ const hostTools = [];
148
+ let pluginSystemPrompt = null;
149
+ ctx.plugin({ name: plugin.manifest.name, apply: (c) => {
150
+ plugin.bindCore({
151
+ registerTool: (t) => { hostTools.push(t); c.provide('tool:' + t.name, t); },
152
+ getRegisteredTools: () => hostTools,
153
+ setSystemPrompt: (text) => { pluginSystemPrompt = text; },
154
+ });
155
+ if (isDemo) {
156
+ // --demo 演示流专属脚手架(真路径不注入——1.1 规划登记的 demo_tool 撞名边缘就此出清)
157
+ plugin.api.registerTool({
158
+ name: 'demo_tool', run: async () => 'demo-ok',
159
+ });
160
+ }
161
+ } });
162
+ console.log(`[1/5 装配] OK(${Date.now() - t0}ms,工具: ${hostTools.map(t => t.name).join(', ') || '无'})`);
163
+ // 环节 2-3:沙箱探测 + 事件运行 + 停止收口
164
+ const probe = await probeCapabilities();
165
+ const sandbox = new SandboxExecutor({ probe, audit: () => { }, workspace: process.cwd() });
166
+ console.log(`[2/5 沙箱] ${probe.degraded ? `降级(${probe.reason})——Q-04 约束生效` : 'Landlock 就绪'}`);
167
+ // 工具执行面 Map(T-22 收口:declaredSideEffect 缺省 write)
168
+ const toolsMap = new Map(hostTools.map(t => [t.name, {
169
+ declaredSideEffect: t.declaredSideEffect ?? 'write',
170
+ run: t.run,
171
+ ...(t.description !== undefined ? { description: t.description } : {}),
172
+ ...(t.parameters !== undefined ? { parameters: t.parameters } : {}),
173
+ }]));
174
+ // 取消信号(SIGINT → 中断在途流;runTurn 既有收口落 turnEnd aborted)
175
+ const signal = { aborted: false };
176
+ const onSigint = () => { signal.aborted = true; console.error('\n[取消] SIGINT——正在中断当前 turn…'); };
177
+ process.once('SIGINT', onSigint);
178
+ // 1.4-S6:遥测门面(CAR_OTEL_ENDPOINT env 通道;缺省关 = noop 零出站)——turn/step span 挂装配层
179
+ const otel = createTelemetryFacade(telemetryConfigFromEnv());
180
+ const tracer = new TurnTracer(otel);
181
+ let r;
182
+ if (isDemo) {
183
+ // --demo:stub 演示流(无模型可跑——五环节叙事与既有测试/CI 承载)
184
+ log.append('user', 'user', 'T0', '请调用 demo_tool 并汇报');
185
+ r = await runTurn({
186
+ log, turnId: 'T0',
187
+ preset: { mode: 'confirm', authorize: async () => { console.log('[授权] 确认模式:放行 demo_tool'); return true; } },
188
+ tools: new Map([['demo_tool', { declaredSideEffect: 'write', run: async () => 'demo-ok' }]]),
189
+ model: async () => ({ stopReason: 'toolUse', toolCalls: [{ id: 'c1', tool: 'demo_tool', args: {} }] }),
190
+ });
191
+ }
192
+ else {
193
+ // 真路径:模型配置解析(D-16 优先级 flag > env > config)+ 装配 RuntimeCore
194
+ const cfgLlm = loadCarConfig({ explicitPath: flagValue(rest, '--config') });
195
+ if (cfgLlm.error) {
196
+ console.error(cfgLlm.error);
197
+ return 2;
198
+ }
199
+ const mtFlag = flagValue(rest, '--max-tokens');
200
+ const llm = mergeLlmConfig(process.env, cfgLlm.config, {
201
+ baseUrl: flagValue(rest, '--base-url'),
202
+ model: flagValue(rest, '--model'),
203
+ adapterId: flagValue(rest, '--adapter-id'),
204
+ ...(mtFlag !== undefined ? { maxTokens: Number(mtFlag) } : {}),
205
+ });
206
+ const prompt = flagValue(rest, '--prompt');
207
+ if (!llm.baseUrl || !llm.model || prompt === undefined) {
208
+ console.error('CAR-E-LLM-CONFIG: 真实模型路径需要 baseUrl + model + --prompt——');
209
+ 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');
210
+ console.error(' anthropic:llm.adapterId "anthropic"(或 --adapter-id anthropic)+ baseUrl https://api.anthropic.com;凭据 ANTHROPIC_API_KEY');
211
+ console.error(' 凭据:OPENAI_API_KEY / ANTHROPIC_API_KEY(env fallback 需 CAR_ALLOW_ENV_CREDENTIALS=1 或 llm.allowEnvFallback)或 keychain;诊断:car doctor');
212
+ console.error(' (无模型演示流:--demo)');
213
+ return 2;
214
+ }
215
+ const modelName = llm.model;
216
+ const credService = new CredentialService();
217
+ const core = new RuntimeCore(credService);
218
+ core.bindContext(ctx);
219
+ // 适配器经根作用域 effect 可逆注册(bindContext 必须先于 register——否则走非可逆直注册分支);
220
+ // 1.5-S1(D-20):adapterId='anthropic' → anthropic 适配器(Messages API);缺省 openai-compat 不变
221
+ core.registerLlmAdapter(llm.adapterId === 'anthropic'
222
+ ? createAnthropicAdapter({ baseUrl: llm.baseUrl, credentials: credService, provider: 'anthropic' })
223
+ : createOpenAICompatAdapter({
224
+ baseUrl: llm.baseUrl,
225
+ credentials: credService,
226
+ provider: 'openai-compat',
227
+ }), { default: true });
228
+ // MCP 工具进 turn(D-18):--mcp <serverId>(可重复;server 启动规格在 car.config.json mcp.servers 登记)
229
+ const mcpIds = [];
230
+ // 1.8-D29:活跃 turn span 句柄(startTurn 后赋值——MCP 工具闭包运行期取 client span 挂链面)
231
+ let activeTurn = null;
232
+ for (let i = 0; i < rest.length; i++) {
233
+ if (rest[i] === '--mcp') {
234
+ const id = rest[++i];
235
+ if (!id) {
236
+ console.error('缺少 --mcp serverId');
237
+ return 2;
238
+ }
239
+ mcpIds.push(id);
240
+ }
241
+ }
242
+ const mcpTransports = [];
243
+ if (mcpIds.length) {
244
+ const { McpGateway } = await import('./mcp/gateway.js');
245
+ const { createStdioClientTransport } = await import('./mcp/stdioClient.js');
246
+ const { createHttpMcpTransport, resolveMcpHeaders } = await import('./mcp/httpClient.js');
247
+ const gw = new McpGateway();
248
+ for (const id of mcpIds) {
249
+ const spec = cfgLlm.config.mcp?.servers?.[id];
250
+ if (!spec) {
251
+ console.error(`CAR-E-MCP: server "${id}" 未在配置 mcp.servers 登记`);
252
+ return 2;
253
+ }
254
+ // 1.5-S2(D-21):url = 远程 Streamable HTTP(懒 initialize + 会话头);command = stdio 本地进程
255
+ const transport = spec.url
256
+ ? createHttpMcpTransport({ url: spec.url, headers: resolveMcpHeaders(spec.headers ?? {}) })
257
+ : createStdioClientTransport({ command: spec.command, args: spec.args });
258
+ mcpTransports.push(transport);
259
+ const mcpTools = await gw.register({ serverId: id, transport, env: spec.env });
260
+ for (const t of mcpTools) {
261
+ const modelName = `mcp_${id}_${t.name}`;
262
+ if (toolsMap.has(modelName)) {
263
+ console.error(`CAR-E-MCP: 工具名冲突 ${modelName}(跨源同名禁静默覆盖)`);
264
+ return 1;
265
+ }
266
+ toolsMap.set(modelName, {
267
+ declaredSideEffect: t.declaredSideEffect ?? 'write',
268
+ // 1.8-D29:client span(car.mcp.tool 挂 turn)+ traceparent 注入 _meta——
269
+ // noop 门面 traceparent='' → 不注入(线上 JSON-RPC 字节面不变)
270
+ run: async (args) => {
271
+ const span = activeTurn?.mcpToolSpan(id, t.name);
272
+ const tp = span?.traceparent ?? '';
273
+ try {
274
+ const r = await gw.callTool(id, t.name, args, tp ? { traceparent: tp } : {});
275
+ if (!r.ok)
276
+ throw new Error(r.error ?? 'mcp call failed');
277
+ span?.end('ok');
278
+ return r.result;
279
+ }
280
+ catch (e) {
281
+ span?.recordException(e);
282
+ span?.end('error');
283
+ throw e;
284
+ }
285
+ },
286
+ ...(t.description !== undefined ? { description: t.description } : {}),
287
+ ...(t.inputSchema !== undefined ? { parameters: t.inputSchema } : {}),
288
+ });
289
+ }
290
+ console.log(`[MCP] ${id}: ${mcpTools.length} 工具入声明面`);
291
+ }
292
+ }
293
+ // PTC 进真实 turn(--ptc):插件工具 Map 即 ToolBridge(声明面=授权面同源快照);
294
+ // run_code 进工具 Map(强制 write);SDK 声明块独立 system 事件(D-17)
295
+ let sdkBlock = null;
296
+ if (rest.includes('--ptc')) {
297
+ const { makePtcToolDefinition, runCode } = await import('./ptc/runCode.js');
298
+ void runCode;
299
+ const { renderSdkFromRegistry } = await import('./ptc/sdk.js');
300
+ const registry = new Map([...toolsMap].map(([name, t]) => [name, { run: t.run, description: t.description, inputSchema: t.parameters }]));
301
+ sdkBlock = renderSdkFromRegistry(registry);
302
+ const ptc = makePtcToolDefinition({ tools: registry, audit: () => { }, spanBridge: () => activeTurn?.ptcRelay() ?? null });
303
+ toolsMap.set(ptc.name, ptc);
304
+ console.log(`[PTC] run_code 入工具面(桥接 ${registry.size} 工具;SDK 声明块进 system)`);
305
+ }
306
+ // system prompt 落链(D-17):--system flag > 插件 setSystemPrompt(last-wins);SDK 块独立追加
307
+ const systemText = flagValue(rest, '--system') ?? pluginSystemPrompt;
308
+ if (systemText)
309
+ log.append('runtime', 'system', 'system', systemText);
310
+ if (sdkBlock)
311
+ log.append('runtime', 'system', 'system', sdkBlock);
312
+ log.append('user', 'user', 'T0', prompt);
313
+ const turn = tracer.startTurn('T0');
314
+ activeTurn = turn; // 1.8-D29/D-31:MCP 工具闭包 / PTC spanBridge 运行期取挂链面
315
+ let stepNo = 0;
316
+ r = await runTurn({
317
+ log, turnId: 'T0',
318
+ preset: { mode: 'confirm', authorize: async (call) => { console.log(`[授权] 确认模式:放行 ${call.tool}`); return true; } },
319
+ tools: toolsMap,
320
+ signal,
321
+ model: () => {
322
+ // 1.4-S6:step span 挂每次 model() 调用(parent = turn span;同 turn 共享 traceId)
323
+ const step = turn.stepSpan(++stepNo);
324
+ return chatStep({
325
+ core, log, turnId: 'T0',
326
+ model: modelName,
327
+ tools: toToolDefinitions(toolsMap),
328
+ ...(llm.maxTokens !== undefined ? { maxTokens: llm.maxTokens } : {}),
329
+ ...(llm.adapterId ? { adapterId: llm.adapterId } : {}),
330
+ signal,
331
+ }).then(res => { step.end(res.stopReason); return res; }, e => { step.recordException(e); step.end('error'); throw e; });
332
+ },
333
+ });
334
+ turn.end(r.reason);
335
+ }
336
+ process.removeListener('SIGINT', onSigint);
337
+ console.log(`[3/5 收口] turnEnd=${r.reason}(steps=${r.steps})`);
338
+ // 1.4-S6:遥测收口 flush(显式开路径;缺省关 = 空转)
339
+ if (otel.enabled) {
340
+ await otel.shutdown();
341
+ console.error(`[otel] stats=${JSON.stringify(otel.stats())}`);
342
+ }
343
+ // 环节 4:落盘收口(全程逐事件 fsync 已发生,此处仅关句柄核对 + 索引 pending 落盘)
344
+ store.close();
345
+ await indexUpdater.close();
346
+ console.log(`[4/5 落盘] ${store.path}(${log.events.length} 事件逐事件 fsync,哈希链 ${log.verifyChain() === null ? '完整' : '断链!'})`);
347
+ // 环节 5:审计回放
348
+ const replay = log.deriveMessages();
349
+ console.log(`[5/5 回放] ${replay.length} 条消息投影(Model-visible means logged: ${log.assertModelVisibleLogged().ok ? 'PASS' : 'FAIL'})`);
350
+ console.log(`首插件跑通总耗时:${((Date.now() - t0) / 1000).toFixed(1)}s(N2 目标 ≤300s)`);
351
+ return 0;
352
+ }
353
+ case 'reload': {
354
+ const target = rest[0];
355
+ if (!target) {
356
+ console.error('缺少插件文件或目录参数');
357
+ return 2;
358
+ }
359
+ const { formatLoadReport } = await import('./load/report.js');
360
+ const mgr = await getReloadManager();
361
+ // 装载签名门透传(M5-S28 + 1.1-S3 配置通道):flag > env > car.config.json,六阶段报告含 verify
362
+ const sig = resolveSigGate(rest);
363
+ if (sig.error) {
364
+ console.error(sig.error);
365
+ return 2;
366
+ }
367
+ const { report, plugins } = await mgr.reload(target, { signature: sig.options });
368
+ for (const line of formatLoadReport(report))
369
+ console.log(line);
370
+ console.log(`装配插件:${plugins.map(p => `${p.manifest.name}@${p.manifest.version}`).join(', ') || '无'}`);
371
+ return report.stages.some(s => s.status === 'FAIL') ? 1 : 0;
372
+ }
373
+ case 'doctor': {
374
+ const probe = await probeCapabilities();
375
+ console.log(`node: ${process.version}(要求 ≥22.19)`);
376
+ console.log(`sandbox: ${probe.degraded ? `DEGRADED(${probe.reason})` : 'Landlock+seccomp 就绪'}`);
377
+ console.log(`盘加密提示: ${process.platform === 'win32' ? '建议启用 BitLocker' : '建议启用 LUKS/FileVault'}`);
378
+ // M6 增强:凭据存在性(只报 present/not-set,值永不打印)
379
+ for (const c of doctorCredentials()) {
380
+ console.log(`credentials.${c.envVar}: ${c.present ? '已设置(值不打印)' : `未设置 — ${c.hint}`}`);
381
+ }
382
+ // M8 增强:keychain 通道状态(模型凭据读取面;缺席显式降级不静默)
383
+ const kc = doctorKeychain();
384
+ console.log(`keychain: ${kc.available ? '就绪' : `缺席(显式降级)— ${kc.note}`}`);
385
+ // 1.1-S2 增强:签名就绪行(mode 生效值 / 信任根可解析性 / 配置文件通道发现态;诊断只读)
386
+ const sig = doctorSignature({ cwd: process.cwd(), configPath: flagValue(rest, '--config') });
387
+ console.log(`signature: ${sig.detail}`);
388
+ // 1.4-S3 增强:模型就绪行(baseUrl/model / 凭据通道 / envFallback;值不打印)
389
+ const mr = doctorModelReadiness({ cwd: process.cwd(), configPath: flagValue(rest, '--config') });
390
+ console.log(`model: ${mr.detail}`);
391
+ // M6 增强:registry 连通性(不可达 = SKIPPED 显式跳过,离线不失败)
392
+ const conn = await doctorConnectivity();
393
+ console.log(`connectivity: ${conn.status} — ${conn.detail}${conn.status === 'PASS' ? `(${conn.latencyMs}ms)` : ''}`);
394
+ return 0;
395
+ }
396
+ case 'session': {
397
+ const [sub, ...args] = rest;
398
+ const flags = {};
399
+ const positional = [];
400
+ for (let i = 0; i < args.length; i++) {
401
+ const a = args[i];
402
+ if (a === '--zstd')
403
+ flags.zstd = true;
404
+ else if (a === '--out' || a === '--db' || a === '--session')
405
+ flags[a.slice(2)] = args[++i] ?? '';
406
+ else
407
+ positional.push(a);
408
+ }
409
+ // C-02 索引重建(§3.2.M7.5 Step 3「异步可容忍,丢失可 rebuild」;幂等单事务)
410
+ if (sub === 'rebuild-index') {
411
+ const root = positional[0];
412
+ if (!root) {
413
+ console.error('缺少 sessions 根目录参数');
414
+ return 2;
415
+ }
416
+ const { rebuildIndex } = await import('./session/indexStore.js');
417
+ try {
418
+ const r = await rebuildIndex(root, flags.db || join(root, 'sessions-index.db'));
419
+ for (const e of r.errors)
420
+ console.error(`[index-skip] ${e.file}: ${e.reason}`);
421
+ console.log(`索引重建:${r.rows.length} 会话入索引,${r.errors.length} 个文件跳过 → ${r.dbPath}`);
422
+ for (const row of r.rows) {
423
+ console.log(` ${row.sessionId} events=${row.eventCount} encoding=${row.encoding}${row.tornTail ? ' tornTail(半行已显式丢弃)' : ''}`);
424
+ }
425
+ return 0;
426
+ }
427
+ catch (e) {
428
+ console.error(e.message);
429
+ return 2;
430
+ }
431
+ }
432
+ // 1.2-S1 W3-1:--session <id> 经索引定位会话文件(与位置路径互斥,显式拒绝歧义)。
433
+ // db 缺省取 cwd sessions-index.db(缺文件 = CAR-E-INDEX 显式报错提示 rebuild-index);
434
+ // 索引未收录 = 显式报错(「可索引 = 可验证」:断链/坏格式会话不入索引)。
435
+ const sessionFlag = flags.session;
436
+ let file = positional[0];
437
+ if (sessionFlag) {
438
+ if (file) {
439
+ console.error('文件路径与 --session 互斥——请二选一');
440
+ return 2;
441
+ }
442
+ const dbPath = flags.db || join(process.cwd(), 'sessions-index.db');
443
+ let row;
444
+ try {
445
+ row = await lookupSession(dbPath, sessionFlag);
446
+ }
447
+ catch (e) {
448
+ console.error(e.message);
449
+ return 2;
450
+ }
451
+ if (!row) {
452
+ console.error(`会话 "${sessionFlag}" 未收录索引(可索引 = 可验证;断链/坏格式会话不入索引)——可先 car session rebuild-index`);
453
+ return 2;
454
+ }
455
+ file = row.file;
456
+ console.log(`[index] ${sessionFlag} → ${file}(events=${row.eventCount} encoding=${row.encoding})`);
457
+ }
458
+ if (!file) {
459
+ console.error('缺少文件参数');
460
+ return 2;
461
+ }
462
+ if (sub === 'export' && !flags.out) {
463
+ console.error('缺少 --out 目录参数');
464
+ return 2;
465
+ }
466
+ let loaded;
467
+ try {
468
+ loaded = loadSessionLog(file);
469
+ }
470
+ catch (e) {
471
+ console.error(e.message);
472
+ return 2;
473
+ }
474
+ const { log, brokenAt } = loaded;
475
+ if (loaded.tornTail)
476
+ console.error(`[torn-tail] 撕裂尾 ${loaded.tornTailBytes} 字节已显式丢弃(崩溃半行,非断链)`);
477
+ if (sub === 'verify') {
478
+ if (brokenAt !== null) {
479
+ console.error(`断链 @ seq=${brokenAt}(审计中止)`);
480
+ return 1;
481
+ }
482
+ console.log(`哈希链完整:${log.events.length} 事件(encoding=${loaded.encoding})`);
483
+ return 0;
484
+ }
485
+ if (sub === 'replay') {
486
+ if (brokenAt !== null) {
487
+ console.error(`断链 @ seq=${brokenAt}(拒绝回放带病日志)`);
488
+ return 1;
489
+ }
490
+ for (const m of log.deriveMessages())
491
+ console.log(JSON.stringify(m));
492
+ return 0;
493
+ }
494
+ // 取证包导出(§3.2.M7.2 导出行 + SQ-06 时序:verifyHashChain → deriveMessages 可用 → 落盘 → 读回重验)
495
+ if (sub === 'export') {
496
+ const outDir = flags.out;
497
+ if (!outDir) {
498
+ console.error('缺少 --out 目录参数');
499
+ return 2;
500
+ }
501
+ if (brokenAt !== null) {
502
+ console.error(`断链 @ seq=${brokenAt}——导出中止(SQ-06:不产出带病审计包)`);
503
+ return 1;
504
+ }
505
+ const wantZstd = flags.zstd === true;
506
+ if (wantZstd && !zstdAvailable()) {
507
+ console.error('CAR-E-ZSTD: --zstd 需 zstd 能力(22.15+/23.8+ 原生内置,当前 Node 无)');
508
+ return 2;
509
+ }
510
+ const events = log.events;
511
+ const meta = {
512
+ sessionId: log.sessionId,
513
+ exportedBy: process.env.USERNAME ?? process.env.USER ?? 'unknown',
514
+ exportedAt: new Date().toISOString(),
515
+ runtimeVersion: process.version,
516
+ sessionRange: { fromSeq: 0, toSeq: Math.max(events.length - 1, 0) },
517
+ };
518
+ const bundle = exportForensicsBundle(log, meta);
519
+ const eventsFile = bundle.files[0];
520
+ let zbuf;
521
+ if (wantZstd) {
522
+ zbuf = compressJsonlZstd(eventsFile.content);
523
+ bundle.manifest.storage = {
524
+ encoding: 'zstd',
525
+ storedAs: eventsFile.name + '.zstd',
526
+ physicalSha256: createHash('sha256').update(zbuf).digest('hex'),
527
+ };
528
+ // storage 注入后重生成 manifest 自身条目(自证口径:盘上 manifest.json 与 bundle 一致)
529
+ const mf = bundle.files.find(f => f.name === 'manifest.json');
530
+ mf.content = JSON.stringify(bundle.manifest, null, 2);
531
+ mf.sha256 = createHash('sha256').update(mf.content).digest('hex');
532
+ }
533
+ mkdirSync(outDir, { recursive: true });
534
+ const eventsPath = join(outDir, wantZstd ? bundle.manifest.storage.storedAs : eventsFile.name);
535
+ mkdirSync(dirname(eventsPath), { recursive: true });
536
+ if (zbuf)
537
+ writeFileSync(eventsPath, zbuf);
538
+ else
539
+ writeFileSync(eventsPath, eventsFile.content, 'utf-8');
540
+ const mfEntry = bundle.files.find(f => f.name === 'manifest.json');
541
+ const manifestPath = join(outDir, 'manifest.json');
542
+ writeFileSync(manifestPath, mfEntry.content, 'utf-8');
543
+ // 落盘读回重验(离线自证:仅凭导出目录字节,不依赖源文件与运行时状态)
544
+ const diskEvents = readFileSync(eventsPath);
545
+ const diskText = decodeLogBuffer(diskEvents).text;
546
+ const diskManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
547
+ const rechecked = diskText === eventsFile.content &&
548
+ createHash('sha256').update(diskText).digest('hex') === diskManifest.files[0].sha256 &&
549
+ JSON.stringify(diskManifest) === JSON.stringify(bundle.manifest) &&
550
+ (!zbuf || createHash('sha256').update(diskEvents).digest('hex') === diskManifest.storage.physicalSha256) &&
551
+ verifyBundle({ files: [{ name: diskManifest.files[0].name, content: diskText, sha256: diskManifest.files[0].sha256 }], manifest: diskManifest }).ok;
552
+ if (!rechecked) {
553
+ console.error('CAR-E-EXPORT: 落盘读回重验失败——导出目录与清单不自证,已中止');
554
+ return 1;
555
+ }
556
+ console.log(`取证包导出:${log.events.length} 事件 → ${outDir}(链 ${bundle.manifest.chainHead?.slice(0, 8)}…→${bundle.manifest.chainTail?.slice(0, 8)}…,encoding=${wantZstd ? 'zstd' : 'plain'})`);
557
+ console.log(`离线自证 PASS:manifest.json + sessions/${log.sessionId}/events.jsonl${wantZstd ? '.zstd' : ''} 读回重验一致`);
558
+ return 0;
559
+ }
560
+ console.error(HELP);
561
+ return 2;
562
+ }
563
+ case 'mcp-serve': {
564
+ // E-6 宿主实连入口(M4-S18):CAR-as-MCP-Server——宿主(Claude Code/Codex)经 stdio JSON-RPC 接入
565
+ // 1.1-S4 装载接线:--plugin <file|dir>(可重复)/ CAR_PLUGINS env → 六阶段流水线(verify 门先于
566
+ // import())挂载进 Context + car_load_total/car_unsigned_confirmed 全链计数。
567
+ // 1.6-S2(D-25)宿主工具面透传接线:插件工具 Map 注入 RuntimeFacade——tool_list 出声明面
568
+ // (declaredSideEffect 不出站:权限面只进权限门红线)、tool_call 经权限门(write 类 policy
569
+ // 拒绝/readonly 执行)+ 成对落链;跨源同名 CAR-E-DUP fail-closed(1.4-S1 纪律同款)。
570
+ const hostIdx = rest.indexOf('--host');
571
+ const { HOST_MAPPINGS } = await import('./host/mappings.js');
572
+ // 默认宿主取映射表首项(数据驱动,静态架构断言红线:宿主标识只在 mappings 数据文件)
573
+ const hostId = hostIdx >= 0 ? rest[hostIdx + 1] : HOST_MAPPINGS[0].hostId;
574
+ // 宿主白名单数据驱动(静态架构断言红线:宿主标识字符串只允许出现在 mappings 数据文件)
575
+ if (!HOST_MAPPINGS.some(h => h.hostId === hostId)) {
576
+ console.error(`未知宿主 "${hostId}"(可用:${HOST_MAPPINGS.map(h => h.hostId).join(' | ')})`);
577
+ return 2;
578
+ }
579
+ // 1.1-S3 配置通道(--config / cwd car.config.json)+ 签名门选项(flag > env > 配置 > 缺省 warn)
580
+ const sig = resolveSigGate(rest);
581
+ if (sig.error) {
582
+ console.error(sig.error);
583
+ return 2;
584
+ }
585
+ const pluginsArg = collectPluginSources(rest);
586
+ if (pluginsArg.error) {
587
+ console.error(pluginsArg.error);
588
+ return 2;
589
+ }
590
+ const { createCounters, assertZeroContent } = await import('./telemetry/metrics.js');
591
+ // 1.2-S3 W3-3:OTel 门面经 env 通道(CAR_OTEL_ENDPOINT 唯一开关;缺省关 = noop 零出站)+
592
+ // S17 counters **双写桥**——快照面(登记表兜底通道)不变,meter 出站为加法通道;
593
+ // labels 为零内容枚举(metrics.ts AllowedLabels),出站同口径
594
+ const otel = createTelemetryFacade(telemetryConfigFromEnv());
595
+ const counters = createCounters();
596
+ const onCount = (name, labels) => {
597
+ counters.onCount(name, labels);
598
+ if (otel.enabled)
599
+ otel.getMeter().createCounter(name).add(1, labels);
600
+ };
601
+ // 1.6-S2:hostTools 提升到块外——跨源同名查重与 toolMap 装配在装载块之后统一收口
602
+ const hostTools = [];
603
+ if (pluginsArg.sources.length) {
604
+ const { loadPlugins, formatLoadReport } = await import('./load/report.js');
605
+ const { Context } = await import('./kernel/context.js');
606
+ const ctx = new Context();
607
+ for (const src of pluginsArg.sources) {
608
+ const result = await loadPlugins({ source: src, signature: { ...sig.options, onCount: (n, l) => onCount(n, l) } });
609
+ if (result.report.stages.some(s => s.status === 'FAIL')) {
610
+ // 加载期显式失败红线(两模式一致):FAIL = 插件未装上,服务不得静默缺插件启动。
611
+ // DEC-1 warn/enforce 之分仅在签名门缺签路径(warn=横幅放行,enforce=verify FAIL 走此分支)
612
+ for (const line of formatLoadReport(result.report))
613
+ console.error(line);
614
+ console.error('[car mcp-serve] 装载 FAIL——fail-closed 启动中止(warn 仅豁免缺签,不豁免装载失败)');
615
+ return 1;
616
+ }
617
+ for (const w of result.report.warnings)
618
+ console.error(`[car mcp-serve] ⚠ ${w}`);
619
+ for (const p of result.plugins) {
620
+ ctx.plugin({ name: p.manifest.name, apply: c => {
621
+ p.bindCore({
622
+ registerTool: t => { hostTools.push(t); c.provide('tool:' + t.name, t); },
623
+ getRegisteredTools: () => hostTools,
624
+ });
625
+ } });
626
+ }
627
+ const names = result.plugins.map(p => `${p.manifest.name}@${p.manifest.version}`).join(', ');
628
+ console.error(`[car mcp-serve] plugins loaded: ${names}(tools: ${hostTools.map(t => t.name).join(', ') || '无'};装载+工具面透传已接线——1.6-S2)`);
629
+ }
630
+ }
631
+ const { HostGateway } = await import('./host/hostGateway.js');
632
+ const { createRuntimeFacade } = await import('./host/facade.js');
633
+ const { createStdioServer } = await import('./host/stdio.js');
634
+ const profiles = new Map(HOST_MAPPINGS.map(h => [h.hostId, h]));
635
+ // 1.6-S2:跨源同名显式报错(禁静默覆盖——last-wins Map 语义下同名即歧义,fail-closed 启动中止)
636
+ const dupTool = hostTools.find((t, i) => hostTools.findIndex(x => x.name === t.name) !== i);
637
+ if (dupTool) {
638
+ console.error(`CAR-E-DUP: 工具 "${dupTool.name}" 跨源同名——禁静默覆盖(mcp-serve fail-closed 启动中止)`);
639
+ return 1;
640
+ }
641
+ const toolMap = new Map(hostTools.map(t => [t.name, t]));
642
+ const facade = createRuntimeFacade({ profiles, ...(toolMap.size ? { tools: toolMap } : {}) });
643
+ const gw = new HostGateway({ facade, audit: e => onCount('car_registry_decision', { source: 'registry' }) });
644
+ for (const h of HOST_MAPPINGS)
645
+ gw.registerHost({ hostId: h.hostId, profile: h, transport: {} });
646
+ // 1.7-S3(D-28):sessionTurn 挂 turn 级 span(无 step span——mcp-serve 无 model() 循环)。
647
+ // 挂点在装配层(1.4-S6 纪律同款);facade 内部自增派生 turnId(T{n})——调用前不可知,
648
+ // 经 setTurnId 回填;outcome = 归一化 turnEnd reason(枚举面零内容)。缺省关 = noop 句柄零出站。
649
+ const tracer = new TurnTracer(otel);
650
+ const server = createStdioServer(async (tool, args, meta) => {
651
+ if (tool !== 'session_turn')
652
+ return gw.handle(hostId, tool, args);
653
+ // 1.8-D30:远端挂链——_meta.traceparent 合法 → turn span 加入宿主 trace
654
+ // (traceId 复用 + parentSpanId=远端 spanId;畸形 fail-open 新 trace——遥测禁 fail-hard)
655
+ const parent = otel.enabled && meta?.traceparent ? parseTraceparent(meta.traceparent) : null;
656
+ const turn = parent
657
+ ? tracer.startTurn('', { trace: { traceId: parent.traceId, parentSpanId: parent.parentSpanId } })
658
+ : tracer.startTurn('');
659
+ try {
660
+ const r = await gw.handle(hostId, tool, args);
661
+ if (!r.ok) {
662
+ // handle 层吞错为 {ok:false}(显式错误结果不抛出)——span 面以 error 收口留痕
663
+ turn.recordException(new Error(r.error ?? 'host call failed'));
664
+ turn.end('error');
665
+ return r;
666
+ }
667
+ const payload = (r.result ?? {});
668
+ turn.setTurnId(`T${payload.steps ?? 0}`);
669
+ turn.end(String(payload.reason ?? 'completed'));
670
+ return r;
671
+ }
672
+ catch (e) {
673
+ turn.recordException(e);
674
+ turn.end('error');
675
+ throw e;
676
+ }
677
+ });
678
+ const n = await server.serve(process.stdin, process.stdout);
679
+ // 采集窗口收口(登记表兜底通道):快照写 stderr(stdout 为协议通道不可污染)
680
+ const snap = counters.snapshot();
681
+ console.error(`[car mcp-serve] host=${hostId} requests=${n} snapshot=${JSON.stringify(snap)} zeroContent=${assertZeroContent(snap)}`);
682
+ // 1.2-S3:OTel 显式开路径收口 flush(shutdown = flush + 停 interval;缺省关为空转)
683
+ if (otel.enabled) {
684
+ await otel.shutdown();
685
+ console.error(`[car mcp-serve] otel=enabled stats=${JSON.stringify(otel.stats())}`);
686
+ }
687
+ return 0;
688
+ }
689
+ case 'plugin-sign': {
690
+ // 1.1-S1 插件签名工具(指南 §2 node -e 手工流固化):keygen / sign / verify
691
+ // 退出码沿用家规:0=成功,1=数据校验失败(签名/验签不通过),2=用法/输入不可用
692
+ const [sub, ...sargs] = rest;
693
+ const parseFiles = (argv, keyFlag) => {
694
+ const files = [];
695
+ let keyPath;
696
+ let trustRoot;
697
+ for (let i = 0; i < argv.length; i++) {
698
+ const a = argv[i];
699
+ if (keyFlag && a === keyFlag) {
700
+ keyPath = argv[++i];
701
+ if (!keyPath)
702
+ return { files, error: `缺少 ${keyFlag} 路径` };
703
+ }
704
+ else if (a === '--trust-root') {
705
+ trustRoot = argv[++i];
706
+ if (!trustRoot)
707
+ return { files, error: '缺少 --trust-root 值' };
708
+ }
709
+ else if (a.startsWith('--')) {
710
+ return { files, error: `未知选项 ${a}` };
711
+ }
712
+ else
713
+ files.push(a);
714
+ }
715
+ return { files, keyPath, trustRoot };
716
+ };
717
+ if (sub === 'keygen') {
718
+ let prefix = 'car-release';
719
+ let force = false;
720
+ for (let i = 0; i < sargs.length; i++) {
721
+ const a = sargs[i];
722
+ if (a === '--out') {
723
+ prefix = sargs[++i] ?? '';
724
+ if (!prefix) {
725
+ console.error('缺少 --out 前缀');
726
+ return 2;
727
+ }
728
+ }
729
+ else if (a === '--force')
730
+ force = true;
731
+ else if (a.startsWith('--')) {
732
+ console.error(`未知选项 ${a}`);
733
+ return 2;
734
+ }
735
+ else {
736
+ console.error(`多余位置参数 ${a}`);
737
+ return 2;
738
+ }
739
+ }
740
+ const privPath = `${prefix}.priv`;
741
+ const pubPath = `${prefix}.pub`;
742
+ if (!force && (existsSync(privPath) || existsSync(pubPath))) {
743
+ console.error(`已存在 ${privPath} / ${pubPath}——拒绝覆盖(--force 显式覆盖)。注意:覆盖 ≠ 轮换,旧公钥仍在用请先按轮换流程处置`);
744
+ return 2;
745
+ }
746
+ const kp = generateSigningKeypair();
747
+ writeFileSync(privPath, kp.privateKeyDer, { mode: 0o600 });
748
+ writeFileSync(pubPath, kp.publicKeyBase64);
749
+ console.log(`密钥对已生成(前缀 ${prefix}):`);
750
+ console.log(` 私钥 ${privPath}(PKCS8 DER;离线保管,不入仓库——secrets 红线)`);
751
+ console.log(` 公钥 ${pubPath}(SPKI DER base64)`);
752
+ console.log(`信任根(CAR_TRUST_ROOT 值,或 plugin-sign verify --trust-root):`);
753
+ console.log(kp.publicKeyBase64);
754
+ return 0;
755
+ }
756
+ if (sub === 'sign') {
757
+ const p = parseFiles(sargs, '--key');
758
+ if (p.error) {
759
+ console.error(p.error);
760
+ return 2;
761
+ }
762
+ if (!p.files.length) {
763
+ console.error('缺少插件文件参数');
764
+ return 2;
765
+ }
766
+ const keyPath = p.keyPath ?? 'car-release.priv';
767
+ let priv;
768
+ try {
769
+ priv = readFileSync(keyPath);
770
+ }
771
+ catch {
772
+ console.error(`私钥不可读:${keyPath}(先 car plugin-sign keygen 生成,或 --key 指定路径)`);
773
+ return 2;
774
+ }
775
+ let failed = false;
776
+ for (const f of p.files) {
777
+ try {
778
+ const r = signPluginFile(f, priv);
779
+ console.log(`signed ${r.file} fp:${r.manifestHash.slice(0, 12)} → ${r.sidecar}`);
780
+ }
781
+ catch (e) {
782
+ console.error(`sign FAIL ${f} — ${e.message}`);
783
+ failed = true;
784
+ }
785
+ }
786
+ return failed ? 1 : 0;
787
+ }
788
+ if (sub === 'verify') {
789
+ const p = parseFiles(sargs);
790
+ if (p.error) {
791
+ console.error(p.error);
792
+ return 2;
793
+ }
794
+ if (!p.files.length) {
795
+ console.error('缺少插件文件参数');
796
+ return 2;
797
+ }
798
+ // 信任根解析序(与指南 §4 对齐):--trust-root > CAR_TRUST_ROOT env > ./car-release.pub
799
+ const tr = resolveTrustRoot({ flag: p.trustRoot, env: process.env, pubFilePath: 'car-release.pub' });
800
+ if (!tr.value) {
801
+ console.error('无信任根:--trust-root / CAR_TRUST_ROOT / ./car-release.pub 均未提供(keygen 产出的 .pub 即信任根)');
802
+ return 2;
803
+ }
804
+ let failed = false;
805
+ for (const f of p.files) {
806
+ // verify 语义固定 enforce:缺签与坏签同为 FAIL(验签命令不做 warn 放行)
807
+ const g = verifyPluginFile(f, { mode: 'enforce', trustRootPublicKey: tr.value });
808
+ if (g.allowed && !g.warning)
809
+ console.log(`verify: PASS ${f} fp:${g.manifestHash?.slice(0, 12)}(信任根来源 ${tr.source})`);
810
+ else {
811
+ console.error(`verify: FAIL ${f} — ${g.error ?? g.warning ?? 'rejected'}`);
812
+ failed = true;
813
+ }
814
+ }
815
+ return failed ? 1 : 0;
816
+ }
817
+ console.error('用法: car plugin-sign <keygen|sign|verify> [args](--help 查看全部命令)');
818
+ return 2;
819
+ }
820
+ default:
821
+ console.log(HELP);
822
+ return cmd ? 2 : 0;
823
+ }
824
+ }
825
+ main().then(code => process.exit(code)).catch(e => { console.error(e); process.exit(1); });