@lqc123qwe/car-runtime 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -116,6 +116,16 @@ export function makePtcToolDefinition(opts: { tools: Map<string, ToolBridge>; au
116
116
  return {
117
117
  name: 'run_code',
118
118
  description: '执行一段 erasable TypeScript 程序(PTC)——程序体内经 tools.<name>(args) 调用已注册工具',
119
+ // 1.4-S7:run_code 入参 schema(进模型声明面——此前无 parameters,模型看不到 code/description 形状)
120
+ parameters: {
121
+ type: 'object',
122
+ properties: {
123
+ code: { type: 'string', description: 'erasable TS 程序体(async 函数体;经 tools.<name>(args) 调用已注册工具)' },
124
+ description: { type: 'string', description: '本段程序的一句话描述(审计留痕)' },
125
+ },
126
+ required: ['code', 'description'],
127
+ additionalProperties: false,
128
+ },
119
129
  // T-22 语义:PTC 与 bash 同信任级别 → 强制 write 最高约束(不可声明为 readonly)
120
130
  declaredSideEffect: 'write' as const,
121
131
  run: async (args: { code: string; description: string; toolCallId?: string }) => {
@@ -15,7 +15,7 @@
15
15
  * 由 runTurn 落 turnEnd);工具调用事件由 runTurn executeBatch 落(本层不重复)。
16
16
  */
17
17
  import type { SessionLog } from '../session/log.ts'
18
- import type { ModelStep, ToolCall } from '../loop/stop.ts'
18
+ import { TurnAborted, type ModelStep, type ToolCall } from '../loop/stop.ts'
19
19
  import type { RuntimeCore } from './llm.ts'
20
20
  import type { ToolDefinition } from './types.ts'
21
21
  import { StreamRedactor } from './redaction.ts'
@@ -32,6 +32,8 @@ export interface ChatStepOptions {
32
32
  tools: ToolDefinition[]
33
33
  maxTokens?: number
34
34
  adapterId?: string
35
+ /** 1.4-S4(D-19):turn 取消信号——chunk 间隙与流结束后(finish 检查前)检查,命中抛 TurnAborted */
36
+ signal?: { aborted: boolean }
35
37
  }
36
38
 
37
39
  /** ModelStep 加法扩展:脱敏计数留痕(S15 secretsRedacted 同名口径) */
@@ -49,7 +51,7 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
49
51
  // 2. 请求消息 = 投影(角色系统:user/assistant/toolResult 与 LlmRequest 对齐;
50
52
  // assistant 工具调用以 { toolCall } content 形态原样透传——deriveMessages 投影即模型可见流)
51
53
  const messages = log.deriveMessages().map(m => ({
52
- role: m.role as 'user' | 'assistant' | 'toolResult',
54
+ role: m.role as 'system' | 'user' | 'assistant' | 'toolResult',
53
55
  content: m.content,
54
56
  }))
55
57
 
@@ -66,9 +68,12 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
66
68
  tools,
67
69
  ...(maxTokens != null ? { maxTokens } : {}),
68
70
  ...(adapterId ? { adapterId } : {}),
71
+ ...(opts.signal ? { signal: opts.signal } : {}),
69
72
  ...(opts.log.sessionId ? { metadata: { sessionId: opts.log.sessionId, turnId, traceId: `${opts.log.sessionId}:${turnId}` } } : {}),
70
73
  })
71
74
  for await (const chunk of stream) {
75
+ // 1.4-S4(D-19):chunk 间隙取消——此刻 assistant 尚未落链(流完才 append),无半截消息
76
+ if (opts.signal?.aborted) throw new TurnAborted()
72
77
  if (chunk.delta !== undefined) text += redactor.push(chunk.delta)
73
78
  if (chunk.toolCallDelta) {
74
79
  const idx = chunk.toolCallDelta.index ?? 0
@@ -81,6 +86,9 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
81
86
  if (chunk.error) errorDetail = chunk.error.message
82
87
  if (chunk.finishReason !== undefined) stopReason = chunk.finishReason === 'aborted' ? 'error' : chunk.finishReason
83
88
  }
89
+ // 1.4-S4(D-19):finish 检查前先查取消(适配器取消路径静默收口无 finishReason——
90
+ // 必须先于此处的契约兜底,否则取消被误判为适配器违规)
91
+ if (opts.signal?.aborted) throw new TurnAborted()
84
92
  if (stopReason === undefined) {
85
93
  // 适配器层已保证首 finishReason(AL-05);防御性兜底显式化
86
94
  throw new Error('CAR-E-LLM-FINISH: 流结束无 finishReason(适配器契约违规)')
@@ -199,7 +199,13 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
199
199
  const id = opts.id ?? 'openai-compat'
200
200
  const baseUrl = opts.baseUrl.replace(/\/+$/, '')
201
201
  if (!baseUrl.startsWith('https://')) {
202
- throw new Error(`CAR-E-LLM-TLS: baseUrl 必须 https(TLS 强制,§3.2.M8.4;本地明文部署请经 TLS 代理或自建适配器)`)
202
+ // 1.4-S4(D-12a,1.2 规划期裁决):http 仅豁免 loopback——本地模型服务(Ollama 等)与
203
+ // 本地 E2E;非回环地址维持 TLS 强制(§3.2.M8.4)。豁免集显式列举,不扩大到局域网。
204
+ let loopback = false
205
+ try { const host = new URL(baseUrl).hostname.replace(/^\[|\]$/g, ''); loopback = host === '127.0.0.1' || host === 'localhost' || host === '::1' } catch { /* 解析失败 = 非法 URL,按非回环拒绝 */ }
206
+ if (!loopback) {
207
+ throw new Error(`CAR-E-LLM-TLS: baseUrl 必须 https(TLS 强制,§3.2.M8.4);http 仅豁免 loopback(127.0.0.1/[::1]/localhost,D-12a)——远程明文部署请经 TLS 代理或自建适配器`)
208
+ }
203
209
  }
204
210
  const url = `${baseUrl}/chat/completions`
205
211
  const fetchImpl = opts.fetchImpl ?? fetch
@@ -210,6 +216,9 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
210
216
  const backoffMs = opts.backoffMs ?? [1_000, 2_000]
211
217
 
212
218
  const toOpenAI = (req: LlmRequest, apiKey: string) => ({
219
+ // 1.4-BUG-1:method: 'POST' 缺失——M8 最小实现潜伏缺陷(注入式 fetch 的单测忽略 method,
220
+ // 真 undici 对 GET+body 直接拒绝);car run 真路径接线时暴露(生产调用点核查产出)
221
+ method: 'POST',
213
222
  headers: {
214
223
  'content-type': 'application/json',
215
224
  authorization: `Bearer ${apiKey}`,
@@ -250,6 +259,8 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
250
259
  const payload = toOpenAI(req, apiKey)
251
260
 
252
261
  for (let attempt = 0; ; attempt++) {
262
+ // 1.4-S4(D-19):外部取消 ≠ 失败——attempt 前检查,静默收口(无 chunk、无 retry)
263
+ if (req.signal?.aborted) return
253
264
  const controller = new AbortController()
254
265
  const started = Date.now()
255
266
  let firstByteSeen = false
@@ -267,6 +278,9 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
267
278
  if (!res.body) throw new Retryable('响应无 body 流')
268
279
  let sawFinish = false
269
280
  for await (const chunk of parseSseStream(res.body)) {
281
+ // 1.4-S4(D-19):chunk 间隙检查取消——abort 连接并静默收口(不产 error chunk,
282
+ // 不违反 finishReason 不可变;上层 chatStep 以自身 signal 检查抛 TurnAborted)
283
+ if (req.signal?.aborted) { controller.abort(); return }
270
284
  if (!firstByteSeen) { firstByteSeen = true; clearTimeout(firstByteTimer) }
271
285
  if (chunk.finishReason !== undefined) sawFinish = true
272
286
  yield chunk
@@ -279,6 +293,8 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
279
293
  }
280
294
  return
281
295
  } catch (e) {
296
+ // 1.4-S4(D-19):外部取消(abort 连锁的 fetch 异常)≠ 失败——静默收口,不走 retry
297
+ if (req.signal?.aborted) return
282
298
  const beforeFirstByte = !firstByteSeen
283
299
  clearTimeout(firstByteTimer)
284
300
  // 首块前失败 = 网络错误/429/5xx/首字节超时(§3.5.4 重试条件全集)——未消费任何
@@ -4,7 +4,8 @@
4
4
  * 口径(§3.2.M8.5 关键约束,逐条落地):
5
5
  * - 默认关:无端点配置时不初始化任何 exporter——无配置 = noop 句柄 + 零出站(可机器断言);
6
6
  * - 显式开:用户配置端点即视为开启(CAR_OTEL_ENDPOINT 或显式 config 传入);
7
- * - 端点归用户:数据只发往用户自配 E-05;OTLP/HTTP JSON(fetch),导出超时 5s;
7
+ * - 端点归用户:数据只发往用户自配 E-05;OTLP/HTTP JSON(node:http(s) 出站——1.4-BUG-2:全局
8
+ * fetch/undici 在 win32 进程退出时触发 libuv 断言崩溃),导出超时 5s;
8
9
  * 1.2-S3 生产级策略(D-12b 口径修订,2026-09-27):**0 重试 → 有界重试 ≤2**(仅 429/5xx/
9
10
  * 网络错误,退避 1s/2s;4xx 业务错不重试)——重试耗尽静默丢弃 + droppedExports 计数,
10
11
  * 永不抛错(BD-05「不抛错、不落审计」收紧保持);隐私三原则不变(数据仍只发用户自配端点);
@@ -22,6 +23,8 @@
22
23
  * 不引入 @opentelemetry/* 依赖(项目零依赖红线,README engines 纪律)。
23
24
  */
24
25
  import { randomBytes } from 'node:crypto'
26
+ import { request as httpRequest } from 'node:http'
27
+ import { request as httpsRequest } from 'node:https'
25
28
 
26
29
  export type SamplingSpec = 'always_on' | 'always_off' | { ratio: number }
27
30
 
@@ -55,10 +58,16 @@ export interface TelemetrySpan {
55
58
  recordException(err: unknown): this
56
59
  end(): void
57
60
  readonly traceId: string
61
+ /** 1.4-S6:子 span 挂链需要父 spanId(TurnTracer turn→step 层级) */
62
+ readonly spanId: string
58
63
  }
59
64
 
60
65
  export interface TelemetryTracer {
61
- startSpan(name: string, opts?: { attributes?: Record<string, string | number | boolean> }): TelemetrySpan
66
+ startSpan(name: string, opts?: {
67
+ attributes?: Record<string, string | number | boolean>
68
+ /** 1.4-S6:trace 透传(traceId 复用 + parentSpanId 挂链);缺省 = 新独立 trace */
69
+ trace?: { traceId: string; parentSpanId?: string }
70
+ }): TelemetrySpan
62
71
  }
63
72
 
64
73
  export interface TelemetryCounter {
@@ -102,6 +111,7 @@ const noopSpan = (): TelemetrySpan => ({
102
111
  recordException() { return noopSpan() },
103
112
  end() {},
104
113
  traceId: '',
114
+ spanId: '',
105
115
  })
106
116
  const NOOP_TRACER: TelemetryTracer = { startSpan: () => noopSpan() }
107
117
  const NOOP_METER: TelemetryMeter = { createCounter: () => ({ add() {} }) }
@@ -164,7 +174,25 @@ export function createTelemetryFacade(
164
174
  }
165
175
  }
166
176
 
167
- const fetchImpl = opts.fetchImpl ?? fetch
177
+ // 1.4-BUG-2:出站默认走 node:http(s)(非全局 fetch/undici)——win32 上 undici socket +
178
+ // 进程退出触发 libuv 断言崩溃(UV_HANDLE_CLOSING, 0xC0000409,Node 23.5 实测、22 不复现);
179
+ // 测试仍可注入 fetchImpl(缝保留)。res.resume() 排空响应体(连接干净归还)。
180
+ const fetchImpl = opts.fetchImpl ?? ((url: string | URL, init?: RequestInit): Promise<{ ok: boolean; status: number }> => new Promise(resolve => {
181
+ try {
182
+ const u = String(url)
183
+ const mod = u.startsWith('https:') ? httpsRequest : httpRequest
184
+ const req = mod(u, { method: init?.method ?? 'GET', headers: init?.headers as Record<string, string> | undefined }, res => {
185
+ res.resume()
186
+ res.on('end', () => {
187
+ const st = res.statusCode ?? 0
188
+ resolve({ ok: st >= 200 && st < 300, status: st })
189
+ })
190
+ })
191
+ req.setTimeout(timeoutMs, () => req.destroy(new Error('timeout')))
192
+ req.on('error', () => resolve({ ok: false, status: 599 })) // 网络/超时错误 → 599 可重试类
193
+ req.end(init?.body ?? null)
194
+ } catch { resolve({ ok: false, status: 599 }) }
195
+ }))
168
196
  const now = opts.now ?? Date.now
169
197
  const sleep = config?.sleep ?? (ms => new Promise<void>(r => setTimeout(r, ms)))
170
198
  const timeoutMs = config?.exportTimeoutMs ?? 5_000
@@ -194,12 +222,16 @@ export function createTelemetryFacade(
194
222
  */
195
223
  const exportOtlp = async (path: string, body: unknown): Promise<boolean> => {
196
224
  for (let attempt = 0; ; attempt++) {
225
+ // 1.4-BUG-2:AbortSignal.timeout 的内部定时器在进程退出时触发 libuv win32 断言崩溃
226
+ // (UV_HANDLE_CLOSING, 0xC0000409)——改手动 AbortController + clearTimeout 确定性清理
227
+ const controller = new AbortController()
228
+ const abortTimer = setTimeout(() => controller.abort(), timeoutMs)
197
229
  try {
198
230
  const res = await fetchImpl(endpoint + path, {
199
231
  method: 'POST',
200
232
  headers: baseHeaders,
201
233
  body: JSON.stringify(body),
202
- signal: AbortSignal.timeout(timeoutMs),
234
+ signal: controller.signal,
203
235
  })
204
236
  if (res.ok) return true
205
237
  if ((res.status === 429 || res.status >= 500) && attempt < retryLimit) {
@@ -214,6 +246,8 @@ export function createTelemetryFacade(
214
246
  continue
215
247
  }
216
248
  return false
249
+ } finally {
250
+ clearTimeout(abortTimer)
217
251
  }
218
252
  }
219
253
  }
@@ -277,9 +311,10 @@ export function createTelemetryFacade(
277
311
  const activeTracer: TelemetryTracer = {
278
312
  startSpan(name, startOpts) {
279
313
  const record: SpanRecord = {
280
- traceId: randomBytes(16).toString('hex'),
314
+ // 1.4-S6:trace 透传(traceId 复用 + parentSpanId 挂链)——缺省仍为独立随机 trace(1.1 行为不变)
315
+ traceId: startOpts?.trace?.traceId ?? randomBytes(16).toString('hex'),
281
316
  spanId: randomBytes(8).toString('hex'),
282
- parentSpanId: null,
317
+ parentSpanId: startOpts?.trace?.parentSpanId ?? null,
283
318
  name,
284
319
  startMs: now(),
285
320
  endMs: 0,
@@ -287,6 +322,7 @@ export function createTelemetryFacade(
287
322
  }
288
323
  return {
289
324
  get traceId() { return record.traceId },
325
+ get spanId() { return record.spanId },
290
326
  setAttribute(key, value) { record.attributes[key] = value; return this },
291
327
  addEvent(evtName, attrs) { record.attributes[`event.${evtName}`] = attrs ? JSON.stringify(attrs) : evtName; return this },
292
328
  recordException(err) { record.attributes['exception.message'] = String((err as Error)?.message ?? err); return this },
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 1.4-S1 · 工具声明面转换(执行面 Map → 模型声明面 ToolDefinition[])
3
+ *
4
+ * 口径(1.4 规划 W1-3 / D-18 / T-22 / 声明面=授权面红线):
5
+ * - **执行面与声明面同一对象**:ToolDef(loop/stop.ts,declaredSideEffect+run)即声明来源——
6
+ * 插件注册的 ToolReg 经 bindCore 收集后直接进本转换器,模型看到的工具集合 = 授权门放行的集合
7
+ * (多渲一个 = 无授权执行面,少渲一个 = 授权白给——PTC SDK 同款红线);
8
+ * - T-22 收口:declaredSideEffect 缺省 write(未声明按最高约束——mcp/gateway.ts 注册点同款);
9
+ * - 跨源同名显式报错(禁静默覆盖):Map 键即名字,插件/MCP 工具合并由调用方先行去重——
10
+ * 本转换器对重名(Map 键重复不可能,但 ToolDef.name 与键不一致时)显式报错;
11
+ * - declaredSideEffect 不出站(llm.ts 出站映射只带 name/description/parameters——权限面只进权限门)。
12
+ */
13
+ import type { ToolDef } from '../loop/stop.ts'
14
+ import type { ToolDefinition } from './types.ts'
15
+
16
+ export function toToolDefinitions(tools: Map<string, ToolDef>): ToolDefinition[] {
17
+ const out: ToolDefinition[] = []
18
+ for (const [name, t] of tools) {
19
+ out.push({
20
+ name,
21
+ ...(t.description !== undefined ? { description: t.description } : {}),
22
+ ...(t.parameters !== undefined ? { parameters: t.parameters } : {}),
23
+ declaredSideEffect: t.declaredSideEffect ?? 'write',
24
+ })
25
+ }
26
+ return out
27
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 1.4-S6 · Turn 级 trace 面(W1 遥测未入链收尾):turn span 包全程、step span 挂每次 model() 调用
3
+ *
4
+ * 口径(1.4 规划 W1 遥测块 / telemetry.ts 头注释):
5
+ * - 层级:turn(根 span,traceId 在此生成)→ step(parentSpanId = turn.spanId)——同一 turn 共享 trace;
6
+ * - **挂点在装配层**(car run 的 model 回调闭包),loop 层零改动——遥测治理属调用方(telemetry.ts
7
+ * 「内容治理属调用方」口径);设计微调登记:规划原文「runTurn opts.telemetry 注入面」改为装配层
8
+ * 闭包挂点,语义等价(turn/step 层级与出站面不变),loop 层保持纯停止语义;
9
+ * - attributes 零内容面:turn_id/step 序号/outcome 枚举——无会话内容/提示词/路径。
10
+ */
11
+ import { randomBytes } from 'node:crypto'
12
+ import type { TelemetryFacade } from './telemetry.ts'
13
+
14
+ export interface StepSpanHandle {
15
+ end(outcome: string): void
16
+ recordException(e: unknown): void
17
+ }
18
+
19
+ export interface TurnSpanHandle {
20
+ traceId: string
21
+ spanId: string
22
+ end(outcome: string): void
23
+ stepSpan(step: number): StepSpanHandle
24
+ }
25
+
26
+ export class TurnTracer {
27
+ readonly #facade: TelemetryFacade | null
28
+
29
+ constructor(facade: TelemetryFacade | null | undefined) {
30
+ // 默认关(无端点)= noop 句柄(零出站零积累——telemetry 三原则)
31
+ this.#facade = facade?.enabled ? facade : null
32
+ }
33
+
34
+ startTurn(turnId: string): TurnSpanHandle {
35
+ if (!this.#facade) {
36
+ return { traceId: '', spanId: '', end() {}, stepSpan: () => ({ end() {}, recordException() {} }) }
37
+ }
38
+ const tracer = this.#facade.getTracer()
39
+ const traceId = randomBytes(16).toString('hex')
40
+ const turn = tracer.startSpan('car.turn', { attributes: { 'car.turn_id': turnId }, trace: { traceId } })
41
+ return {
42
+ traceId,
43
+ spanId: turn.spanId,
44
+ end(outcome) {
45
+ turn.setAttribute('car.outcome', outcome)
46
+ turn.end()
47
+ },
48
+ stepSpan(step: number): StepSpanHandle {
49
+ const s = tracer.startSpan('car.step', {
50
+ attributes: { 'car.turn_id': turnId, 'car.step': step },
51
+ trace: { traceId, parentSpanId: turn.spanId },
52
+ })
53
+ return {
54
+ end(outcome) {
55
+ s.setAttribute('car.outcome', outcome)
56
+ s.end()
57
+ },
58
+ recordException(e) { s.recordException(e) },
59
+ }
60
+ },
61
+ }
62
+ }
63
+ }
@@ -19,9 +19,12 @@ export interface LlmRequest {
19
19
  tools: ToolDefinition[]
20
20
  maxTokens?: number
21
21
  metadata?: { sessionId: string; turnId: string; traceId: string }
22
- // —— 规格外加法扩展(登记:M8 收口文档 §4)——
22
+ // —— 规格外加法扩展(登记:M8 收口文档 §4 / 1.4-S4)——
23
23
  /** 多适配器在册时显式路由;缺省用默认适配器 */
24
24
  adapterId?: string
25
+ /** 1.4-S4(D-19):turn 取消信号(runTurn 的 {aborted} 形态透传)——适配器在 attempt 前 /
26
+ * chunk 间隙检查;外部取消 ≠ 失败(不 retry、不产 error chunk,静默收口由 chatStep 抛 TurnAborted) */
27
+ signal?: { aborted: boolean }
25
28
  }
26
29
 
27
30
  /** 工具调用流式增量(跨 chunk 聚合出完整 ToolCall) */
@@ -10,7 +10,7 @@ import { createHash } from 'node:crypto'
10
10
  import { readFileSync } from 'node:fs'
11
11
  import { decodeLogBuffer, splitJsonlLines } from './format.ts'
12
12
 
13
- export type EventKind = 'user' | 'assistant' | 'toolCall' | 'toolResult' | 'turnEnd' | 'goalUpdate' | 'hostRaw' | 'fork' // M3-S11 加法扩展:多宿主归一化降级通道(pattern 未命中留痕,零静默);M5-DEC4 加法扩展:跨宿主 fork 标记事件;六值 TurnEndReason 不变
13
+ export type EventKind = 'user' | 'assistant' | 'toolCall' | 'toolResult' | 'turnEnd' | 'goalUpdate' | 'hostRaw' | 'fork' | 'system' // M3-S11 加法扩展:多宿主归一化降级通道(pattern 未命中留痕,零静默);M5-DEC4 加法扩展:跨宿主 fork 标记事件;1.4-S2 加法扩展:system prompt 落链(§3.5.5:新模型可见输入 = 新事件类型);六值 TurnEndReason 不变
14
14
  export type Actor = 'user' | 'model' | 'plugin' | 'runtime'
15
15
  export interface SessionEvent {
16
16
  seq: number
@@ -88,7 +88,8 @@ export class SessionLog {
88
88
  const out: Array<{ role: string; [k: string]: unknown }> = []
89
89
  for (const e of this.events) {
90
90
  if (upTo !== undefined && e.seq >= upTo) break
91
- if (e.kind === 'user') out.push({ role: 'user', content: e.payload })
91
+ if (e.kind === 'system') out.push({ role: 'system', content: e.payload })
92
+ else if (e.kind === 'user') out.push({ role: 'user', content: e.payload })
92
93
  else if (e.kind === 'assistant') out.push({ role: 'assistant', content: e.payload })
93
94
  else if (e.kind === 'toolCall') out.push({ role: 'assistant', toolCall: e.payload })
94
95
  else if (e.kind === 'toolResult') out.push({ role: 'toolResult', content: e.payload })
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "comment": "J-09 contract 基线:SessionEventMap/SessionEvent 签名字段集。只增不删——本文件中任何键从 src/session/log.ts 的当前签名中消失即 FAIL(M2部署设计增补 §2.3 J-09)",
3
- "eventKinds": ["user", "assistant", "toolCall", "toolResult", "turnEnd", "goalUpdate", "hostRaw", "fork"],
3
+ "eventKinds": ["user", "assistant", "toolCall", "toolResult", "turnEnd", "goalUpdate", "hostRaw", "fork", "system"],
4
4
  "actors": ["user", "model", "plugin", "runtime"],
5
5
  "sessionEventFields": ["seq", "ts", "actor", "kind", "turnId", "payload", "meta", "prevHash", "hash"],
6
6
  "sessionLogApi": ["append", "snapshotModelRequest", "verifyChain", "deriveMessages", "assertModelVisibleLogged", "stats", "fromEvents", "exportJSONL"]
@@ -273,7 +273,7 @@ test('S26: CLI 配置通道真实消费——car run 经 car.config.json 走 enf
273
273
  assert.equal(r1.code, 1)
274
274
  assert.match(r1.stderr, /签名门禁拒绝.*signature missing/)
275
275
  // env 显式 0 压过配置 → warn 放行到装配段
276
- const r2 = await car(['run', 'plugin.ts'], { cwd: dir, env: { CAR_SIG_ENFORCE: '0' } })
276
+ const r2 = await car(['run', 'plugin.ts', '--demo'], { cwd: dir, env: { CAR_SIG_ENFORCE: '0' } }) // 1.4 D-13:stub 流挂 --demo
277
277
  assert.equal(r2.code, 0, r2.stderr)
278
278
  assert.match(r2.stdout, /\[1\/5 装配\] OK/)
279
279
  // 坏配置 fail-visible 中止(exit 2,不进入装载)
@@ -213,7 +213,7 @@ test('S28: 生产调用点——car run 真进程 append → cwd sessions-index.
213
213
  // 工具名避开 demo_tool:CLI 演示脚手架硬编码注册 demo_tool,同名插件工具触发预存在的 provide
214
214
  // 撞名边缘(登记不修——W1 重做 car run 装配面时出清)
215
215
  writeFileSync(join(dir, 'p.ts'), `export const manifest = { name: 'p', version: '1.0.0' }\nexport default function apply(api) {\n api.registerTool({ name: 't', run: async () => 'ok' })\n}\n`)
216
- const r = await car(['run', 'p.ts'], { cwd: dir })
216
+ const r = await car(['run', 'p.ts', '--demo'], { cwd: dir }) // 1.4 D-13:stub 流挂 --demo
217
217
  assert.equal(r.code, 0, r.stderr)
218
218
  const dbPath = join(dir, 'sessions-index.db')
219
219
  assert.ok(existsSync(dbPath), 'car run 运行后 cwd 索引自举在位')