@lqc123qwe/car-runtime 1.8.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 (178) hide show
  1. package/README.md +29 -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/mcp/gateway.ts +10 -2
  107. package/src/mcp/httpClient.ts +23 -5
  108. package/src/runtime-core/trace.ts +5 -0
  109. package/src/seam.ts +21 -0
  110. package/.gitattributes +0 -11
  111. package/.github/workflows/ci.yml +0 -293
  112. package/.github/workflows/release.yml +0 -54
  113. package/adr/ADR-001.md +0 -4
  114. package/adr/ADR-002.md +0 -5
  115. package/adr/ADR-003.md +0 -35
  116. package/adr/host-onboarding.md +0 -68
  117. package/adr/mlps-audit-checklist.md +0 -41
  118. package/adr/win32-spike.md +0 -45
  119. package/docs/plugin-authoring.md +0 -82
  120. package/native/landlock-run/Makefile +0 -19
  121. package/native/landlock-run/landlock-run.c +0 -162
  122. package/scripts/contract-check.ts +0 -56
  123. package/scripts/invariant-n1.ts +0 -96
  124. package/scripts/package-check.ts +0 -29
  125. package/scripts/ptc-baseline/run-baseline.ts +0 -118
  126. package/scripts/release-artifacts.ts +0 -111
  127. package/scripts/release-pipeline.ts +0 -245
  128. package/scripts/secrets-baseline.ts +0 -33
  129. package/scripts/secrets-scan.ts +0 -56
  130. package/scripts/win32-ffi-debug.ts +0 -143
  131. package/scripts/win32-koffi-spike.ts +0 -537
  132. package/test/contract/eventmap.golden.json +0 -7
  133. package/test/fixtures/hello-plugin.ts +0 -5
  134. package/test/kernel.spec.ts +0 -136
  135. package/test/registry-e2e/run-e2e.ts +0 -213
  136. package/test/s11.spec.ts +0 -129
  137. package/test/s12.spec.ts +0 -135
  138. package/test/s13.spec.ts +0 -117
  139. package/test/s14.spec.ts +0 -117
  140. package/test/s15.spec.ts +0 -80
  141. package/test/s16.spec.ts +0 -133
  142. package/test/s17.spec.ts +0 -102
  143. package/test/s18.spec.ts +0 -81
  144. package/test/s19.spec.ts +0 -138
  145. package/test/s2.spec.ts +0 -245
  146. package/test/s20.spec.ts +0 -241
  147. package/test/s21.spec.ts +0 -231
  148. package/test/s22-m7-store-format.spec.ts +0 -135
  149. package/test/s23-m7-export-index.spec.ts +0 -189
  150. package/test/s24-signature-gate.spec.ts +0 -185
  151. package/test/s25-m8-runtime-core.spec.ts +0 -600
  152. package/test/s26-signature-closeout.spec.ts +0 -349
  153. package/test/s27-host-load-wiring.spec.ts +0 -134
  154. package/test/s28-session-index-followup.spec.ts +0 -236
  155. package/test/s29-otel-producer.spec.ts +0 -252
  156. package/test/s3.spec.ts +0 -179
  157. package/test/s30-release-artifacts.spec.ts +0 -127
  158. package/test/s31-real-model-wiring.spec.ts +0 -329
  159. package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
  160. package/test/s33-anthropic-adapter.spec.ts +0 -354
  161. package/test/s34-mcp-remote-transport.spec.ts +0 -250
  162. package/test/s35-config-ts-zod.spec.ts +0 -145
  163. package/test/s36-host-tool-surface.spec.ts +0 -250
  164. package/test/s37-projection-dedupe.spec.ts +0 -161
  165. package/test/s38-config-jiti.spec.ts +0 -116
  166. package/test/s39-otlp-span-semantics.spec.ts +0 -100
  167. package/test/s4.spec.ts +0 -169
  168. package/test/s40-metrics-cumulative.spec.ts +0 -67
  169. package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
  170. package/test/s42-traceparent-propagation.spec.ts +0 -415
  171. package/test/s5.spec.ts +0 -95
  172. package/test/s6.spec.ts +0 -169
  173. package/test/s7.spec.ts +0 -136
  174. package/test/s7b-baseline.spec.ts +0 -20
  175. package/test/s9.spec.ts +0 -89
  176. package/test/sandbox-escape/j17-orchestrate.sh +0 -30
  177. package/test/sandbox-escape/run-matrix.sh +0 -98
  178. package/tsconfig.json +0 -18
@@ -0,0 +1,285 @@
1
+ /**
2
+ * 1.1-S3 签名配置文件通道 → 1.5-S3 完整形态(car.config.ts + zod,D-22)→ 1.6-S3 jiti 载体(D-24)
3
+ *
4
+ * 口径:
5
+ * - 载体(D-22 + D-24 修订):发现序 **car.config.ts > car.config.json**(同目录两者并存 = ts 胜出,
6
+ * json 忽略——禁静默合并);显式 --config 按扩展名分派(.ts 走 TS 载体,其余 JSON 解析);
7
+ * TS 载体 = **jiti 自包含转译**(1.6-S3——零传递依赖 bundled 形态,第二个常规运行时依赖;
8
+ * D-22「jiti 继续登记不引入」到期出清):Node 22.19+ 加载 car.config.ts 不再依赖
9
+ * --experimental-transform-types,非可擦除语法(enum 等 transform-only 形态)可用——
10
+ * erasable-only 预检登记项随之出清(被取代);插件加载的原生类型剥离纪律不动。
11
+ * 形态 = `export default {…}`(非对象/缺 default 显式报错)。
12
+ * - **预热协议**:loadCarConfig 保持同步签名(doctor/resolveSigGate 等既有调用面不动);
13
+ * car.config.ts 须先经 warmCarConfig 异步预热入缓存(CLI 入口统一预热),未预热显式报错
14
+ * (fail-visible,禁静默回退 json——「配置写了但不生效」的漂移面必须显式化)。
15
+ * - 校验层 zod 化(D-11a ① 修订):zod ^3 为首个常规运行时依赖(用户点名登记项);
16
+ * fail-visible 语义与 CAR-E-CONFIG 错误码保持,zod issues 渲染为 `路径 原因`(首个 issue);
17
+ * 键位空间与 1.1/1.4 完全一致(sandbox.* / llm.* / mcp.*;mcp.servers 本迭代加 url/headers)。
18
+ * 未登记键 / 类型错 / 坏 JSON / 坏 TS 一律 CAR-E-CONFIG 显式拒绝(禁静默忽略)。
19
+ * - 优先级(1.1 规划 §3 S3):flag > env > 配置文件 > 缺省。env 已定义即为显式意见。
20
+ * 无配置文件 + 无 env 时行为与 1.0 完全一致(warn / 无信任根 / 不豁免)。
21
+ * - 本模块只读配置与合并优先级;门禁判定在 sigGate/verifier。发现序:显式路径 > cwd。
22
+ * - 代码求值面(D-22 登记):car.config.ts 为本机文件信任边界(与插件同域;配置无签名机制)。
23
+ */
24
+ import { existsSync, readFileSync } from 'node:fs';
25
+ import { isAbsolute, join } from 'node:path';
26
+ import { z } from 'zod';
27
+ export const CONFIG_FILENAME = 'car.config.json';
28
+ export const CONFIG_TS_FILENAME = 'car.config.ts';
29
+ // ==================== zod 校验层(1.5-S3 迁移;错误消息含 s31/s34 回归锚点) ====================
30
+ // 叶子消息不含路径——渲染层统一拼 `${path} ${message}`(s31 锚点:'llm.maxTokens 必须为正整数')
31
+ const nonEmptyTrimmed = () => z.string({ invalid_type_error: '必须为非空字符串', required_error: '必须为非空字符串' })
32
+ .trim()
33
+ .refine(v => v.length > 0, { message: '必须为非空字符串' });
34
+ const stringMap = () => z.record(z.string(), z.string({ invalid_type_error: '必须为字符串' }), { invalid_type_error: '必须为字符串映射', required_error: '必须为字符串映射' });
35
+ const mcpServerSchema = z.object({
36
+ command: nonEmptyTrimmed().optional(),
37
+ url: nonEmptyTrimmed().refine(v => /^https?:\/\//.test(v), { message: '必须为 http(s) URL' }).optional(),
38
+ args: z.array(z.string({ invalid_type_error: '必须为字符串' }), { invalid_type_error: '必须为字符串数组' }).optional(),
39
+ env: stringMap().optional(),
40
+ headers: stringMap().optional(),
41
+ }).strict().superRefine((obj, ctx) => {
42
+ const hasCommand = typeof obj.command === 'string' && !!obj.command;
43
+ const hasUrl = typeof obj.url === 'string' && !!obj.url;
44
+ if (hasCommand === hasUrl) {
45
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [], message: '需要 command(stdio)或 url(远程 Streamable HTTP)恰好其一(互斥)' });
46
+ return;
47
+ }
48
+ if (obj.args !== undefined && !hasCommand)
49
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['args'], message: '仅 stdio 通道(command)适用' });
50
+ if (obj.env !== undefined && !hasCommand)
51
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['env'], message: '仅 stdio 通道(command)适用' });
52
+ if (obj.headers !== undefined && !hasUrl)
53
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['headers'], message: '仅远程通道(url)适用' });
54
+ });
55
+ const rawSchema = z.object({
56
+ sandbox: z.object({
57
+ unsigned: z.object({
58
+ allow: z.boolean({ invalid_type_error: '必须为布尔', required_error: '必须为布尔' }).optional(),
59
+ }).strict().optional(),
60
+ sig: z.object({
61
+ enforce: z.boolean({ invalid_type_error: '必须为布尔', required_error: '必须为布尔' }).optional(),
62
+ trustRoot: nonEmptyTrimmed().optional(),
63
+ }).strict().optional(),
64
+ }).strict().optional(),
65
+ llm: z.object({
66
+ baseUrl: nonEmptyTrimmed().optional(),
67
+ model: nonEmptyTrimmed().optional(),
68
+ adapterId: nonEmptyTrimmed().optional(),
69
+ maxTokens: z.number({ invalid_type_error: '必须为正整数', required_error: '必须为正整数' })
70
+ .refine(v => Number.isFinite(v) && Number.isInteger(v) && v > 0, { message: '必须为正整数' })
71
+ .optional(),
72
+ allowEnvFallback: z.boolean({ invalid_type_error: '必须为布尔', required_error: '必须为布尔' }).optional(),
73
+ }).strict().optional(),
74
+ mcp: z.object({
75
+ servers: z.record(z.string(), mcpServerSchema, { invalid_type_error: '必须为对象(serverId → 启动规格)' }),
76
+ }).strict().optional(),
77
+ }, { invalid_type_error: '配置顶层必须为 JSON 对象' }).strict();
78
+ /** 未登记键提示(渲染层拼 path)——与系统设计键位空间同步 */
79
+ const KEY_SPACES = '登记键位:sandbox.* / llm.* / mcp.servers.*';
80
+ /** zod issue → CAR-E-CONFIG 渲染(首个 issue;unrecognized_keys 按键路径展开) */
81
+ function renderIssue(error) {
82
+ const iss = error.issues[0];
83
+ if (iss.code === 'unrecognized_keys') {
84
+ const keys = iss.keys ?? [];
85
+ const keyPath = [...iss.path, keys[0] ?? '?'].join('.');
86
+ return iss.path.length
87
+ ? `CAR-E-CONFIG: 未知键 ${keyPath}(${KEY_SPACES})`
88
+ : `CAR-E-CONFIG: 未知顶层键 "${keys[0] ?? '?'}"(${KEY_SPACES})`;
89
+ }
90
+ const p = iss.path.join('.');
91
+ return p ? `CAR-E-CONFIG: ${p} ${iss.message}` : `CAR-E-CONFIG: ${iss.message}`;
92
+ }
93
+ /** zod 解析 → CarConfig(扁平渲染:sandbox 嵌套键拍平——mergeSignatureGate 消费形态不变) */
94
+ function parseCarConfig(raw) {
95
+ const parsed = rawSchema.safeParse(raw);
96
+ if (!parsed.success)
97
+ return { config: {}, error: renderIssue(parsed.error) };
98
+ const d = parsed.data;
99
+ const out = {};
100
+ if (d.sandbox) {
101
+ const sb = {};
102
+ if (d.sandbox.unsigned?.allow !== undefined)
103
+ sb.unsignedAllow = d.sandbox.unsigned.allow;
104
+ if (d.sandbox.sig?.enforce !== undefined)
105
+ sb.sigEnforce = d.sandbox.sig.enforce;
106
+ if (d.sandbox.sig?.trustRoot !== undefined)
107
+ sb.sigTrustRoot = d.sandbox.sig.trustRoot;
108
+ out.sandbox = sb;
109
+ }
110
+ if (d.llm) {
111
+ const llm = {};
112
+ if (d.llm.baseUrl !== undefined)
113
+ llm.baseUrl = d.llm.baseUrl;
114
+ if (d.llm.model !== undefined)
115
+ llm.model = d.llm.model;
116
+ if (d.llm.adapterId !== undefined)
117
+ llm.adapterId = d.llm.adapterId;
118
+ if (d.llm.maxTokens !== undefined)
119
+ llm.maxTokens = d.llm.maxTokens;
120
+ if (d.llm.allowEnvFallback !== undefined)
121
+ llm.allowEnvFallback = d.llm.allowEnvFallback;
122
+ out.llm = llm;
123
+ }
124
+ if (d.mcp) {
125
+ const servers = {};
126
+ for (const [sid, sv] of Object.entries(d.mcp.servers)) {
127
+ const spec = {};
128
+ if (sv.command !== undefined)
129
+ spec.command = sv.command;
130
+ if (sv.url !== undefined)
131
+ spec.url = sv.url;
132
+ if (sv.args !== undefined)
133
+ spec.args = sv.args;
134
+ if (sv.env !== undefined)
135
+ spec.env = sv.env;
136
+ if (sv.headers !== undefined)
137
+ spec.headers = sv.headers;
138
+ servers[sid] = spec;
139
+ }
140
+ out.mcp = { servers };
141
+ }
142
+ return { config: out };
143
+ }
144
+ // ==================== 载体装载(JSON 同步 / TS 预热缓存) ====================
145
+ /** car.config.ts 预热缓存(path → 装载结果;warmCarConfig 每次以新 epoch 查询串击穿模块缓存) */
146
+ const tsCache = new Map();
147
+ let tsEpoch = 0;
148
+ /**
149
+ * 1.5-S3(D-22)→ 1.6-S3(D-24):car.config.ts 载体预热。
150
+ * jiti 自包含转译(每 epoch 新实例 + moduleCache 关 = 击穿语义与 1.5 等价)——
151
+ * Node 22.19+ 无需 --experimental-transform-types;直接调用 loadCarConfig 的库方
152
+ * 在存在 car.config.ts 时须先 await 本函数(CLI 入口统一预热,协议不变)。
153
+ */
154
+ export async function warmCarConfig(opts = {}) {
155
+ let path;
156
+ if (opts.explicitPath) {
157
+ if (!opts.explicitPath.endsWith('.ts'))
158
+ return; // JSON 显式路径走同步通道,无需预热
159
+ path = isAbsolute(opts.explicitPath) ? opts.explicitPath : join(opts.cwd ?? process.cwd(), opts.explicitPath);
160
+ }
161
+ else {
162
+ const candidate = join(opts.cwd ?? process.cwd(), CONFIG_TS_FILENAME);
163
+ if (!existsSync(candidate))
164
+ return; // cwd 发现序:无 ts 载体 = 无需预热(json 同步可读)
165
+ path = candidate;
166
+ }
167
+ tsEpoch++;
168
+ const result = await loadTsConfigFile(path, tsEpoch);
169
+ tsCache.set(path, result);
170
+ }
171
+ async function loadTsConfigFile(path, _epoch) {
172
+ try {
173
+ // D-24:jiti 转译(零传递依赖 bundled);每 epoch 新实例 + moduleCache 关——击穿语义保持;
174
+ // interopDefault 显式关(jiti 缺省 true 会把无 default 模块的 exports 整体当 default 返回,
175
+ // 「缺 default export」fail-visible 校验即失效)
176
+ const { createJiti } = await import('jiti');
177
+ const jiti = createJiti(import.meta.url, { moduleCache: false, fsCache: false, interopDefault: false });
178
+ const mod = await jiti.import(path);
179
+ const raw = mod.default;
180
+ if (raw === undefined) {
181
+ return { path, config: {}, error: `CAR-E-CONFIG: ${path} 缺 default export(car.config.ts 形态 = export default {…})` };
182
+ }
183
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
184
+ return { path, config: {}, error: `CAR-E-CONFIG: ${path} default export 必须为对象(配置顶层)` };
185
+ }
186
+ const v = parseCarConfig(raw);
187
+ return v.error ? { path, config: {}, error: v.error } : { path, config: v.config };
188
+ }
189
+ catch (e) {
190
+ const err = e;
191
+ return { path, config: {}, error: `CAR-E-CONFIG: car.config.ts 载入失败(${err.message ?? String(e)})——jiti 转译失败显式拒绝,不静默忽略` };
192
+ }
193
+ }
194
+ /** 加载配置:显式路径(必须存在,否则报错)或 cwd 发现(未发现 = 正常态返回空配置) */
195
+ export function loadCarConfig(opts = {}) {
196
+ if (opts.explicitPath) {
197
+ const path = isAbsolute(opts.explicitPath) ? opts.explicitPath : join(opts.cwd ?? process.cwd(), opts.explicitPath);
198
+ if (path.endsWith('.ts')) {
199
+ const hit = tsCache.get(path);
200
+ if (hit)
201
+ return hit;
202
+ return { path, config: {}, error: `CAR-E-CONFIG: car.config.ts 未预热(CLI 入口自动 warmCarConfig;直接调用方须先 await warmCarConfig({ explicitPath }))` };
203
+ }
204
+ if (!existsSync(path)) {
205
+ return { path, config: {}, error: `CAR-E-CONFIG: 配置文件不存在:${path}(--config 显式指定的路径必须存在)` };
206
+ }
207
+ return loadJsonConfigFile(path);
208
+ }
209
+ // cwd 发现序(D-22):car.config.ts > car.config.json(并存 = ts 胜出,json 忽略)
210
+ const tsPath = join(opts.cwd ?? process.cwd(), CONFIG_TS_FILENAME);
211
+ if (existsSync(tsPath)) {
212
+ const hit = tsCache.get(tsPath);
213
+ if (hit)
214
+ return hit;
215
+ return { path: tsPath, config: {}, error: `CAR-E-CONFIG: car.config.ts 未预热(CLI 入口自动 warmCarConfig;直接调用方须先 await warmCarConfig({ cwd }))` };
216
+ }
217
+ const jsonPath = join(opts.cwd ?? process.cwd(), CONFIG_FILENAME);
218
+ if (!existsSync(jsonPath))
219
+ return { config: {} };
220
+ return loadJsonConfigFile(jsonPath);
221
+ }
222
+ function loadJsonConfigFile(path) {
223
+ let raw;
224
+ try {
225
+ raw = JSON.parse(readFileSync(path, 'utf-8'));
226
+ }
227
+ catch (e) {
228
+ return { path, config: {}, error: `CAR-E-CONFIG: 配置文件解析失败(${e.message})——坏 JSON 显式拒绝,不静默忽略` };
229
+ }
230
+ const v = parseCarConfig(raw);
231
+ return v.error ? { path, config: {}, error: v.error } : { path, config: v.config };
232
+ }
233
+ /**
234
+ * 1.4-S3(D-16):模型接入配置合并(flag > env > 配置 > 缺省)。
235
+ * env 已定义为显式意见(压过配置),未定义落配置层——与 mergeSignatureGate 同纪律。
236
+ * 凭据不在此层(CR-05):走 CredentialService(keychain → env OPENAI_API_KEY/ANTHROPIC_API_KEY/
237
+ * CAR_LLM_API_KEY,env fallback 需显式开启 = CAR_ALLOW_ENV_CREDENTIALS=1 或 llm.allowEnvFallback)。
238
+ */
239
+ export function mergeLlmConfig(env, cfg, flags = {}) {
240
+ let baseUrl = cfg.llm?.baseUrl;
241
+ if (env.CAR_LLM_BASE_URL)
242
+ baseUrl = env.CAR_LLM_BASE_URL;
243
+ if (flags.baseUrl)
244
+ baseUrl = flags.baseUrl;
245
+ let model = cfg.llm?.model;
246
+ if (env.CAR_LLM_MODEL)
247
+ model = env.CAR_LLM_MODEL;
248
+ if (flags.model)
249
+ model = flags.model;
250
+ let adapterId = cfg.llm?.adapterId;
251
+ if (flags.adapterId)
252
+ adapterId = flags.adapterId;
253
+ let maxTokens = cfg.llm?.maxTokens;
254
+ if (flags.maxTokens !== undefined)
255
+ maxTokens = flags.maxTokens;
256
+ let allowEnvFallback = cfg.llm?.allowEnvFallback === true;
257
+ if (env.CAR_ALLOW_ENV_CREDENTIALS !== undefined)
258
+ allowEnvFallback = env.CAR_ALLOW_ENV_CREDENTIALS === '1';
259
+ return {
260
+ ...(baseUrl ? { baseUrl } : {}),
261
+ ...(model ? { model } : {}),
262
+ ...(adapterId ? { adapterId } : {}),
263
+ ...(maxTokens !== undefined ? { maxTokens } : {}),
264
+ allowEnvFallback,
265
+ };
266
+ }
267
+ /**
268
+ * 优先级合并(flag > env > 配置 > 缺省):产出装载签名门选项。
269
+ * flagEnforce = CLI --sig-enforce;env 已定义即为显式意见(压过配置),未定义落配置层。
270
+ */
271
+ export function mergeSignatureGate(env, cfg, flagEnforce) {
272
+ // cfg 为校验器产出的扁平形状(sandbox.{unsignedAllow,sigEnforce,sigTrustRoot},JSON 嵌套键已拍平)
273
+ let mode = cfg.sandbox?.sigEnforce === true ? 'enforce' : 'warn';
274
+ if (env.CAR_SIG_ENFORCE !== undefined)
275
+ mode = env.CAR_SIG_ENFORCE === '1' ? 'enforce' : 'warn';
276
+ if (flagEnforce)
277
+ mode = 'enforce';
278
+ let trustRootPublicKey = cfg.sandbox?.sigTrustRoot;
279
+ if (env.CAR_TRUST_ROOT)
280
+ trustRootPublicKey = env.CAR_TRUST_ROOT;
281
+ let unsignedAllow = cfg.sandbox?.unsignedAllow === true;
282
+ if (env.CAR_UNSIGNED_ALLOW !== undefined)
283
+ unsignedAllow = env.CAR_UNSIGNED_ALLOW === '1';
284
+ return { mode, trustRootPublicKey: trustRootPublicKey || undefined, unsignedAllow };
285
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * F10 · peer 版本约束校验(严格模式默认 + 显式豁免,经中间确认⑥)
3
+ * 冲突 = 加载期硬报错并输出冲突链;豁免需 manifest.peerPolicyOverride(reason 必填→审计留痕)
4
+ */
5
+ export interface PeerViolation {
6
+ peer: string;
7
+ range: string;
8
+ installed: string | null;
9
+ optional: boolean;
10
+ }
11
+ /** 纯收集:仅计算违规项,不抛错(M6 加载报告用——冲突链需结构化定位而非只有异常消息) */
12
+ export declare function computePeerViolations(manifest: Manifest, installed: Map<string, string>): PeerViolation[];
13
+ export declare function validatePeers(manifest: Manifest, installed: Map<string, string>): {
14
+ exempted: boolean;
15
+ violations: PeerViolation[];
16
+ };
17
+ export interface Manifest {
18
+ name: string;
19
+ version: string;
20
+ peers?: Array<{
21
+ peer: string;
22
+ range: string;
23
+ optional?: boolean;
24
+ }>;
25
+ peerPolicyOverride?: {
26
+ relaxed: true;
27
+ reason: string;
28
+ };
29
+ }
30
+ export declare function parseManifest(raw: unknown, file?: string): Manifest;
31
+ export interface ToolReg {
32
+ name: string;
33
+ run: (args: any) => Promise<unknown>;
34
+ /** 工具描述(进模型 function.description) */
35
+ description?: string;
36
+ /** 入参 JSON Schema(进模型 function.parameters) */
37
+ parameters?: unknown;
38
+ /** 副作用声明(缺省 write 收口——T-22 同款:未声明按最高约束) */
39
+ declaredSideEffect?: 'readonly' | 'write';
40
+ }
41
+ export interface CommandReg {
42
+ name: string;
43
+ run: (args: string[]) => Promise<void>;
44
+ }
45
+ export interface PluginApi {
46
+ registerTool(tool: ToolReg): void;
47
+ registerCommand(cmd: CommandReg): void;
48
+ getRegisteredTools(): ToolReg[];
49
+ /** 1.4-S2:插件设定 system prompt(session 级;last-wins;bindCore 时冲刷进宿主收集器,落 'system' 事件) */
50
+ setSystemPrompt(text: string): void;
51
+ }
52
+ interface HostBinding {
53
+ registerTool?(tool: ToolReg): void;
54
+ registerCommand?(cmd: CommandReg): void;
55
+ getRegisteredTools(): ToolReg[];
56
+ setSystemPrompt?(text: string): void;
57
+ }
58
+ export interface LoadedPlugin {
59
+ manifest: Manifest;
60
+ api: PluginApi;
61
+ bindCore(host: HostBinding): void;
62
+ invalidate(): void;
63
+ isInvalidated(): boolean;
64
+ }
65
+ /** 装配插件模块(免编译加载 + Stub 占位 + invalidate 语义) */
66
+ export declare function mountPlugin(spec: {
67
+ file: string;
68
+ manifest: unknown;
69
+ /** reload 版本号:每次 +1 变更 import 查询串击穿 ESM 缓存 */
70
+ reloadEpoch?: number;
71
+ }): Promise<LoadedPlugin>;
72
+ export {};
@@ -0,0 +1,162 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ /**
10
+ * F6 · 插件装配层:manifest 校验 + 免编译加载(缓存击穿热重载)+ Stub 占位 + invalidate
11
+ *
12
+ * 口径:
13
+ * - 载体:jiti 实装受阻(沙箱 npm),以 Node --experimental-transform-types + 动态 import
14
+ * 查询串缓存击空承载(POC-2 同口径);被测不变量与载体无关
15
+ * - Stub 占位(Pi 范式,CAR 自研层):**注册类动作始终可用(收集待注册项)**,
16
+ * 「依赖宿主绑定的能力查询」在 bindCore 前一调用即抛错——用启动期显式失败换运行期时序正确
17
+ * - invalidate:unload/reload 后旧句柄访问显式报错(CAR-INVALIDATED)
18
+ * - manifest:name 必填、version 精确 semver(禁区间);peer 校验挂点预留(O7,M2 启用)
19
+ */
20
+ import { pathToFileURL } from 'node:url';
21
+ import { readFileSync } from 'node:fs';
22
+ import { checkErasableOnly } from '../ptc/erasable.js';
23
+ /** 简易 semver 区间求解(M2 S5:仅支持 ^x.y.z / x.y.z 两种形态;完整区间求解 M3 扩展) */
24
+ function satisfiesRange(version, range) {
25
+ if (range === version)
26
+ return true;
27
+ const m = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(range);
28
+ if (!m)
29
+ return false;
30
+ const [M, mnr, P] = version.split('.').map(Number);
31
+ // npm caret 语义:左起第一个非零位锁定(^1.2.3→>=1.2.3 <2;^0.2.3→>=0.2.3 <0.3;^0.0.3→==0.0.3)
32
+ if (M !== Number(m[1]))
33
+ return false;
34
+ if (M > 0) {
35
+ if (mnr !== Number(m[2]))
36
+ return mnr > Number(m[2]);
37
+ return P >= Number(m[3]);
38
+ }
39
+ if (mnr !== Number(m[2]))
40
+ return false; // ^0.x:minor 锁定
41
+ return P >= Number(m[3]);
42
+ }
43
+ /** 纯收集:仅计算违规项,不抛错(M6 加载报告用——冲突链需结构化定位而非只有异常消息) */
44
+ export function computePeerViolations(manifest, installed) {
45
+ const violations = [];
46
+ for (const p of manifest.peers ?? []) {
47
+ const v = installed.get(p.peer);
48
+ if (v === undefined) {
49
+ if (!p.optional)
50
+ violations.push({ peer: p.peer, range: p.range, installed: null, optional: false });
51
+ continue;
52
+ }
53
+ if (!satisfiesRange(v, p.range))
54
+ violations.push({ peer: p.peer, range: p.range, installed: v, optional: !!p.optional });
55
+ }
56
+ return violations;
57
+ }
58
+ export function validatePeers(manifest, installed) {
59
+ const violations = computePeerViolations(manifest, installed);
60
+ const hard = violations.filter(v => !v.optional);
61
+ if (!hard.length)
62
+ return { exempted: false, violations };
63
+ if (manifest.peerPolicyOverride?.relaxed) {
64
+ // 显式豁免:放宽生效,调用方负责将 manifest.peerPolicy='exempt' 写入审计(留痕)
65
+ return { exempted: true, violations };
66
+ }
67
+ // 严格模式默认:冲突链硬报错(文件/依赖路径/区间/实际版本)
68
+ const chainStr = hard.map(v => `${v.peer}@${v.range} required but ${v.installed ?? '<missing>'} installed`).join('; ');
69
+ throw new Error(`CAR-E-PEER: peer dependency conflict for "${manifest.name}" — ${chainStr}(放宽需 manifest.peerPolicyOverride 显式声明 + reason,审计留痕)`);
70
+ }
71
+ const SEMVER_EXACT = /^\d+\.\d+\.\d+$/;
72
+ const chain = (file, detail) => `CAR-E-MANIFEST: ${detail} (conflict chain: ${file})`;
73
+ export function parseManifest(raw, file = '<manifest>') {
74
+ const m = raw;
75
+ if (!m || typeof m !== 'object')
76
+ throw new Error(chain(file, 'manifest must be an object'));
77
+ if (!m.name || typeof m.name !== 'string')
78
+ throw new Error(chain(file, 'field "name" is required and must be a string'));
79
+ if (!m.version || !SEMVER_EXACT.test(m.version)) {
80
+ throw new Error(chain(file, `field "version" must be exact semver (x.y.z), got "${m.version}"(版本区间仅用于 peer 校验)`));
81
+ }
82
+ if (m.peerPolicyOverride && !m.peerPolicyOverride.reason) {
83
+ throw new Error(chain(file, 'field "peerPolicyOverride.reason" is required when豁免声明(审计留痕)'));
84
+ }
85
+ return m;
86
+ }
87
+ class ApiImpl {
88
+ #host;
89
+ #pendingTools = [];
90
+ #pendingCommands = [];
91
+ #pendingSystemPrompt = null;
92
+ registerTool(tool) {
93
+ if (this.#host?.registerTool) {
94
+ this.#host.registerTool(tool);
95
+ return;
96
+ }
97
+ this.#pendingTools.push(tool); // 注册类动作始终可用:Stub 期收集,bind 后冲刷
98
+ }
99
+ registerCommand(cmd) {
100
+ if (this.#host?.registerCommand) {
101
+ this.#host.registerCommand(cmd);
102
+ return;
103
+ }
104
+ this.#pendingCommands.push(cmd);
105
+ }
106
+ setSystemPrompt(text) {
107
+ if (this.#host?.setSystemPrompt) {
108
+ this.#host.setSystemPrompt(text);
109
+ return;
110
+ }
111
+ this.#pendingSystemPrompt = text; // last-wins:多次设定取最后一次
112
+ }
113
+ getRegisteredTools() {
114
+ // 依赖宿主绑定的能力查询:bindCore 前一调用即抛错(Stub 显式失败)
115
+ if (!this.#host)
116
+ throw new Error('CAR-STUB: "getRegisteredTools" called before bindCore(启动期显式失败换运行期时序正确)');
117
+ return this.#host.getRegisteredTools();
118
+ }
119
+ bindHost(host) {
120
+ this.#host = host;
121
+ for (const t of this.#pendingTools)
122
+ host.registerTool?.(t);
123
+ for (const c of this.#pendingCommands)
124
+ host.registerCommand?.(c);
125
+ if (this.#pendingSystemPrompt !== null)
126
+ host.setSystemPrompt?.(this.#pendingSystemPrompt);
127
+ this.#pendingTools = [];
128
+ this.#pendingCommands = [];
129
+ this.#pendingSystemPrompt = null;
130
+ }
131
+ }
132
+ /** 装配插件模块(免编译加载 + Stub 占位 + invalidate 语义) */
133
+ export async function mountPlugin(spec) {
134
+ const manifest = parseManifest(spec.manifest, spec.file);
135
+ const epoch = spec.reloadEpoch ?? 0;
136
+ // erasable-only 双挂点之二:loader 提交链(挂点一在 run_code 入口——单一事实源 checkErasableOnly)
137
+ const era = checkErasableOnly(readFileSync(spec.file, 'utf-8'));
138
+ if (!era.ok)
139
+ throw new Error(`CAR-E-PTC: ${era.violation} (file: ${spec.file})`);
140
+ const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(spec.file).href + (epoch ? `?epoch=${epoch}` : ''))));
141
+ if (typeof mod.default !== 'function') {
142
+ throw new Error(chain(spec.file, 'plugin module must default-export a factory (api) => void'));
143
+ }
144
+ let alive = true;
145
+ const apiImpl = new ApiImpl();
146
+ const api = new Proxy(apiImpl, {
147
+ get(target, prop) {
148
+ if (!alive)
149
+ throw new Error(`CAR-INVALIDATED: "${prop}" on stale plugin context(旧上下文已失效,请重新加载)`);
150
+ const v = target[prop];
151
+ return typeof v === 'function' ? v.bind(target) : v;
152
+ },
153
+ });
154
+ mod.default(api);
155
+ return {
156
+ manifest,
157
+ api,
158
+ bindCore(host) { apiImpl.bindHost(host); },
159
+ invalidate() { alive = false; },
160
+ isInvalidated() { return !alive; },
161
+ };
162
+ }
@@ -0,0 +1,44 @@
1
+ export interface RegistryEntry {
2
+ url: string;
3
+ priority: number; /** 签名物承载方式(T-5 定稿①:不参与信任分级,验签义务恒在 verifier) */
4
+ signed: boolean;
5
+ }
6
+ export interface RegistryConfig {
7
+ registries: RegistryEntry[];
8
+ /** 公共 npm 兜底(缺省必须显式声明——企业内网 false 为试点准入判据,T-5 定稿③) */
9
+ allowNpmFallback: boolean;
10
+ /** 离线模式:全部拒绝(显式错误) */
11
+ offline: boolean;
12
+ }
13
+ export interface PackageSpec {
14
+ name: string;
15
+ version: string; /** 显式源绑定(manifest registry 字段)——校验级失败硬失败不换源 */
16
+ pinnedRegistry?: string;
17
+ }
18
+ export type ResolutionSource = 'registry' | 'npm-fallback' | 'rejected';
19
+ export interface Resolution {
20
+ source: ResolutionSource;
21
+ url?: string;
22
+ /** 判定摘要(审计落盘 payload;含拒绝原因/解析路径) */
23
+ detail: string;
24
+ /** 解析决策指纹(可落审计哈希链) */
25
+ decisionId: string;
26
+ }
27
+ /**
28
+ * 单源失败排除记录(T-5 定稿②):reason 类型层仅允许 'network'——
29
+ * 「校验级失败换源」在本签名上不可表达(校验失败由调用方硬拒,不经本层换源)。
30
+ */
31
+ export type ExcludedSource = {
32
+ url: string;
33
+ reason: 'network';
34
+ };
35
+ export declare function resolvePackage(spec: PackageSpec, config: RegistryConfig, audit?: (detail: Record<string, unknown>) => void, opts?: {
36
+ exclude?: ExcludedSource[];
37
+ }): Resolution;
38
+ /** 只读预览(T-5 定稿②新增):返回按优先级的候选链(不含 pinned 语义,供装载面展示与演练) */
39
+ export declare function resolveCandidateChain(config: RegistryConfig): {
40
+ chain: string[];
41
+ fallback: boolean;
42
+ };
43
+ /** 白名单校验(插件安装面调用:来源不在清单 = 拒绝并审计) */
44
+ export declare function isWhitelisted(url: string, config: RegistryConfig): boolean;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * T-3/T-4 · registry 对接(M3-S13 落位 + M4-S17 T-5 终局对齐增补)
3
+ *
4
+ * 口径(M3部署设计增补 §3 + M3安全设计增补 T-4 + 决议⑦ + M4安全增补registry对齐 定稿):
5
+ * - **registries 清单即白名单本身**:白名单外默认拒绝 + 审计落盘(不做静默 fallback);
6
+ * - resolution 四级顺序:①显式源绑定(校验级失败**硬失败不换源**——换源重试已拒内容 = 降级攻击面)
7
+ * → ②白名单 priority 升序(网络级失败可按 priority 继续——exclude 通道,类型上仅允许 reason='network')
8
+ * → ③公共 npm 兜底(缺省 false——**候选耗尽**或白名单为空时生效,企业内网安全默认)
9
+ * → ④全不可用 = 离线拒绝(显式错误,非静默);
10
+ * - **signed 字段语义(T-5 定稿①)**:签名物承载方式(true=源直供 / false=镜像透传),**永不产生免验/降档/直信**——验签义务恒在 verifier 层(ADR-003);
11
+ * - 本模块纯判定(零网络 I/O),CI 可全量测试。
12
+ */
13
+ import { createHash } from 'node:crypto';
14
+ export function resolvePackage(spec, config, audit, opts = {}) {
15
+ const finish = (r) => {
16
+ const decisionId = 'RD-' + createHash('sha256').update(JSON.stringify({ spec, r })).digest('hex').slice(0, 16);
17
+ audit?.({ kind: 'registry-resolution', spec: spec.name, ...r, decisionId });
18
+ return { ...r, decisionId };
19
+ };
20
+ // ④ 离线:全拒(最高优先级——离线声明不可被任何源覆盖)
21
+ if (config.offline)
22
+ return finish({ source: 'rejected', detail: 'offline mode: all sources rejected(显式拒绝,非静默)' });
23
+ const excluded = new Set((opts.exclude ?? []).map(e => e.url));
24
+ // ① 显式源绑定:校验级失败硬失败不换源(pinned 不在白名单);
25
+ // pinned 命中但被 exclude(网络失败)→ 语义同「按 priority 继续」(T-5 定稿②网络路径)
26
+ if (spec.pinnedRegistry) {
27
+ const pinned = config.registries.find(r => r.url === spec.pinnedRegistry);
28
+ if (!pinned)
29
+ return finish({ source: 'rejected', detail: `pinned registry "${spec.pinnedRegistry}" not in whitelist——显式绑定不在白名单,硬失败不换源(fail-closed)` });
30
+ if (!excluded.has(pinned.url)) {
31
+ return finish({ source: 'registry', url: pinned.url, detail: `pinned to ${pinned.url}(priority=${pinned.priority})` });
32
+ }
33
+ // pinned 网络失败:落审计后按 priority 继续剩余白名单(不含 pinned)
34
+ }
35
+ // ② 白名单 priority 升序(排除网络失败源)
36
+ const sorted = [...config.registries].sort((a, b) => a.priority - b.priority).filter(r => !excluded.has(r.url));
37
+ if (sorted.length) {
38
+ const first = sorted[0];
39
+ return finish({ source: 'registry', url: first.url, detail: `whitelist priority order → ${first.url}(candidates=${sorted.length}${excluded.size ? `, excluded=${excluded.size}` : ''})` });
40
+ }
41
+ // ③ 公共 npm 兜底(**候选耗尽**或白名单为空时生效——T-5 定稿②③修正语义)
42
+ if (config.allowNpmFallback) {
43
+ const why = config.registries.length ? 'candidates exhausted(网络级失败耗尽)' : 'whitelist empty';
44
+ return finish({ source: 'npm-fallback', url: 'https://registry.npmjs.org', detail: `${why} → npm fallback(显式开启;兜底命中包强制过 verifier 不降级)` });
45
+ }
46
+ return finish({ source: 'rejected', detail: config.registries.length
47
+ ? 'candidates exhausted and npm fallback disabled——候选耗尽且兜底关闭(fail-closed)'
48
+ : 'no whitelist entries and npm fallback disabled——白名单为空且兜底关闭(企业内网安全默认)' });
49
+ }
50
+ /** 只读预览(T-5 定稿②新增):返回按优先级的候选链(不含 pinned 语义,供装载面展示与演练) */
51
+ export function resolveCandidateChain(config) {
52
+ const chain = [...config.registries].sort((a, b) => a.priority - b.priority).map(r => r.url);
53
+ return { chain, fallback: config.allowNpmFallback };
54
+ }
55
+ /** 白名单校验(插件安装面调用:来源不在清单 = 拒绝并审计) */
56
+ export function isWhitelisted(url, config) {
57
+ return !config.offline && config.registries.some(r => r.url === url);
58
+ }