@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
@@ -0,0 +1,79 @@
1
+ /**
2
+ * M8 · 脱敏钩子(redact ——《系统设计》§3.2.M8.2 脱敏行 / SQ-07 输出敏感扫描 / O-14)
3
+ *
4
+ * 口径:
5
+ * - 检测器单一权威:本层只做 M8 面(RedactionResult 形态 + 流式 hold-back),
6
+ * 模式库/三层检测/遮蔽口径全部委托 F12 scanSecrets(security/secrets.ts)——
7
+ * 双实现必然漂移,S15 已确立「回填前强制 redact」先例;
8
+ * - 遮蔽口径(§3.2.M8.3 字段表):API Key 形如 sk-****(保留首尾 ≤4 字符)= F12 maskToken;
9
+ * - 流式扫描(StreamRedactor):delta 直发会造成跨 chunk 命中逃逸——hold-back 窗口
10
+ * 「从最早疑似锚点起驻留」,命中补齐即遮蔽放行,finishReason 收口时 flush 全量扫描;
11
+ * 结构标记类模式(GCP service_account 等)的匹配起点先于锚点,流式模式不保证完整遮蔽
12
+ * (登记为流式已知边界;非流式 redact() 为权威口径)。
13
+ */
14
+ import { scanSecrets } from '../security/secrets.js';
15
+ /** 非流式脱敏钩子(§3.2.M8.2:redact(text): RedactionResult,天然幂等) */
16
+ export function redact(text) {
17
+ const { hits } = scanSecrets(text);
18
+ let out = '';
19
+ let cursor = 0;
20
+ for (const h of hits) {
21
+ out += text.slice(cursor, h.start) + h.masked;
22
+ cursor = h.end;
23
+ }
24
+ out += text.slice(cursor);
25
+ return { redacted: out, redactedCount: hits.length, hits };
26
+ }
27
+ /**
28
+ * 疑似锚点:所有模式匹配串的起始字面量(大小写不敏感)。
29
+ * 命中检测必然先经过锚点——锚点之后的字节才需要驻留等待命中补齐。
30
+ */
31
+ const ANCHOR_RE = /(sk[-_]|gh[pousr]_|xox|akia|rk_|npm_|-----begin|postgres|mysql|mongodb|redis|jdbc:|aws|service_account|accountkey|eyJ|authorization|bearer|api[_-]?key|client[_-]?secret|access[_-]?token|password)/i;
32
+ /** 无锚点时的尾部驻留长度(≥ 最长锚点 15,防锚点跨 chunk 拼合逃逸) */
33
+ const TRAILING_HOLD = 16;
34
+ /**
35
+ * 流式脱敏器:push() 返回「当前可安全放行」的文本(已遮蔽),flush() 收口全量扫描。
36
+ * 语义:不泄漏(任何已放行字节不可能成为命中的一部分)、不误遮(锚点未补齐成命中时
37
+ * 原文放行)、粒度换正确性(疑似锚点驻留直到命中补齐或 flush——长行含假锚点时
38
+ * 流式粒度退化到收口时点,登记为已接受折衷)。
39
+ */
40
+ export class StreamRedactor {
41
+ #pending = '';
42
+ /** 追加 delta,返回可安全放行的文本(可能为空串) */
43
+ push(text) {
44
+ this.#pending += text;
45
+ let out = '';
46
+ // 1) 命中补齐:从前往后逐个遮蔽(遮蔽后重扫——遮蔽串不会再命中)
47
+ for (;;) {
48
+ const { hits } = scanSecrets(this.#pending);
49
+ const h = hits[0];
50
+ if (!h)
51
+ break;
52
+ out += this.#pending.slice(0, h.start) + h.masked;
53
+ this.#pending = this.#pending.slice(h.end);
54
+ }
55
+ // 2) 无完整命中:从最早疑似锚点起驻留
56
+ const m = ANCHOR_RE.exec(this.#pending);
57
+ if (m && m.index !== undefined) {
58
+ out += this.#pending.slice(0, m.index);
59
+ this.#pending = this.#pending.slice(m.index);
60
+ return out;
61
+ }
62
+ // 3) 无锚点:放行除尾部驻留窗外的全部(尾窗防锚点跨 chunk 拼合)
63
+ if (this.#pending.length > TRAILING_HOLD) {
64
+ out += this.#pending.slice(0, this.#pending.length - TRAILING_HOLD);
65
+ this.#pending = this.#pending.slice(-TRAILING_HOLD);
66
+ }
67
+ return out;
68
+ }
69
+ /** 收口(finishReason 时点):全量扫描遮蔽剩余驻留 */
70
+ flush() {
71
+ const r = redact(this.#pending);
72
+ this.#pending = '';
73
+ return r.redacted;
74
+ }
75
+ /** 已遮蔽计数口径(测试/审计观察面):flush 前的累计放行不含命中原文 */
76
+ get pendingLength() {
77
+ return this.#pending.length;
78
+ }
79
+ }
@@ -0,0 +1,88 @@
1
+ export type SamplingSpec = 'always_on' | 'always_off' | {
2
+ ratio: number;
3
+ };
4
+ export interface TelemetryConfig {
5
+ /** 用户自配 OTLP/HTTP 端点(如 https://otel.example.internal)——无值 = 默认关 */
6
+ endpoint?: string;
7
+ serviceName?: string;
8
+ headers?: Record<string, string>;
9
+ /** 导出超时(§3.5.4 基线 5s) */
10
+ exportTimeoutMs?: number;
11
+ /** 自动导出间隔 ms;0(缺省)= 仅手动 flush(测试/收口时点调用) */
12
+ intervalMs?: number;
13
+ /** 有界重试上限(仅 429/5xx/网络错误;§3.5.4 基线 ≤2;D-12b 口径修订) */
14
+ retryLimit?: number;
15
+ /** 重试退避序列 ms(缺省 [1000, 2000];测试可注入) */
16
+ retryBackoffMs?: number[];
17
+ /** 采样(缺省 always_on = 全量);head 决策于 span.end() */
18
+ sampling?: SamplingSpec;
19
+ /** 单请求 spans 上限(超限分批;缺省 512) */
20
+ maxBatchSize?: number;
21
+ /** span buffer 上限(溢出丢最旧 + queueOverflows 计数;缺省 2048) */
22
+ maxQueueSize?: number;
23
+ /** 测试注入睡眠(退避可观测;缺省真实定时器) */
24
+ sleep?: (ms: number) => Promise<void>;
25
+ }
26
+ export interface TelemetrySpan {
27
+ setAttribute(key: string, value: string | number | boolean): this;
28
+ addEvent(name: string, attrs?: Record<string, string | number | boolean>): this;
29
+ recordException(err: unknown): this;
30
+ /** 1.8-D31:atMs = 结束时刻覆盖(跨隔离体计时 relay;缺省 now() 行为不变) */
31
+ end(atMs?: number): void;
32
+ readonly traceId: string;
33
+ /** 1.4-S6:子 span 挂链需要父 spanId(TurnTracer turn→step 层级) */
34
+ readonly spanId: string;
35
+ }
36
+ export interface TelemetryTracer {
37
+ startSpan(name: string, opts?: {
38
+ attributes?: Record<string, string | number | boolean>;
39
+ /** 1.4-S6:trace 透传(traceId 复用 + parentSpanId 挂链);缺省 = 新独立 trace */
40
+ trace?: {
41
+ traceId: string;
42
+ parentSpanId?: string;
43
+ };
44
+ /** 1.8-D31:起始时刻覆盖(跨隔离体计时 relay;缺省 now() 行为不变) */
45
+ time?: {
46
+ startMs: number;
47
+ };
48
+ }): TelemetrySpan;
49
+ }
50
+ export interface TelemetryCounter {
51
+ add(value: number, labels?: Record<string, string>): void;
52
+ }
53
+ export interface TelemetryMeter {
54
+ createCounter(name: string): TelemetryCounter;
55
+ }
56
+ export interface TelemetryStats {
57
+ spansEnded: number;
58
+ /** 1.2-S3 加法:head 采样被采出的 span 数(缺省 always_on 下恒 0) */
59
+ spansSampledOut: number;
60
+ /** 成功出站的 OTLP 请求计数(traces/metrics 分批后按请求计) */
61
+ tracesExported: number;
62
+ metricsExported: number;
63
+ droppedExports: number;
64
+ /** 1.2-S3 加法:buffer 溢出丢最旧计数(maxQueueSize 上限) */
65
+ queueOverflows: number;
66
+ }
67
+ export interface TelemetryFacade {
68
+ /** 默认关 = false(无端点配置时) */
69
+ readonly enabled: boolean;
70
+ /** 幂等句柄(§3.2.M8.2:天然幂等) */
71
+ getTracer(): TelemetryTracer;
72
+ getMeter(): TelemetryMeter;
73
+ /** 手动导出(BD-05:端点不可达静默丢弃——resolve 不 reject) */
74
+ flush(): Promise<void>;
75
+ shutdown(): Promise<void>;
76
+ /** 观察面(测试/收口;不含内容字段) */
77
+ stats(): TelemetryStats;
78
+ }
79
+ /**
80
+ * 1.2-S3 env 配置通道:CAR_OTEL_ENDPOINT(唯一开关)/ CAR_OTEL_SAMPLING(always_on|always_off|0..1,
81
+ * 非法值保持缺省 always_on——遥测面禁 fail-hard)/ CAR_OTEL_INTERVAL_MS / CAR_OTEL_SERVICE_NAME。
82
+ * 无 endpoint = null(默认关,noop 门面)。
83
+ */
84
+ export declare function telemetryConfigFromEnv(env?: NodeJS.ProcessEnv): TelemetryConfig | null;
85
+ export declare function createTelemetryFacade(config: TelemetryConfig | null | undefined, opts?: {
86
+ fetchImpl?: typeof fetch;
87
+ now?: () => number;
88
+ }): TelemetryFacade;
@@ -0,0 +1,332 @@
1
+ /**
2
+ * M8 · 遥测门面(getTracer/getMeter ——《系统设计》§3.2.M8.2 遥测行 / §3.2.M8.5 隐私三原则)
3
+ *
4
+ * 口径(§3.2.M8.5 关键约束,逐条落地):
5
+ * - 默认关:无端点配置时不初始化任何 exporter——无配置 = noop 句柄 + 零出站(可机器断言);
6
+ * - 显式开:用户配置端点即视为开启(CAR_OTEL_ENDPOINT 或显式 config 传入);
7
+ * - 端点归用户:数据只发往用户自配 E-05;OTLP/HTTP JSON(node:http(s) 出站——1.4-BUG-2:全局
8
+ * fetch/undici 在 win32 进程退出时触发 libuv 断言崩溃),导出超时 5s;
9
+ * 1.2-S3 生产级策略(D-12b 口径修订,2026-09-27):**0 重试 → 有界重试 ≤2**(仅 429/5xx/
10
+ * 网络错误,退避 1s/2s;4xx 业务错不重试)——重试耗尽静默丢弃 + droppedExports 计数,
11
+ * 永不抛错(BD-05「不抛错、不落审计」收紧保持);隐私三原则不变(数据仍只发用户自配端点);
12
+ * - 采样(1.2-S3 加法):head 采样于 span.end() 决策——always_on(缺省,全量,1.1 行为不变)/
13
+ * always_off / { ratio }(概率);被采出计入 spansSampledOut;采样只减少出站数据(隐私正向);
14
+ * - 批处理上限(1.2-S3 加法):maxBatchSize 单请求分批(512)+ maxQueueSize buffer 上限
15
+ * (2048,溢出丢最旧 + queueOverflows 计数)——内存有界;
16
+ * - env 通道(1.2-S3 加法):telemetryConfigFromEnv——CAR_OTEL_ENDPOINT(唯一开关)/
17
+ * CAR_OTEL_SAMPLING(always_on|always_off|0..1,非法值保持缺省——遥测面禁 fail-hard)/
18
+ * CAR_OTEL_INTERVAL_MS / CAR_OTEL_SERVICE_NAME;
19
+ * - 严格解耦:本文件零 import 自 session/*(遥测与审计日志禁止混流)——测试做静态断言;
20
+ * - S17 零内容计数器(telemetry/metrics.ts)是登记表兜底通道,与本门面并存互补:
21
+ * 本面是 OTel 形态的出站通道,不承接 S17 的零内容红线语义(内容治理属调用方);
22
+ * - 零依赖:OTel「标准句柄形态」自研(startSpan/attributes/end、createCounter/add),
23
+ * 不引入 @opentelemetry/* 依赖(项目零依赖红线,README engines 纪律)。
24
+ *
25
+ * 1.7(D-26 span 语义标准化 / D-27 metrics 时序修正):
26
+ * - recordException → OTel 规范形态:span event(name='exception',exception.type/exception.message
27
+ * 于事件属性)+ span status → STATUS_CODE_ERROR;stacktrace 缺省不出站(防路径/源码面外泄——
28
+ * 隐私三原则正向收紧;可选开关不随本迭代引入);正常 span 恒 STATUS_CODE_UNSET(行为不变面);
29
+ * - addEvent → 标准 events 数组编码(name + attributes 原形态 + 事件时刻 timeUnixNano——
30
+ * 替换 1.1 起的 `event.<name>` JSON 属性串非标准形态);
31
+ * - 出站体 spans[] 加法字段 events/status(空形态合法——无事件 span 不出 events 键);
32
+ * - metrics 累计基数保持:flush 不再清空 counters——每次导出全量累计快照(真 CUMULATIVE)。
33
+ * 1.2-S3 起 clear() 与 AGGREGATION_TEMPORALITY_CUMULATIVE 标注矛盾:intervalMs>0 自动导出时
34
+ * 下游把增量解读为总量回落;内存有界由 label 基数保证(零内容枚举红线——AllowedLabels 3 计数器)。
35
+ *
36
+ * 1.8(D-31 PTC worker relay span 时刻覆盖):startSpan 加法 time.startMs / end 加法 atMs——
37
+ * 跨隔离体计时回传(worker_threads 同进程同时钟域)后主线程单点收口(trace.ts ptcRelay);
38
+ * 采样/buffer 上限/出站形态路径零改动(relay span 走既有 end() 全路径)。缺省时刻行为不变。
39
+ */
40
+ import { randomBytes } from 'node:crypto';
41
+ import { request as httpRequest } from 'node:http';
42
+ import { request as httpsRequest } from 'node:https';
43
+ // ==================== noop 句柄(默认关路径:零出站、零积累) ====================
44
+ const noopSpan = () => ({
45
+ setAttribute() { return noopSpan(); },
46
+ addEvent() { return noopSpan(); },
47
+ recordException() { return noopSpan(); },
48
+ end() { },
49
+ traceId: '',
50
+ spanId: '',
51
+ });
52
+ const NOOP_TRACER = { startSpan: () => noopSpan() };
53
+ const NOOP_METER = { createCounter: () => ({ add() { } }) };
54
+ const NOOP_STATS = { spansEnded: 0, spansSampledOut: 0, tracesExported: 0, metricsExported: 0, droppedExports: 0, queueOverflows: 0 };
55
+ /**
56
+ * 1.2-S3 env 配置通道:CAR_OTEL_ENDPOINT(唯一开关)/ CAR_OTEL_SAMPLING(always_on|always_off|0..1,
57
+ * 非法值保持缺省 always_on——遥测面禁 fail-hard)/ CAR_OTEL_INTERVAL_MS / CAR_OTEL_SERVICE_NAME。
58
+ * 无 endpoint = null(默认关,noop 门面)。
59
+ */
60
+ export function telemetryConfigFromEnv(env = process.env) {
61
+ const endpoint = env.CAR_OTEL_ENDPOINT;
62
+ if (!endpoint)
63
+ return null;
64
+ const cfg = { endpoint };
65
+ if (env.CAR_OTEL_SERVICE_NAME)
66
+ cfg.serviceName = env.CAR_OTEL_SERVICE_NAME;
67
+ if (env.CAR_OTEL_INTERVAL_MS) {
68
+ const n = Number(env.CAR_OTEL_INTERVAL_MS);
69
+ if (Number.isFinite(n) && n >= 0)
70
+ cfg.intervalMs = n;
71
+ }
72
+ if (env.CAR_OTEL_SAMPLING) {
73
+ const s = env.CAR_OTEL_SAMPLING;
74
+ if (s === 'always_on' || s === 'always_off')
75
+ cfg.sampling = s;
76
+ else {
77
+ const r = Number(s);
78
+ if (Number.isFinite(r) && r >= 0 && r <= 1)
79
+ cfg.sampling = { ratio: r };
80
+ // 非法采样值:保持缺省 always_on(登记口径,禁 fail-hard)
81
+ }
82
+ }
83
+ return cfg;
84
+ }
85
+ const attrValue = (v) => typeof v === 'string' ? { stringValue: v } : typeof v === 'boolean' ? { boolValue: v } : { doubleValue: v };
86
+ export function createTelemetryFacade(config, opts = {}) {
87
+ const endpoint = config?.endpoint?.replace(/\/+$/, '');
88
+ if (!endpoint) {
89
+ return {
90
+ enabled: false,
91
+ getTracer: () => NOOP_TRACER,
92
+ getMeter: () => NOOP_METER,
93
+ flush: async () => { },
94
+ shutdown: async () => { },
95
+ stats: () => ({ ...NOOP_STATS }),
96
+ };
97
+ }
98
+ // 1.4-BUG-2:出站默认走 node:http(s)(非全局 fetch/undici)——win32 上 undici socket +
99
+ // 进程退出触发 libuv 断言崩溃(UV_HANDLE_CLOSING, 0xC0000409,Node 23.5 实测、22 不复现);
100
+ // 测试仍可注入 fetchImpl(缝保留)。res.resume() 排空响应体(连接干净归还)。
101
+ const fetchImpl = opts.fetchImpl ?? ((url, init) => new Promise(resolve => {
102
+ try {
103
+ const u = String(url);
104
+ const mod = u.startsWith('https:') ? httpsRequest : httpRequest;
105
+ const req = mod(u, { method: init?.method ?? 'GET', headers: init?.headers }, res => {
106
+ res.resume();
107
+ res.on('end', () => {
108
+ const st = res.statusCode ?? 0;
109
+ resolve({ ok: st >= 200 && st < 300, status: st });
110
+ });
111
+ });
112
+ req.setTimeout(timeoutMs, () => req.destroy(new Error('timeout')));
113
+ req.on('error', () => resolve({ ok: false, status: 599 })); // 网络/超时错误 → 599 可重试类
114
+ req.end(init?.body ?? null);
115
+ }
116
+ catch {
117
+ resolve({ ok: false, status: 599 });
118
+ }
119
+ }));
120
+ const now = opts.now ?? Date.now;
121
+ const sleep = config?.sleep ?? (ms => new Promise(r => setTimeout(r, ms)));
122
+ const timeoutMs = config?.exportTimeoutMs ?? 5_000;
123
+ const retryLimit = config?.retryLimit ?? 2;
124
+ const backoff = config?.retryBackoffMs ?? [1_000, 2_000];
125
+ const sampling = config?.sampling ?? 'always_on';
126
+ const maxBatchSize = config?.maxBatchSize ?? 512;
127
+ const maxQueueSize = config?.maxQueueSize ?? 2_048;
128
+ const baseHeaders = { 'content-type': 'application/json', ...(config?.headers ?? {}) };
129
+ const serviceName = config?.serviceName ?? 'car-runtime';
130
+ const spans = [];
131
+ // 计数器键 = name+labels JSON(聚合去重);记录体分离保存 name/labels——
132
+ // OTLP metric 名必须是纯 name(1.2-BUG-3:键直当名会把 labels JSON 拼进 metric 名,
133
+ // M8 最小实现即存在,meter 零生产调用点掩盖至今,1.2-S3 meter 桥接入时暴露)
134
+ const counters = new Map();
135
+ const stats = { spansEnded: 0, spansSampledOut: 0, tracesExported: 0, metricsExported: 0, droppedExports: 0, queueOverflows: 0 };
136
+ let tracer = null;
137
+ let meter = null;
138
+ let timer = null;
139
+ let flushing = false;
140
+ /**
141
+ * OTLP 出站(D-12b 口径:有界重试)——ok / 429/5xx/网络错误退避重试 ≤retryLimit /
142
+ * 4xx 业务错不重试(端点配置类问题,重试无益);重试耗尽返回 false 由调用方计入
143
+ * droppedExports(静默丢弃,永不抛错——BD-05「不抛错」收紧保持)。
144
+ */
145
+ const exportOtlp = async (path, body) => {
146
+ for (let attempt = 0;; attempt++) {
147
+ // 1.4-BUG-2:AbortSignal.timeout 的内部定时器在进程退出时触发 libuv win32 断言崩溃
148
+ // (UV_HANDLE_CLOSING, 0xC0000409)——改手动 AbortController + clearTimeout 确定性清理
149
+ const controller = new AbortController();
150
+ const abortTimer = setTimeout(() => controller.abort(), timeoutMs);
151
+ try {
152
+ const res = await fetchImpl(endpoint + path, {
153
+ method: 'POST',
154
+ headers: baseHeaders,
155
+ body: JSON.stringify(body),
156
+ signal: controller.signal,
157
+ });
158
+ if (res.ok)
159
+ return true;
160
+ if ((res.status === 429 || res.status >= 500) && attempt < retryLimit) {
161
+ await sleep(backoff[Math.min(attempt, backoff.length - 1)]);
162
+ continue;
163
+ }
164
+ return false;
165
+ }
166
+ catch {
167
+ // 网络错误/超时:可重试类
168
+ if (attempt < retryLimit) {
169
+ await sleep(backoff[Math.min(attempt, backoff.length - 1)]);
170
+ continue;
171
+ }
172
+ return false;
173
+ }
174
+ finally {
175
+ clearTimeout(abortTimer);
176
+ }
177
+ }
178
+ };
179
+ const flush = async () => {
180
+ if (flushing)
181
+ return;
182
+ flushing = true;
183
+ try {
184
+ const spanQueue = spans.splice(0, spans.length);
185
+ // maxBatchSize 分批(1.2-S3):tracesExported 按成功出站请求计数
186
+ for (let i = 0; i < spanQueue.length; i += maxBatchSize) {
187
+ const batch = spanQueue.slice(i, i + maxBatchSize);
188
+ const body = {
189
+ resource: { attributes: [{ key: 'service.name', value: { stringValue: serviceName } }] },
190
+ scopeSpans: [{
191
+ scope: { name: 'car-runtime' },
192
+ spans: batch.map(s => ({
193
+ traceId: s.traceId,
194
+ spanId: s.spanId,
195
+ ...(s.parentSpanId ? { parentSpanId: s.parentSpanId } : {}),
196
+ name: s.name,
197
+ kind: 'SPAN_KIND_INTERNAL',
198
+ startTimeUnixNano: String(s.startMs * 1e6),
199
+ endTimeUnixNano: String(s.endMs * 1e6),
200
+ attributes: Object.entries(s.attributes).map(([key, v]) => ({ key, value: attrValue(v) })),
201
+ // 1.7-S1(D-26):标准 events/status 加法字段(无事件 span 不出 events 键——空形态合法)
202
+ ...(s.events.length ? {
203
+ events: s.events.map(e => ({
204
+ timeUnixNano: String(e.atMs * 1e6),
205
+ name: e.name,
206
+ attributes: Object.entries(e.attributes).map(([key, v]) => ({ key, value: attrValue(v) })),
207
+ })),
208
+ } : {}),
209
+ status: { code: s.status },
210
+ })),
211
+ }],
212
+ };
213
+ if (await exportOtlp('/v1/traces', body))
214
+ stats.tracesExported++;
215
+ else
216
+ stats.droppedExports++;
217
+ }
218
+ if (counters.size) {
219
+ const batch = [...counters.entries()];
220
+ // 1.7-S2(D-27):累计基数保持——不清空,每次导出全量累计快照(真 CUMULATIVE)。
221
+ // 1.2-S3 起 clear() 使后续导出仅含增量却标 CUMULATIVE——intervalMs>0 时下游解读为总量回落;
222
+ // 内存有界由 label 基数保证(零内容枚举红线:AllowedLabels 3 计数器)
223
+ const body = {
224
+ resource: { attributes: [{ key: 'service.name', value: { stringValue: serviceName } }] },
225
+ scopeMetrics: [{
226
+ scope: { name: 'car-runtime' },
227
+ metrics: batch.map(([, c]) => ({
228
+ name: c.name,
229
+ sum: {
230
+ aggregationTemporality: 'AGGREGATION_TEMPORALITY_CUMULATIVE',
231
+ dataPoints: Object.entries(c.labels).length
232
+ ? Object.entries(c.labels).map(([k, v]) => ({ asInt: c.value, attributes: [{ key: k, value: { stringValue: v } }] }))
233
+ : [{ asInt: c.value }],
234
+ },
235
+ })),
236
+ }],
237
+ };
238
+ if (await exportOtlp('/v1/metrics', body))
239
+ stats.metricsExported++;
240
+ else
241
+ stats.droppedExports++;
242
+ }
243
+ }
244
+ finally {
245
+ flushing = false;
246
+ }
247
+ };
248
+ if ((config?.intervalMs ?? 0) > 0)
249
+ timer = setInterval(() => { void flush(); }, config.intervalMs);
250
+ const activeTracer = {
251
+ startSpan(name, startOpts) {
252
+ const record = {
253
+ // 1.4-S6:trace 透传(traceId 复用 + parentSpanId 挂链)——缺省仍为独立随机 trace(1.1 行为不变)
254
+ traceId: startOpts?.trace?.traceId ?? randomBytes(16).toString('hex'),
255
+ spanId: randomBytes(8).toString('hex'),
256
+ parentSpanId: startOpts?.trace?.parentSpanId ?? null,
257
+ name,
258
+ // 1.8-D31:起始时刻覆盖(跨隔离体计时 relay;缺省 now() 行为不变)
259
+ startMs: startOpts?.time?.startMs ?? now(),
260
+ endMs: 0,
261
+ attributes: { ...startOpts?.attributes },
262
+ events: [],
263
+ status: 'STATUS_CODE_UNSET',
264
+ };
265
+ return {
266
+ get traceId() { return record.traceId; },
267
+ get spanId() { return record.spanId; },
268
+ setAttribute(key, value) { record.attributes[key] = value; return this; },
269
+ // 1.7-S1(D-26):标准 events 编码——原形态属性 + 事件时刻(替换 event.<name> JSON 串)
270
+ addEvent(evtName, attrs) { record.events.push({ name: evtName, atMs: now(), attributes: attrs ? { ...attrs } : {} }); return this; },
271
+ // 1.7-S1(D-26):OTel 规范 exception 形态——exception.type/exception.message 事件属性
272
+ // + status ERROR;stacktrace 缺省不出站(D-26 口径:防路径/源码面外泄)
273
+ recordException(err) {
274
+ record.events.push({
275
+ name: 'exception',
276
+ atMs: now(),
277
+ attributes: {
278
+ 'exception.type': String(err?.name ?? 'Error'),
279
+ 'exception.message': String(err?.message ?? err),
280
+ },
281
+ });
282
+ record.status = 'STATUS_CODE_ERROR';
283
+ return this;
284
+ },
285
+ end(atMs) {
286
+ record.endMs = atMs ?? now();
287
+ stats.spansEnded++;
288
+ // head 采样于 end() 决策(1.2-S3):缺省 always_on 全量(1.1 行为不变);采样只减少出站
289
+ const keep = sampling === 'always_on' ? true
290
+ : sampling === 'always_off' ? false
291
+ : Math.random() < sampling.ratio;
292
+ if (!keep) {
293
+ stats.spansSampledOut++;
294
+ return;
295
+ }
296
+ // buffer 上限:溢出丢最旧 + 计数(内存有界)
297
+ if (spans.length >= maxQueueSize) {
298
+ spans.shift();
299
+ stats.queueOverflows++;
300
+ }
301
+ spans.push(record);
302
+ },
303
+ };
304
+ },
305
+ };
306
+ const activeMeter = {
307
+ createCounter(name) {
308
+ return {
309
+ add(value, labels = {}) {
310
+ const key = name + JSON.stringify(labels);
311
+ const cur = counters.get(key) ?? { name, labels, value: 0 };
312
+ cur.value += value;
313
+ counters.set(key, cur);
314
+ },
315
+ };
316
+ },
317
+ };
318
+ return {
319
+ enabled: true,
320
+ getTracer() { tracer ??= activeTracer; return tracer; },
321
+ getMeter() { meter ??= activeMeter; return meter; },
322
+ flush,
323
+ async shutdown() {
324
+ if (timer) {
325
+ clearInterval(timer);
326
+ timer = null;
327
+ }
328
+ await flush();
329
+ },
330
+ stats: () => ({ ...stats }),
331
+ };
332
+ }
@@ -0,0 +1,15 @@
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
+ export declare function toToolDefinitions(tools: Map<string, ToolDef>): ToolDefinition[];
@@ -0,0 +1,12 @@
1
+ export function toToolDefinitions(tools) {
2
+ const out = [];
3
+ for (const [name, t] of tools) {
4
+ out.push({
5
+ name,
6
+ ...(t.description !== undefined ? { description: t.description } : {}),
7
+ ...(t.parameters !== undefined ? { parameters: t.parameters } : {}),
8
+ declaredSideEffect: t.declaredSideEffect ?? 'write',
9
+ });
10
+ }
11
+ return out;
12
+ }
@@ -0,0 +1,55 @@
1
+ import type { TelemetryFacade } from './telemetry.ts';
2
+ export interface StepSpanHandle {
3
+ end(outcome: string): void;
4
+ recordException(e: unknown): void;
5
+ }
6
+ /** 1.8-D29:MCP 外呼 client span 句柄——traceparent = '' 表示无遥测(不注入 _meta) */
7
+ export interface McpClientSpanHandle {
8
+ readonly traceparent: string;
9
+ end(outcome: string): void;
10
+ recordException(e: unknown): void;
11
+ }
12
+ /** 1.8-D31:PTC worker 桥挂链句柄——workerTrace=null 表示无遥测(worker 零感知零改动) */
13
+ export interface PtcRelayHandle {
14
+ readonly workerTrace: {
15
+ traceId: string;
16
+ parentSpanId: string;
17
+ } | null;
18
+ /** worker 计时回传收口(主线程单点出站;error 路径 recordException D-26 形态) */
19
+ complete(startMs: number, endMs: number, outcome: string, error?: unknown): void;
20
+ }
21
+ export interface TurnSpanHandle {
22
+ traceId: string;
23
+ spanId: string;
24
+ /** 1.7-D28:turnId 事后回填(mcp-serve——facade 内部自增派生,调用前不可知;car run 直传不受影响) */
25
+ setTurnId(turnId: string): void;
26
+ /** 1.7-D28:turn 级异常留痕(D-26 exception 形态在 turn span 同样成立) */
27
+ recordException(e: unknown): void;
28
+ end(outcome: string): void;
29
+ stepSpan(step: number): StepSpanHandle;
30
+ /** 1.8-D29:MCP 外呼 client span(parent = turn span;noop 门面 traceparent='' 零注入) */
31
+ mcpToolSpan(serverId: string, tool: string): McpClientSpanHandle;
32
+ /** 1.8-D31:PTC worker 桥挂链面(noop 门面 workerTrace=null 零载荷) */
33
+ ptcRelay(): PtcRelayHandle;
34
+ }
35
+ /** 1.8-D29:W3C traceparent 出站形态(flags '01' = 本端记录中;采样为端点本地决策不跨端传播) */
36
+ export declare function formatTraceparent(traceId: string, spanId: string): string;
37
+ /**
38
+ * 1.8-D29:W3C traceparent 解析——严格校验(version 00 / trace-id 32hex 非 / span-id 16hex 非 / 全零拒绝)。
39
+ * 畸形返回 null = 挂链放弃(调用方走新 trace——协议面 fail-open,禁 fail-hard)。
40
+ */
41
+ export declare function parseTraceparent(tp: string): {
42
+ traceId: string;
43
+ parentSpanId: string;
44
+ } | null;
45
+ export declare class TurnTracer {
46
+ #private;
47
+ constructor(facade: TelemetryFacade | null | undefined);
48
+ /** 1.8-D30:opts.trace = 远端挂链(mcp-serve session_turn `_meta.traceparent` 提取后传入;缺省新 trace 不变) */
49
+ startTurn(turnId: string, opts?: {
50
+ trace?: {
51
+ traceId: string;
52
+ parentSpanId: string;
53
+ };
54
+ }): TurnSpanHandle;
55
+ }