@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
@@ -1,127 +0,0 @@
1
- /**
2
- * 1.3 · 发布资产自动化(S30):release-artifacts.ts 制品构建 + release.yml 结构校验
3
- *
4
- * 覆盖(1.3-迭代规划 W4-1/W4-3/W4-4;防幽灵门禁——CI 新步骤必须有本地可验证断言):
5
- * - buildReleaseArtifacts:五件套构建(dist 注入临时目录)、SHA256SUMS 裸字节自检(G-11 口径)、
6
- * SBOM 同构(G-05 口径)、构建审计行完整、sha256 与外部 sha256sum 口径一致
7
- * - releaseAssetNames 单一事实源:五件套、**不含 car-release.pub**(D-14 资产集迁移钉死)
8
- * - release.yml 结构断言:contents: write / id-token: write / cosign keyless(--yes + bundle)/
9
- * gh release upload --clobber / npm publish --provenance / **upload 先于 publish**(W4 步骤序红线)/
10
- * cosign-installer 钉版(R-4)
11
- */
12
- import { test } from 'node:test'
13
- import assert from 'node:assert/strict'
14
- import { createHash } from 'node:crypto'
15
- import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
16
- import { tmpdir } from 'node:os'
17
- import { join, dirname } from 'node:path'
18
- import { fileURLToPath } from 'node:url'
19
- import { buildReleaseArtifacts, releaseAssetNames } from '../scripts/release-artifacts.ts'
20
-
21
- const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
22
-
23
- function withTempDir(name: string, fn: (dir: string) => Promise<void> | void): Promise<void> {
24
- const dir = mkdtempSync(join(tmpdir(), `car-s30-${name}-`))
25
- try {
26
- const r = fn(dir)
27
- const cleanup = () => { try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ } }
28
- if (r instanceof Promise) return r.finally(cleanup)
29
- cleanup()
30
- return Promise.resolve()
31
- } catch (e) {
32
- try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ }
33
- throw e
34
- }
35
- }
36
-
37
- test('S30: buildReleaseArtifacts——五件套之四件落盘(bundle 由签名轨产出)+ SHA256SUMS 裸字节自检', () => {
38
- withTempDir('build', dir => {
39
- const records: Array<{ stage: string; detail: string }> = []
40
- const r = buildReleaseArtifacts({
41
- version: '0.0.0-s30test',
42
- dist: dir,
43
- onRecord: (stage, detail) => records.push({ stage, detail }),
44
- })
45
- // 制品在位
46
- assert.ok(existsSync(r.tgzPath), 'tgz 在位')
47
- assert.ok(existsSync(r.sumsPath), 'SHA256SUMS 在位')
48
- assert.ok(existsSync(r.sbomPath), 'sbom.json 在位')
49
- // G-11 口径:外部 sha256sum 可验证(裸字节哈希)
50
- const bytes = readFileSync(r.tgzPath)
51
- assert.equal(createHash('sha256').update(bytes).digest('hex'), r.sha256)
52
- assert.equal(readFileSync(r.sumsPath, 'utf-8'), `${r.sha256} car-runtime-0.0.0-s30test.tgz\n`)
53
- assert.match(readFileSync(r.sumsPath, 'utf-8').trim(), /^[0-9a-f]{64} car-runtime-0\.0\.0-s30test\.tgz$/)
54
- // G-05 同构:SBOM 组件面 + 零运行时依赖
55
- const sbom = JSON.parse(readFileSync(r.sbomPath, 'utf-8')) as { bomFormat: string; components: unknown[]; runtimeDependencies: unknown[] }
56
- assert.equal(sbom.bomFormat, 'CAR-SBOM')
57
- assert.ok(sbom.components.length >= 10, 'src/*.ts + package.json + README')
58
- assert.deepEqual(sbom.runtimeDependencies, [], '零运行时依赖红线')
59
- // 构建审计完整(archive / sha256sums / sbom 三段记录)
60
- assert.deepEqual(records.map(x => x.stage), ['archive', 'sha256sums', 'sbom'])
61
- assert.equal(r.bytes, bytes.length)
62
- })
63
- })
64
-
65
- test('S30: releaseAssetNames 单一事实源——五件套(D-14:car-release.pub 移除,bundle 在列)', () => {
66
- const names = releaseAssetNames('9.9.9')
67
- assert.deepEqual(names, [
68
- 'car-runtime-9.9.9.tgz',
69
- 'car-runtime-9.9.9.tgz.sig.bundle',
70
- 'SHA256SUMS',
71
- 'sbom.json',
72
- 'release-audit.jsonl',
73
- ])
74
- assert.equal(names.includes('car-release.pub'), false, 'D-14 资产集迁移:公钥不再是验证锚')
75
- })
76
-
77
- test('S30: G-10/CI/测试三方同源——pipeline 引用 releaseAssetNames(静态断言防漂移,R-3)', () => {
78
- const pipeline = readFileSync(join(ROOT, 'scripts', 'release-pipeline.ts'), 'utf-8')
79
- assert.match(pipeline, /import \{ buildReleaseArtifacts, releaseAssetNames \} from '\.\/release-artifacts\.ts'/)
80
- assert.match(pipeline, /releaseAssetNames\(VERSION\)/, 'G-10 need-list 改引单一事实源')
81
- // car-release.pub 在本地预演轨 keypair 验签(keysDir)合法存在——D-15 双轨;资产清单面已由 releaseAssetNames 收口
82
- // G-11 保留独立复核(门禁不写死为 true)
83
- assert.match(pipeline, /S6-制品封装/)
84
- assert.match(pipeline, /S2 产出独立复核/)
85
- })
86
-
87
- // ==================== release.yml 结构校验(W4-2 防幽灵门禁) ====================
88
-
89
- const yml = readFileSync(join(ROOT, '.github', 'workflows', 'release.yml'), 'utf-8')
90
-
91
- test('S30: release.yml 权限面——contents: write(上传需要)+ id-token: write(keyless 凭据)', () => {
92
- const permBlock = yml.slice(yml.indexOf('permissions:'), yml.indexOf('jobs:'))
93
- assert.match(permBlock, /contents:\s*write/)
94
- assert.match(permBlock, /id-token:\s*write/)
95
- })
96
-
97
- test('S30: release.yml 步骤面——构建/签名/上传/publish 四段齐备且序正确(upload 先于 publish)', () => {
98
- // 四段关键词
99
- assert.match(yml, /node --experimental-transform-types scripts\/release-artifacts\.ts/, '构建步骤调单一事实源脚本')
100
- assert.match(yml, /sigstore\/cosign-installer@v3\.7\.0/, 'cosign 钉版(R-4)')
101
- assert.match(yml, /cosign sign-blob --yes --bundle/, 'keyless 签名(--yes 免交互 + bundle 产出)')
102
- assert.match(yml, /gh release upload "\$VERSION" "\$f" --clobber/, '五件套上传(--clobber 幂等;目标=版本号——D-14 裸 tag 形态)')
103
- assert.match(yml, /npm publish --provenance --tag "\$TAG"/, 'publish 步骤不变(provenance 口径)')
104
- // 步骤序红线:upload 步在 publish 步之前(上传失败 job 红,publish 不带病出街)
105
- const uploadAt = yml.indexOf('gh release upload')
106
- const publishAt = yml.indexOf('npm publish --provenance')
107
- assert.ok(uploadAt > -1 && publishAt > -1 && uploadAt < publishAt, 'upload 先于 publish(W4 步骤序红线)')
108
- // 签名记录追加进构建审计(五件套之五的内容完整性)
109
- assert.match(yml, /release-audit\.jsonl/, '审计文件在签名步被追加')
110
- })
111
-
112
- test('S30: release.yml 解析安全——name 行不得含未引号冒号(1.3-BUG-2:`name: x (y: z)` 令 YAML 解析崩溃,release 事件静默失效)', () => {
113
- // startup failure 形态:run.name = 文件路径、jobs 为空、release 事件不触发——结构断言在此钉死本失败类
114
- const offenders = yml.split('\n').filter(l => /^\s*- name: [^'"].*:\s/.test(l))
115
- assert.deepEqual(offenders, [], `以下 step name 含未引号冒号+空格:${JSON.stringify(offenders)}`)
116
- // 上传目标 = 包版本号(D-14 裸 tag:release tag == version;dispatch 触发亦可用)
117
- assert.match(yml, /gh release upload "\$VERSION" "\$f" --clobber/)
118
- })
119
-
120
- test('S30: release.yml keyless 验证口径——sign-blob 无 --key(keyless 非 keypair)', () => {
121
- // 正式轨不得使用本地 keypair(私钥不出本地红线);keypair 仅存在于本地预演轨 release-pipeline.ts
122
- const signStep = yml.slice(yml.indexOf('cosign sign-blob'))
123
- assert.doesNotMatch(signStep, /--key/, 'CI 签名步无 keypair 材料')
124
- assert.doesNotMatch(yml, /car-release\.key|COSIGN_PASSWORD/, 'CI 无本地私钥/口令引用')
125
- const pipeline = readFileSync(join(ROOT, 'scripts', 'release-pipeline.ts'), 'utf-8')
126
- assert.match(pipeline, /car-release\.key/, '本地预演轨保留 keypair(D-15 双轨语义)')
127
- })
@@ -1,329 +0,0 @@
1
- /**
2
- * 1.4-S1~S5 · 真实模型接线(S31):声明面统一 / system 落链 / llm.* 配置 / loopback 豁免 /
3
- * 取消透传 / CarM8Error 保形 / car run 装配重做(D-13)+ 多步真 E2E
4
- *
5
- * 覆盖(1.4-迭代规划 W1-1~W1-7):
6
- * - toToolDefinitions:schema 透传 / T-22 缺省 write 收口
7
- * - 'system' 事件:投影 role:'system'(§3.5.5 新可见输入 = 新事件类型)
8
- * - llm.* 配置:校验 fail-visible / 优先级 flag > env > config / CAR_LLM_* env
9
- * - D-12a:http loopback 豁免(127.0.0.1/localhost/[::1])+ 非 loopback http 拒绝
10
- * - 取消(D-19):TurnAborted 首次实抛(finish 检查前序)+ runTurn 收口 aborted
11
- * - CarM8Error 保形:A080001 userHint/retryable 到 turnEnd
12
- * - 多步真 E2E:本地 loopback 假 SSE 服务——system 出站 / 工具 schema 出站 / Bearer 凭据 /
13
- * 第二请求含 toolResult / 声明面=授权面;car run spawn 全链 + 无配置 exit 2
14
- */
15
- import { test } from 'node:test'
16
- import assert from 'node:assert/strict'
17
- import { spawn } from 'node:child_process'
18
- import { createServer, type Server } from 'node:http'
19
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
20
- import { tmpdir } from 'node:os'
21
- import { join, dirname } from 'node:path'
22
- import { fileURLToPath } from 'node:url'
23
- import { toToolDefinitions } from '../src/runtime-core/tools.ts'
24
- import { createOpenAICompatAdapter, RuntimeCore } from '../src/runtime-core/llm.ts'
25
- import { chatStep } from '../src/runtime-core/chatStep.ts'
26
- import { CredentialService } from '../src/runtime-core/credentials.ts'
27
- import { runTurn } from '../src/loop/stop.ts'
28
- import { Context } from '../src/kernel/context.ts'
29
- import { SessionLog } from '../src/session/log.ts'
30
- import { mergeLlmConfig, loadCarConfig } from '../src/load/config.ts'
31
- import { doctorModelReadiness } from '../src/dx/doctor.ts'
32
-
33
- // 密钥扫描口径(generic-secret-assign):夹具凭据经变量注入(赋值行无引号字面量);值非真实凭据
34
- const TEST_KEY = 'test-key-123'
35
-
36
- const CLI = join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'cli.ts')
37
-
38
- function withTempDir(name: string, fn: (dir: string) => Promise<void> | void): Promise<void> {
39
- const dir = mkdtempSync(join(tmpdir(), `car-s31-${name}-`))
40
- try {
41
- const r = fn(dir)
42
- const cleanup = () => { try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ } }
43
- if (r instanceof Promise) return r.finally(cleanup)
44
- cleanup()
45
- return Promise.resolve()
46
- } catch (e) {
47
- try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ }
48
- throw e
49
- }
50
- }
51
-
52
- /** SSE 流响应(openai-compat 形态) */
53
- function sse(chunks: object[]): string {
54
- return chunks.map(c => `data: ${JSON.stringify(c)}\n\n`).join('') + 'data: [DONE]\n\n'
55
- }
56
-
57
- const TOOL_CALL_SSE = (name: string, argsJson: string, id: string) => sse([
58
- { choices: [{ delta: { role: 'assistant', content: '' } }] },
59
- { choices: [{ delta: { tool_calls: [{ index: 0, id, function: { name, arguments: argsJson.slice(0, 3) } }] } }] },
60
- { choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: argsJson.slice(3) } }] } }] },
61
- { choices: [{ delta: {}, finish_reason: 'tool_calls' }] },
62
- ])
63
- const TEXT_SSE = (text: string) => sse([
64
- { choices: [{ delta: { role: 'assistant', content: '' } }] },
65
- { choices: [{ delta: { content: text } }] },
66
- { choices: [{ delta: {}, finish_reason: 'stop' }] },
67
- ])
68
-
69
- /** 本地假 OpenAI 兼容服务:捕获请求体,按脚本回放 SSE */
70
- function fakeOpenAI(script: Array<(body: any) => string>): Promise<{ server: Server; url: string; bodies: any[]; headers: any[]; done: Promise<void> }> {
71
- const bodies: any[] = []
72
- const headers: any[] = []
73
- let idx = 0
74
- let finish: (v: void) => void = () => {}
75
- const done = new Promise<void>(r => { finish = r })
76
- const server = createServer((req, res) => {
77
- let raw = ''
78
- req.on('data', c => { raw += c })
79
- req.on('end', () => {
80
- bodies.push(JSON.parse(raw))
81
- headers.push(req.headers)
82
- res.writeHead(200, { 'content-type': 'text/event-stream' })
83
- const step = script[Math.min(idx, script.length - 1)]
84
- idx++
85
- res.end(step(bodies[bodies.length - 1]))
86
- if (idx >= script.length) finish()
87
- })
88
- })
89
- const url = new Promise<string>(r => server.listen(0, '127.0.0.1', () => r(`http://127.0.0.1:${(server.address() as { port: number }).port}/v1`)))
90
- return url.then(u => ({ server, url: u, bodies, headers, done }))
91
- }
92
-
93
- // ==================== S1:声明面统一 ====================
94
-
95
- test('S31: toToolDefinitions——description/parameters 透传 + T-22 缺省 write 收口 + 声明面=授权面', () => {
96
- const map = new Map<string, any>([
97
- ['full', { declaredSideEffect: 'readonly' as const, run: async () => 1, description: '读工具', parameters: { type: 'object', properties: { q: { type: 'string' } } } }],
98
- ['bare', { declaredSideEffect: 'write' as const, run: async () => 2 }],
99
- ])
100
- const defs = toToolDefinitions(map)
101
- assert.deepEqual(defs, [
102
- { name: 'full', description: '读工具', parameters: { type: 'object', properties: { q: { type: 'string' } } }, declaredSideEffect: 'readonly' },
103
- { name: 'bare', declaredSideEffect: 'write' },
104
- ])
105
- // declaredSideEffect 不出站的口径由 llm.ts 出站映射承载(llm.ts:237 只带 name/description/parameters)
106
- })
107
-
108
- // ==================== S2:system 落链 ====================
109
-
110
- test('S31: system 事件——落链 → deriveMessages 投影 role:system(§3.5.5 新可见输入 = 新事件类型)', () => {
111
- const log = new SessionLog('S-sys-1')
112
- log.append('runtime', 'system', 'system', '你是测试助手')
113
- log.append('user', 'user', 'T0', 'hi')
114
- const msgs = log.deriveMessages()
115
- assert.deepEqual(msgs[0], { role: 'system', content: '你是测试助手' })
116
- assert.equal(log.verifyChain(), null, '哈希链含 system 事件仍完整')
117
- assert.equal(log.assertModelVisibleLogged().ok, true, 'N1 含 system 事件成立')
118
- })
119
-
120
- // ==================== S3:llm.* 配置通道 ====================
121
-
122
- test('S31: llm.* 配置——校验 fail-visible / 优先级 flag > env > config / CAR_LLM_* env 通道', () => {
123
- withTempDir('llm-config', dir => {
124
- // 未知键 / 类型错 fail-visible
125
- writeFileSync(join(dir, 'bad.json'), JSON.stringify({ llm: { temperature: 0.7 } }))
126
- assert.match(loadCarConfig({ explicitPath: join(dir, 'bad.json') }).error!, /未知键 llm\.temperature/)
127
- writeFileSync(join(dir, 'bad2.json'), JSON.stringify({ llm: { maxTokens: 1.5 } }))
128
- assert.match(loadCarConfig({ explicitPath: join(dir, 'bad2.json') }).error!, /llm\.maxTokens 必须为正整数/)
129
- // 有效配置
130
- const good = join(dir, 'good.json')
131
- writeFileSync(good, JSON.stringify({ llm: { baseUrl: 'https://cfg.example/v1', model: 'cfg-model', maxTokens: 100, allowEnvFallback: true } }))
132
- const cfg = loadCarConfig({ explicitPath: good }).config
133
- // 优先级:config < env < flag
134
- const c = mergeLlmConfig({}, cfg)
135
- assert.equal(c.baseUrl, 'https://cfg.example/v1')
136
- assert.equal(c.model, 'cfg-model')
137
- assert.equal(c.allowEnvFallback, true)
138
- const e = mergeLlmConfig({ CAR_LLM_BASE_URL: 'https://env.example/v1', CAR_LLM_MODEL: 'env-model' }, cfg)
139
- assert.equal(e.baseUrl, 'https://env.example/v1')
140
- assert.equal(e.model, 'env-model')
141
- const f = mergeLlmConfig({ CAR_LLM_MODEL: 'env-model' }, cfg, { baseUrl: 'https://flag.example/v1', model: 'flag-model', maxTokens: 7 })
142
- assert.equal(f.baseUrl, 'https://flag.example/v1')
143
- assert.equal(f.model, 'flag-model')
144
- assert.equal(f.maxTokens, 7)
145
- // CAR_ALLOW_ENV_CREDENTIALS 显式意见压过配置(=0 关)
146
- assert.equal(mergeLlmConfig({ CAR_ALLOW_ENV_CREDENTIALS: '0' }, cfg).allowEnvFallback, false)
147
- })
148
- })
149
-
150
- test('S31: doctorModelReadiness——configured/not-found/env 三态(值不打印红线)', () => {
151
- withTempDir('doctor-model', dir => {
152
- const d1 = doctorModelReadiness({ env: {}, cwd: dir })
153
- assert.equal(d1.configured, false)
154
- assert.equal(d1.credential, 'not-found')
155
- assert.match(d1.detail, /未配置/)
156
- const d2 = doctorModelReadiness({
157
- env: { CAR_LLM_BASE_URL: 'https://x/v1', CAR_LLM_MODEL: 'm', OPENAI_API_KEY: TEST_KEY, CAR_ALLOW_ENV_CREDENTIALS: '1' },
158
- cwd: dir,
159
- })
160
- assert.equal(d2.configured, true)
161
- assert.equal(d2.credential, 'env')
162
- assert.equal(d2.envFallback, 'on')
163
- assert.equal(d2.detail.includes(TEST_KEY), false, '凭据值永不打印')
164
- })
165
- })
166
-
167
- // ==================== S4:D-12a + 取消 + CarM8Error ====================
168
-
169
- test('S31: D-12a loopback 豁免——http 仅 127.0.0.1/localhost/[::1];非 loopback http 拒绝', () => {
170
- assert.ok(createOpenAICompatAdapter({ baseUrl: 'http://127.0.0.1:1234/v1' }), 'loopback http 允许')
171
- assert.ok(createOpenAICompatAdapter({ baseUrl: 'http://localhost:1234/v1' }), 'localhost http 允许')
172
- assert.ok(createOpenAICompatAdapter({ baseUrl: 'http://[::1]:1234/v1' }), 'IPv6 loopback http 允许')
173
- assert.throws(() => createOpenAICompatAdapter({ baseUrl: 'http://10.0.0.1:8080/v1' }), /CAR-E-LLM-TLS/, '非 loopback http 拒绝')
174
- assert.throws(() => createOpenAICompatAdapter({ baseUrl: 'http://example.com/v1' }), /CAR-E-LLM-TLS/)
175
- assert.ok(createOpenAICompatAdapter({ baseUrl: 'https://api.example.com/v1' }), 'https 任意域允许')
176
- })
177
-
178
- test('S31: 取消(D-19)——TurnAborted 首次实抛:pre-aborted 走 finish 前检查;runTurn 收口 aborted', async () => {
179
- await withTempDir('cancel', async dir => {
180
- const log = new SessionLog('S-cancel')
181
- log.append('runtime', 'system', 'system', 'sys')
182
- log.append('user', 'user', 'T0', 'hi')
183
- const core = new RuntimeCore()
184
- const signal = { aborted: true } // pre-aborted:适配器 attempt 前静默收口(零 chunk)
185
- const tools = new Map<string, any>([['t', { declaredSideEffect: 'readonly', run: async () => 'ok' }]])
186
- // pre-aborted:适配器 attempt 前静默收口(零 chunk)→ chatStep finish 检查前抛 TurnAborted
187
- const adapter = createOpenAICompatAdapter({ baseUrl: 'http://127.0.0.1:1/v1', fetchImpl: (async () => { throw new Error('不应发起请求') }) as typeof fetch })
188
- core.registerLlmAdapter(adapter)
189
- const r = await runTurn({
190
- log, turnId: 'T0', tools, signal,
191
- model: () => chatStep({ core, log, turnId: 'T0', model: 'm', tools: toToolDefinitions(tools), signal }),
192
- })
193
- assert.equal(r.reason, 'aborted', 'runTurn 既有 catch 收口 aborted')
194
- const turnEnd = log.events.find(e => e.kind === 'turnEnd')!
195
- assert.equal((turnEnd.meta as any)?.reason, 'aborted')
196
- assert.equal(log.events.some(e => e.kind === 'assistant'), false, '无半截 assistant 落链')
197
- })
198
- })
199
-
200
- test('S31: CarM8Error 保形——A080001 code/userHint/retryable 进 turnEnd(引导文案可达用户)', async () => {
201
- await withTempDir('m8-error', async dir => {
202
- const log = new SessionLog('S-m8err')
203
- log.append('user', 'user', 'T0', 'hi')
204
- const cred = new CredentialService()
205
- const core = new RuntimeCore(cred)
206
- core.registerLlmAdapter(createOpenAICompatAdapter({ baseUrl: 'http://127.0.0.1:1/v1', credentials: cred, provider: 'openai-compat' }))
207
- const r = await runTurn({
208
- log, turnId: 'T0',
209
- tools: new Map<string, any>(),
210
- model: () => chatStep({ core, log, turnId: 'T0', model: 'm', tools: [] }),
211
- })
212
- assert.equal(r.reason, 'error')
213
- const turnEnd = log.events.find(e => e.kind === 'turnEnd')!
214
- const meta = turnEnd.meta as any
215
- assert.equal(meta.code, 'A080001')
216
- assert.match(meta.userHint, /car doctor/, '引导文案进 turnEnd(1.4 前被 String(e) 拍平)')
217
- assert.equal(meta.retryable, false)
218
- })
219
- })
220
-
221
- // ==================== S5:多步真 E2E(真 HTTP + 真装配) ====================
222
-
223
- test('S31: 多步真 E2E——system 出站 / 工具 schema 出站 / Bearer 凭据 / 第二请求含 toolResult / 声明面=授权面', async () => {
224
- await withTempDir('multistep', async dir => {
225
- const { server, url, bodies, headers, done } = await fakeOpenAI([
226
- () => TOOL_CALL_SSE('echo_tool', '{"text":"hi"}', 'call_1'),
227
- () => TEXT_SSE('完成了'),
228
- ])
229
- try {
230
- const log = new SessionLog('S-e2e')
231
- log.append('runtime', 'system', 'system', '你是集成测试助手')
232
- log.append('user', 'user', 'T0', '请调用 echo_tool')
233
- const cred = new CredentialService({ keychainReader: s => (s === 'car-runtime/openai-compat' ? 'test-key-123' : null) })
234
- const core = new RuntimeCore(cred)
235
- const ctx = new Context()
236
- core.bindContext(ctx)
237
- core.registerLlmAdapter(createOpenAICompatAdapter({
238
- baseUrl: url, credentials: cred, provider: 'openai-compat',
239
- }), { default: true })
240
- const tools = new Map<string, any>([[
241
- 'echo_tool',
242
- { declaredSideEffect: 'readonly', description: '回声工具', parameters: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] }, run: async (a: any) => 'echo:' + a.text },
243
- ]])
244
- const r = await runTurn({
245
- log, turnId: 'T0', tools,
246
- model: () => chatStep({ core, log, turnId: 'T0', model: 'test-model', tools: toToolDefinitions(tools) }),
247
- })
248
- assert.equal(r.reason, 'completed')
249
- assert.equal(r.steps, 2, '两步:toolUse → 工具执行 → stop')
250
- await done
251
- // 请求 1:system 消息出站(§3.5.5)+ 工具 schema 出站 + Bearer 凭据
252
- assert.equal(bodies[0].messages[0].role, 'system')
253
- assert.equal(bodies[0].messages[0].content, '你是集成测试助手')
254
- assert.equal(bodies[0].tools.length, 1)
255
- assert.equal(bodies[0].tools[0].function.name, 'echo_tool')
256
- assert.deepEqual(bodies[0].tools[0].function.parameters, { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] })
257
- assert.equal(bodies[0].tools[0].function.description, '回声工具')
258
- assert.equal(headers[0].authorization, 'Bearer ' + TEST_KEY, '凭据经 CredentialService→Bearer')
259
- assert.equal(JSON.stringify(bodies[0]).includes(TEST_KEY), false, '凭据值不出站到消息体')
260
- // 请求 2:第二请求含 toolResult 消息(executeBatch 落链 → 投影 → 出站)
261
- const toolMsg = bodies[1].messages.find((m: any) => m.role === 'tool')
262
- assert.ok(toolMsg, '第二请求含 tool 消息')
263
- assert.equal(toolMsg.tool_call_id, 'call_1')
264
- assert.equal(toolMsg.content, '"echo:hi"')
265
- // 落链面:assistant/toolCall/toolResult 齐备(N1)
266
- assert.equal(log.assertModelVisibleLogged().ok, true)
267
- const final = log.deriveMessages().at(-1)!
268
- assert.equal(final.role, 'assistant')
269
- assert.equal(final.content, '完成了')
270
- void server
271
- } finally {
272
- server.close()
273
- await done
274
- }
275
- })
276
- })
277
- test('S31: spawn CLI 全链——car run 真路径(--prompt/--base-url/--model + env 凭据)+ 无配置 exit 2', async () => {
278
- await withTempDir('cli-real', async dir => {
279
- const plugin = `export const manifest = { name: 'p', version: '1.0.0' }\n` +
280
- `export default function apply(api) {\n` +
281
- ` api.registerTool({ name: 'echo_tool', description: '回声工具', parameters: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] }, declaredSideEffect: 'readonly', run: async (a) => 'echo:' + a.text })\n` +
282
- `}\n`
283
- writeFileSync(join(dir, 'p.ts'), plugin)
284
- const bodies: any[] = []
285
- let n = 0
286
- const server = createServer((req, res) => {
287
- let raw = ''
288
- req.on('data', c => { raw += c })
289
- req.on('end', () => {
290
- bodies.push(JSON.parse(raw))
291
- res.writeHead(200, { 'content-type': 'text/event-stream' })
292
- res.end(n++ === 0 ? TOOL_CALL_SSE('echo_tool', '{"text":"hi"}', 'call_1') : TEXT_SSE('完成了'))
293
- })
294
- })
295
- await new Promise<void>(r => server.listen(0, '127.0.0.1', r))
296
- const port = (server.address() as { port: number }).port
297
- const child = spawn(process.execPath, ['--experimental-transform-types', CLI, 'run', 'p.ts',
298
- '--prompt', 'call echo_tool with text hi', '--base-url', `http://127.0.0.1:${port}/v1`, '--model', 'cli-model',
299
- '--system', '你是 CLI 集成测试助手'], {
300
- cwd: dir,
301
- env: { ...process.env, OPENAI_API_KEY: TEST_KEY, CAR_ALLOW_ENV_CREDENTIALS: '1' },
302
- stdio: ['ignore', 'pipe', 'pipe'],
303
- })
304
- let out = ''
305
- let err = ''
306
- child.stdout.on('data', d => { out += d })
307
- child.stderr.on('data', d => { err += d })
308
- const code = await new Promise<number>(resolve => child.on('exit', c => resolve(c ?? -1)))
309
- server.close()
310
- assert.equal(code, 0, err)
311
- assert.match(out, /turnEnd=completed/)
312
- // system 出站(--system flag > 插件 setSystemPrompt)
313
- assert.equal(bodies[0].messages[0].role, 'system')
314
- assert.equal(bodies[0].messages[0].content, '你是 CLI 集成测试助手')
315
- assert.equal(bodies[0].tools[0].function.name, 'echo_tool')
316
- assert.match(out, /回放|投影/)
317
-
318
- // 无模型配置 → exit 2 显式引导(D-13)
319
- const noCfg = await new Promise<{ code: number; stderr: string }>(resolve => {
320
- const c2 = spawn(process.execPath, ['--experimental-transform-types', CLI, 'run', 'p.ts'], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] })
321
- let e2 = ''
322
- c2.stderr.on('data', d => { e2 += d })
323
- c2.on('exit', c => resolve({ code: c ?? -1, stderr: e2 }))
324
- })
325
- assert.equal(noCfg.code, 2)
326
- assert.match(noCfg.stderr, /CAR-E-LLM-CONFIG/)
327
- assert.match(noCfg.stderr, /--demo/, '引导至演示流')
328
- })
329
- })