@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.
- package/README.md +17 -1
- package/dist/SHA256SUMS +1 -0
- package/dist/authz/authz.d.ts +44 -0
- package/dist/authz/authz.js +65 -0
- package/dist/car-runtime-1.10.0.tgz +0 -0
- package/dist/car-runtime-1.10.0.tgz.sig.bundle +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +825 -0
- package/dist/dx/doctor.d.ts +62 -0
- package/dist/dx/doctor.js +127 -0
- package/dist/governance/dump.d.ts +47 -0
- package/dist/governance/dump.js +17 -0
- package/dist/host/facade.d.ts +13 -0
- package/dist/host/facade.js +154 -0
- package/dist/host/hostGateway.d.ts +117 -0
- package/dist/host/hostGateway.js +65 -0
- package/dist/host/mappings.d.ts +27 -0
- package/dist/host/mappings.js +52 -0
- package/dist/host/stdio.d.ts +14 -0
- package/dist/host/stdio.js +83 -0
- package/dist/kernel/context.d.ts +76 -0
- package/dist/kernel/context.js +203 -0
- package/dist/kernel/events.d.ts +37 -0
- package/dist/kernel/events.js +82 -0
- package/dist/load/config.d.ts +79 -0
- package/dist/load/config.js +285 -0
- package/dist/load/loader.d.ts +72 -0
- package/dist/load/loader.js +162 -0
- package/dist/load/registry.d.ts +44 -0
- package/dist/load/registry.js +58 -0
- package/dist/load/report.d.ts +98 -0
- package/dist/load/report.js +301 -0
- package/dist/load/sigGate.d.ts +22 -0
- package/dist/load/sigGate.js +68 -0
- package/dist/load/sign.d.ts +25 -0
- package/dist/load/sign.js +47 -0
- package/dist/load/verifier.d.ts +35 -0
- package/dist/load/verifier.js +56 -0
- package/dist/loop/goal.d.ts +31 -0
- package/dist/loop/goal.js +34 -0
- package/dist/loop/recover.d.ts +17 -0
- package/dist/loop/recover.js +39 -0
- package/dist/loop/stop.d.ts +79 -0
- package/dist/loop/stop.js +151 -0
- package/dist/mcp/gateway.d.ts +96 -0
- package/dist/mcp/gateway.js +69 -0
- package/dist/mcp/httpClient.d.ts +27 -0
- package/dist/mcp/httpClient.js +196 -0
- package/dist/mcp/stdioClient.d.ts +8 -0
- package/dist/mcp/stdioClient.js +73 -0
- package/dist/ptc/budget.d.ts +21 -0
- package/dist/ptc/budget.js +36 -0
- package/dist/ptc/erasable.d.ts +14 -0
- package/dist/ptc/erasable.js +27 -0
- package/dist/ptc/runCode.d.ts +65 -0
- package/dist/ptc/runCode.js +140 -0
- package/dist/ptc/schemaRender.d.ts +15 -0
- package/dist/ptc/schemaRender.js +90 -0
- package/dist/ptc/sdk.d.ts +27 -0
- package/dist/ptc/sdk.js +23 -0
- package/dist/ptc/worker-entry.d.ts +1 -0
- package/dist/ptc/worker-entry.js +71 -0
- package/dist/release-audit.jsonl +4 -0
- package/dist/runtime-core/chatStep.d.ts +40 -0
- package/dist/runtime-core/chatStep.js +104 -0
- package/dist/runtime-core/credentials.d.ts +49 -0
- package/dist/runtime-core/credentials.js +118 -0
- package/dist/runtime-core/errors.d.ts +30 -0
- package/dist/runtime-core/errors.js +36 -0
- package/dist/runtime-core/llm.d.ts +76 -0
- package/dist/runtime-core/llm.js +553 -0
- package/dist/runtime-core/redaction.d.ts +39 -0
- package/dist/runtime-core/redaction.js +79 -0
- package/dist/runtime-core/telemetry.d.ts +88 -0
- package/dist/runtime-core/telemetry.js +332 -0
- package/dist/runtime-core/tools.d.ts +15 -0
- package/dist/runtime-core/tools.js +12 -0
- package/dist/runtime-core/trace.d.ts +55 -0
- package/dist/runtime-core/trace.js +137 -0
- package/dist/runtime-core/types.d.ts +84 -0
- package/dist/runtime-core/types.js +13 -0
- package/dist/sandbox/sandbox.d.ts +68 -0
- package/dist/sandbox/sandbox.js +153 -0
- package/dist/sbom.json +263 -0
- package/dist/seam.d.ts +21 -0
- package/dist/seam.js +20 -0
- package/dist/security/secrets-baseline.d.ts +43 -0
- package/dist/security/secrets-baseline.js +176 -0
- package/dist/security/secrets.d.ts +45 -0
- package/dist/security/secrets.js +138 -0
- package/dist/session/export.d.ts +55 -0
- package/dist/session/export.js +81 -0
- package/dist/session/fork.d.ts +51 -0
- package/dist/session/fork.js +63 -0
- package/dist/session/format.d.ts +21 -0
- package/dist/session/format.js +49 -0
- package/dist/session/indexStore.d.ts +44 -0
- package/dist/session/indexStore.js +265 -0
- package/dist/session/log.d.ts +79 -0
- package/dist/session/log.js +157 -0
- package/dist/session/store.d.ts +9 -0
- package/dist/session/store.js +37 -0
- package/dist/telemetry/metrics.d.ts +25 -0
- package/dist/telemetry/metrics.js +26 -0
- package/package.json +17 -1
- package/src/seam.ts +21 -0
- package/.gitattributes +0 -11
- package/.github/workflows/ci.yml +0 -293
- package/.github/workflows/release.yml +0 -54
- package/adr/ADR-001.md +0 -4
- package/adr/ADR-002.md +0 -5
- package/adr/ADR-003.md +0 -35
- package/adr/host-onboarding.md +0 -68
- package/adr/mlps-audit-checklist.md +0 -41
- package/adr/win32-spike.md +0 -45
- package/docs/plugin-authoring.md +0 -82
- package/native/landlock-run/Makefile +0 -19
- package/native/landlock-run/landlock-run.c +0 -162
- package/scripts/contract-check.ts +0 -56
- package/scripts/invariant-n1.ts +0 -96
- package/scripts/package-check.ts +0 -29
- package/scripts/ptc-baseline/run-baseline.ts +0 -118
- package/scripts/release-artifacts.ts +0 -111
- package/scripts/release-pipeline.ts +0 -245
- package/scripts/secrets-baseline.ts +0 -33
- package/scripts/secrets-scan.ts +0 -56
- package/scripts/win32-ffi-debug.ts +0 -143
- package/scripts/win32-koffi-spike.ts +0 -537
- package/test/contract/eventmap.golden.json +0 -7
- package/test/fixtures/hello-plugin.ts +0 -5
- package/test/kernel.spec.ts +0 -136
- package/test/registry-e2e/run-e2e.ts +0 -213
- package/test/s11.spec.ts +0 -129
- package/test/s12.spec.ts +0 -135
- package/test/s13.spec.ts +0 -117
- package/test/s14.spec.ts +0 -117
- package/test/s15.spec.ts +0 -80
- package/test/s16.spec.ts +0 -133
- package/test/s17.spec.ts +0 -102
- package/test/s18.spec.ts +0 -81
- package/test/s19.spec.ts +0 -138
- package/test/s2.spec.ts +0 -245
- package/test/s20.spec.ts +0 -241
- package/test/s21.spec.ts +0 -231
- package/test/s22-m7-store-format.spec.ts +0 -135
- package/test/s23-m7-export-index.spec.ts +0 -189
- package/test/s24-signature-gate.spec.ts +0 -185
- package/test/s25-m8-runtime-core.spec.ts +0 -600
- package/test/s26-signature-closeout.spec.ts +0 -349
- package/test/s27-host-load-wiring.spec.ts +0 -134
- package/test/s28-session-index-followup.spec.ts +0 -236
- package/test/s29-otel-producer.spec.ts +0 -252
- package/test/s3.spec.ts +0 -179
- package/test/s30-release-artifacts.spec.ts +0 -127
- package/test/s31-real-model-wiring.spec.ts +0 -329
- package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
- package/test/s33-anthropic-adapter.spec.ts +0 -354
- package/test/s34-mcp-remote-transport.spec.ts +0 -250
- package/test/s35-config-ts-zod.spec.ts +0 -145
- package/test/s36-host-tool-surface.spec.ts +0 -250
- package/test/s37-projection-dedupe.spec.ts +0 -161
- package/test/s38-config-jiti.spec.ts +0 -116
- package/test/s39-otlp-span-semantics.spec.ts +0 -100
- package/test/s4.spec.ts +0 -169
- package/test/s40-metrics-cumulative.spec.ts +0 -67
- package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
- package/test/s42-traceparent-propagation.spec.ts +0 -415
- package/test/s43-remote-http-traceparent.spec.ts +0 -262
- package/test/s5.spec.ts +0 -95
- package/test/s6.spec.ts +0 -169
- package/test/s7.spec.ts +0 -136
- package/test/s7b-baseline.spec.ts +0 -20
- package/test/s9.spec.ts +0 -89
- package/test/sandbox-escape/j17-orchestrate.sh +0 -30
- package/test/sandbox-escape/run-matrix.sh +0 -98
- package/tsconfig.json +0 -18
package/test/s13.spec.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { resolvePackage, isWhitelisted, type RegistryConfig } from '../src/load/registry.ts'
|
|
4
|
-
import { HOST_MAPPINGS } from '../src/host/mappings.ts'
|
|
5
|
-
import { HostGateway } from '../src/host/hostGateway.ts'
|
|
6
|
-
import { createRuntimeFacade } from '../src/host/facade.ts'
|
|
7
|
-
import { deriveSessionId } from '../src/host/mappings.ts'
|
|
8
|
-
|
|
9
|
-
// ==================== registry resolution(T-3/T-4 fail-closed 组合) ====================
|
|
10
|
-
|
|
11
|
-
const enterprise: RegistryConfig = {
|
|
12
|
-
registries: [
|
|
13
|
-
{ url: 'https://verdaccio.corp.local', priority: 1, signed: false },
|
|
14
|
-
{ url: 'https://artifactory.corp.local/npm', priority: 2, signed: true },
|
|
15
|
-
],
|
|
16
|
-
allowNpmFallback: false,
|
|
17
|
-
offline: false,
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
test('registry: resolution 四级顺序——② 白名单 priority 升序', () => {
|
|
21
|
-
const r = resolvePackage({ name: 'pkg', version: '1.0.0' }, enterprise)
|
|
22
|
-
assert.equal(r.source, 'registry')
|
|
23
|
-
assert.equal(r.url, 'https://verdaccio.corp.local')
|
|
24
|
-
assert.match(r.decisionId, /^RD-[0-9a-f]{16}$/)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('registry: ① 显式源绑定命中 → 直连;未命中 → 硬失败不降级', () => {
|
|
28
|
-
const hit = resolvePackage({ name: 'pkg', version: '1.0.0', pinnedRegistry: 'https://artifactory.corp.local/npm' }, enterprise)
|
|
29
|
-
assert.equal(hit.source, 'registry')
|
|
30
|
-
assert.equal(hit.url, 'https://artifactory.corp.local/npm')
|
|
31
|
-
const miss = resolvePackage({ name: 'pkg', version: '1.0.0', pinnedRegistry: 'https://evil.example.com' }, enterprise)
|
|
32
|
-
assert.equal(miss.source, 'rejected')
|
|
33
|
-
assert.match(miss.detail, /硬失败不换源/)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('registry: ③ 兜底可关——企业默认拒绝;④ 离线最高优先级', () => {
|
|
37
|
-
const empty: RegistryConfig = { registries: [], allowNpmFallback: false, offline: false }
|
|
38
|
-
const r = resolvePackage({ name: 'pkg', version: '1.0.0' }, empty)
|
|
39
|
-
assert.equal(r.source, 'rejected')
|
|
40
|
-
assert.match(r.detail, /白名单为空且兜底关闭/)
|
|
41
|
-
const open: RegistryConfig = { registries: [], allowNpmFallback: true, offline: false }
|
|
42
|
-
assert.equal(resolvePackage({ name: 'pkg', version: '1.0.0' }, open).source, 'npm-fallback')
|
|
43
|
-
const offline = resolvePackage({ name: 'pkg', version: '1.0.0', pinnedRegistry: 'https://artifactory.corp.local/npm' }, { ...enterprise, offline: true })
|
|
44
|
-
assert.equal(offline.source, 'rejected', '离线覆盖一切(含显式绑定)')
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test('registry: 审计回调全量落盘(含拒绝决策)+ isWhitelisted', () => {
|
|
48
|
-
const audited: string[] = []
|
|
49
|
-
resolvePackage({ name: 'a', version: '1.0.0' }, enterprise, d => audited.push(d.kind))
|
|
50
|
-
resolvePackage({ name: 'b', version: '1.0.0', pinnedRegistry: 'https://evil' }, enterprise, d => audited.push(d.kind))
|
|
51
|
-
assert.equal(audited.length, 2)
|
|
52
|
-
assert.ok(auditsAllRegistry(audited))
|
|
53
|
-
function auditsAllRegistry(kinds: string[]) { return kinds.every(k => k === 'registry-resolution') }
|
|
54
|
-
assert.equal(isWhitelisted('https://verdaccio.corp.local', enterprise), true)
|
|
55
|
-
assert.equal(isWhitelisted('https://evil.example.com', enterprise), false)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
// ==================== 双宿主契约测试:五类语义扩全(S11 v0 → S13 v1) ====================
|
|
59
|
-
|
|
60
|
-
function setup() {
|
|
61
|
-
const profiles = new Map(HOST_MAPPINGS.map(h => [h.hostId, h]))
|
|
62
|
-
const facade = createRuntimeFacade({ profiles })
|
|
63
|
-
const gw = new HostGateway({ facade, audit: () => {} })
|
|
64
|
-
for (const h of HOST_MAPPINGS) gw.registerHost({ hostId: h.hostId, profile: h, transport: {} as never })
|
|
65
|
-
return gw
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** 五类语义 × 双宿主 fixture(契约测试数据源——host-mappings-v1 的行为化规约) */
|
|
69
|
-
const FIVE_CLASSES = [
|
|
70
|
-
{ semantic: 'user-input', cc: ['user_message', { content: 'hi' }], cx: ['input_item', { text: 'hi' }] },
|
|
71
|
-
{ semantic: 'assistant-output', cc: ['assistant_message', { content: 'done' }], cx: ['agent_message', { message: 'done' }] },
|
|
72
|
-
{ semantic: 'tool-call', cc: ['tool_use', { tool_name: 'fs', tool_call_id: 'c1' }], cx: ['function_call', { name: 'fs', call_id: 'c1' }] },
|
|
73
|
-
{ semantic: 'tool-result', cc: ['tool_result', { tool_call_id: 'c1', status: 'ok' }], cx: ['function_call_output', { call_id: 'c1', status: 'ok' }] },
|
|
74
|
-
{ semantic: 'turn-end', cc: ['turn_complete', { reason: 'completed' }], cx: ['task_complete', { reason: 'completed' }] },
|
|
75
|
-
] as const
|
|
76
|
-
|
|
77
|
-
test('契约测试 v1: 五类语义双宿主全链路投影等价(经 facade 落哈希链)', async () => {
|
|
78
|
-
const gw = setup()
|
|
79
|
-
const ccId = ((await gw.handle('claude-code', 'session_start', { hostSessionId: 'fx' })).result as { sessionId: string }).sessionId
|
|
80
|
-
const cxId = ((await gw.handle('codex', 'session_start', { hostSessionId: 'fx' })).result as { sessionId: string }).sessionId
|
|
81
|
-
await gw.handle('claude-code', 'session_turn', { sessionId: ccId, input: { events: FIVE_CLASSES.map(f => ({ hostEvent: f.cc[0], payload: f.cc[1] })) } })
|
|
82
|
-
await gw.handle('codex', 'session_turn', { sessionId: cxId, input: { events: FIVE_CLASSES.map(f => ({ hostEvent: f.cx[0], payload: f.cx[1] })) } })
|
|
83
|
-
const r1 = await gw.handle('claude-code', 'session_replay', { sessionId: ccId })
|
|
84
|
-
const r2 = await gw.handle('codex', 'session_replay', { sessionId: cxId })
|
|
85
|
-
const m1 = (r1.result as { messages: unknown[] }).messages
|
|
86
|
-
const m2 = (r2.result as { messages: unknown[] }).messages
|
|
87
|
-
assert.deepEqual(m1, m2, '五类语义投影逐字节一致')
|
|
88
|
-
assert.equal(m1.length, 4, 'user/assistant/toolCall(assistant)/toolResult 入投影;turnEnd 不入消息流')
|
|
89
|
-
// 两侧哈希链独立完整
|
|
90
|
-
const v1 = await gw.handle('claude-code', 'session_verify', { sessionId: ccId })
|
|
91
|
-
const v2 = await gw.handle('codex', 'session_verify', { sessionId: cxId })
|
|
92
|
-
assert.equal((v1.result as { ok: boolean }).ok, true)
|
|
93
|
-
assert.equal((v2.result as { ok: boolean }).ok, true)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
test('契约测试 v1: 降级同步——未知宿主事件在两侧同构落 hostRaw', async () => {
|
|
97
|
-
const gw = setup()
|
|
98
|
-
const ccId = ((await gw.handle('claude-code', 'session_start', { hostSessionId: 'dg' })).result as { sessionId: string }).sessionId
|
|
99
|
-
const cxId = ((await gw.handle('codex', 'session_start', { hostSessionId: 'dg' })).result as { sessionId: string }).sessionId
|
|
100
|
-
await gw.handle('claude-code', 'session_turn', { sessionId: ccId, input: { events: [{ hostEvent: 'future_thing', payload: { p: 1 } }] } })
|
|
101
|
-
await gw.handle('codex', 'session_turn', { sessionId: cxId, input: { events: [{ hostEvent: 'future_thing', payload: { p: 1 } }] } })
|
|
102
|
-
const e1 = await gw.handle('claude-code', 'session_export', { sessionId: ccId })
|
|
103
|
-
const e2 = await gw.handle('codex', 'session_export', { sessionId: cxId })
|
|
104
|
-
for (const e of [e1, e2]) {
|
|
105
|
-
const jsonl = (e.result as { bundle: { files: { name: string; content: string }[] } }).bundle.files[0].content
|
|
106
|
-
assert.ok(jsonl.includes('"hostRaw"'), 'hostRaw 载体')
|
|
107
|
-
assert.ok(jsonl.includes('future_thing'), '原事件名留痕')
|
|
108
|
-
}
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
test('契约测试 v1: 会话隔离与确定性派生(同 hostSessionId 跨宿主独立)', async () => {
|
|
112
|
-
assert.notEqual(deriveSessionId('claude-code', 's'), deriveSessionId('codex', 's'))
|
|
113
|
-
const gw = setup()
|
|
114
|
-
const a = await gw.handle('claude-code', 'session_start', { hostSessionId: 'dup' })
|
|
115
|
-
const b = await gw.handle('claude-code', 'session_start', { hostSessionId: 'dup' })
|
|
116
|
-
assert.equal((a.result as { sessionId: string }).sessionId, (b.result as { sessionId: string }).sessionId, '同宿主幂等')
|
|
117
|
-
})
|
package/test/s14.spec.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { convergeBudget, PTC_BUDGET_BASELINE } from '../src/ptc/budget.ts'
|
|
4
|
-
import { checkErasableOnly } from '../src/ptc/erasable.ts'
|
|
5
|
-
import { runCode, makePtcToolDefinition, type ToolBridge } from '../src/ptc/runCode.ts'
|
|
6
|
-
import { runTurn, type ModelStep, type ToolDef } from '../src/loop/stop.ts'
|
|
7
|
-
import { SessionLog } from '../src/session/log.ts'
|
|
8
|
-
|
|
9
|
-
// ==================== 预算收敛(只许下调) ====================
|
|
10
|
-
|
|
11
|
-
test('PTC 预算: 基线对齐 dsh 一手口径(60s/600s/64MB)', () => {
|
|
12
|
-
assert.deepEqual(PTC_BUDGET_BASELINE, { computeMs: 60_000, maxWallMs: 600_000, maxOutputBytes: 64 * 1024 * 1024 })
|
|
13
|
-
const b = convergeBudget()
|
|
14
|
-
assert.deepEqual(b, { computeMs: 60_000, maxWallMs: 600_000, maxOutputBytes: 64 * 1024 * 1024 })
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('PTC 预算: 下调通过;上调 = CAR-E-BUDGET 显式报错(fail-closed)', () => {
|
|
18
|
-
const b = convergeBudget({ computeMs: 30_000, maxWallMs: 1000, maxOutputBytes: 1024 })
|
|
19
|
-
assert.deepEqual(b, { computeMs: 30_000, maxWallMs: 1000, maxOutputBytes: 1024 })
|
|
20
|
-
assert.throws(() => convergeBudget({ maxWallMs: 700_000 }), /只许下调/)
|
|
21
|
-
assert.throws(() => convergeBudget({ maxOutputBytes: 65 * 1024 * 1024 }), /CAR-E-BUDGET/)
|
|
22
|
-
assert.throws(() => convergeBudget({ computeMs: -1 }), /positive/)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
// ==================== erasable-only 检查 ====================
|
|
26
|
-
|
|
27
|
-
test('erasable: enum/namespace/参数属性/装饰器/import=require 全拒绝', () => {
|
|
28
|
-
const cases: Array<[string, string]> = [
|
|
29
|
-
['enum 声明', 'enum Color { Red }\nreturn 1'],
|
|
30
|
-
['namespace 声明', 'namespace Util {\n export const x = 1\n}\nreturn 1'],
|
|
31
|
-
['构造器参数属性', 'class A { constructor(private x: number) {} }\nreturn 1'],
|
|
32
|
-
['import=require', "import fs = require('node:fs')\nreturn 1"],
|
|
33
|
-
['装饰器', 'const y = Object()\n@Component()\nclass B {}\nreturn 1'],
|
|
34
|
-
]
|
|
35
|
-
for (const [name, code] of cases) {
|
|
36
|
-
const r = checkErasableOnly(code)
|
|
37
|
-
assert.equal(r.ok, false, `${name} 应被拒绝`)
|
|
38
|
-
assert.match(r.violation!, /non-erasable/)
|
|
39
|
-
}
|
|
40
|
-
// 普通 erasable async 函数体通过
|
|
41
|
-
assert.equal(checkErasableOnly('const x: number = 1\nawait Promise.resolve()\nreturn x + 1').ok, true)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
// ==================== runCode:worker 隔离 + 工具桥 + 预算 ====================
|
|
45
|
-
|
|
46
|
-
function tools(): Map<string, ToolBridge> {
|
|
47
|
-
return new Map([
|
|
48
|
-
['add', { run: async (a: unknown) => (a as { x: number; y: number }).x + (a as { x: number; y: number }).y }],
|
|
49
|
-
])
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
test('runCode: code+description 双必填(description = 授权门人审凭据)', async () => {
|
|
53
|
-
const t = tools()
|
|
54
|
-
await assert.rejects(() => runCode({ code: ' ', description: 'd', toolCallId: 'p1', budget: { maxWallMs: 1000 } }, { tools: t }), /code is required/)
|
|
55
|
-
await assert.rejects(() => runCode({ code: 'return 1', description: '', toolCallId: 'p1', budget: { maxWallMs: 5000 } }, { tools: t }), /description is required/)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test('runCode: worker 执行程序体 return 值回传(每次新 worker)', async () => {
|
|
59
|
-
const r = await runCode({ code: 'const a = await tools.add({ x: 2, y: 3 })\nreturn { sum: a }', description: '两数相加验证工具桥', toolCallId: 'p2', budget: { maxWallMs: 10_000 } }, { tools: tools() })
|
|
60
|
-
assert.equal(r.ok, true, r.error)
|
|
61
|
-
assert.deepEqual(r.result, { sum: 5 })
|
|
62
|
-
assert.ok(r.wallMs < 10_000)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
test('runCode: 未知工具调用 = 显式错误(不静默)', async () => {
|
|
66
|
-
const r = await runCode({ code: 'await tools.nope({})\nreturn 1', description: '未知工具调用路径验证', toolCallId: 'p3', budget: { maxWallMs: 5000 } }, { tools: tools() })
|
|
67
|
-
assert.equal(r.ok, false)
|
|
68
|
-
assert.match(r.error!, /unknown tool/)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('runCode: maxWallMs 超限 = budget-exceeded 工具错误结果(不掐 turn 不抛异常)', async () => {
|
|
72
|
-
const r = await runCode({ code: 'while (true) { await new Promise(r => setTimeout(r, 10)) }\nreturn 1', description: '死循环预算护栏验证', toolCallId: 'p4', budget: { maxWallMs: 300 } }, { tools: tools() })
|
|
73
|
-
assert.equal(r.ok, false)
|
|
74
|
-
assert.equal(r.budgetExceeded, true)
|
|
75
|
-
assert.match(r.error!, /budget-exceeded \(maxWallMs=300\)/)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
test('runCode: erasable 违规在入口拒绝(不产生 worker)', async () => {
|
|
79
|
-
await assert.rejects(() => runCode({ code: 'enum E { A }\nreturn 1', description: 'erasable 挂点验证', toolCallId: 'p5', budget: { maxWallMs: 1000 } }, { tools: tools() }), /CAR-E-PTC.*non-erasable/)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
// ==================== 停止语义衔接(ADR-001:预算到期不掐 turn) ====================
|
|
83
|
-
|
|
84
|
-
test('PTC × runTurn: 一等 ToolDefinition 强制 write;程序错误结果交回模型后 turn 正常收口', async () => {
|
|
85
|
-
const log = new SessionLog()
|
|
86
|
-
const ptc = makePtcToolDefinition({ tools: tools() })
|
|
87
|
-
assert.equal(ptc.declaredSideEffect, 'write') // containment 非安全边界 → 最高约束
|
|
88
|
-
const all = new Map<string, ToolDef>([['run_code', ptc as unknown as ToolDef]])
|
|
89
|
-
let n = 0
|
|
90
|
-
const r = await runTurn({
|
|
91
|
-
log, turnId: 'PTC', tools: all, preset: { mode: 'full' },
|
|
92
|
-
model: async (): Promise<ModelStep> => {
|
|
93
|
-
if (n++ === 0) return { stopReason: 'toolUse', toolCalls: [{ id: 'pc1', tool: 'run_code', args: { code: 'return await tools.add({ x: 20, y: 22 })', description: '求和演示' } }] }
|
|
94
|
-
return { stopReason: 'stop', text: 'sum is 42' }
|
|
95
|
-
},
|
|
96
|
-
})
|
|
97
|
-
assert.equal(r.reason, 'completed') // 预算语义外:正常收口
|
|
98
|
-
const call = log.events.find(e => e.kind === 'toolCall')
|
|
99
|
-
assert.ok(call, 'PTC 调用落哈希链(与其他工具同链路,无旁路)')
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test('PTC × runTurn: 程序体抛错 = 工具错误结果交回模型(turn 不 interrupted/aborted)', async () => {
|
|
103
|
-
const log = new SessionLog()
|
|
104
|
-
const ptc = makePtcToolDefinition({ tools: tools() })
|
|
105
|
-
const all = new Map<string, ToolDef>([['run_code', ptc as unknown as ToolDef]])
|
|
106
|
-
let n = 0
|
|
107
|
-
const r = await runTurn({
|
|
108
|
-
log, turnId: 'PTC2', tools: all, preset: { mode: 'full' },
|
|
109
|
-
model: async (): Promise<ModelStep> => {
|
|
110
|
-
if (n++ === 0) return { stopReason: 'toolUse', toolCalls: [{ id: 'pc2', tool: 'run_code', args: { code: 'throw new Error("boom")', description: '错误路径验证' } }] }
|
|
111
|
-
return { stopReason: 'stop', text: 'handled' }
|
|
112
|
-
},
|
|
113
|
-
})
|
|
114
|
-
assert.equal(r.reason, 'completed') // 错误结果交回模型继续,六值枚举不被 PTC 扩展
|
|
115
|
-
const res = log.events.find(e => e.kind === 'toolResult') as { payload: { error?: string } } | undefined
|
|
116
|
-
assert.ok(res, 'toolResult 落链')
|
|
117
|
-
})
|
package/test/s15.spec.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { renderSdk, renderSdkFromRegistry } from '../src/ptc/sdk.ts'
|
|
4
|
-
import { runCode, makePtcToolDefinition, type ToolBridge } from '../src/ptc/runCode.ts'
|
|
5
|
-
|
|
6
|
-
// ==================== TS SDK 渲染(声明面 = 授权面) ====================
|
|
7
|
-
|
|
8
|
-
test('SDK 渲染: 注册表快照 → system prompt TS 声明(非 .d.ts 文件)', () => {
|
|
9
|
-
const reg = new Map<string, ToolBridge & { description?: string; paramHint?: string }>([
|
|
10
|
-
['add', { run: async () => {}, description: '两数相加', paramHint: 'args: { x: number; y: number }' }],
|
|
11
|
-
['fs_read', { run: async () => {} }],
|
|
12
|
-
])
|
|
13
|
-
const sdk = renderSdkFromRegistry(reg)
|
|
14
|
-
assert.ok(sdk.includes('declare const tools: {'))
|
|
15
|
-
assert.ok(sdk.includes('"add": (args: { x: number; y: number }) => Promise<unknown> // 两数相加'))
|
|
16
|
-
assert.ok(sdk.includes('"fs_read": (args: unknown) => Promise<unknown>'), '无 paramHint 落 unknown 形状')
|
|
17
|
-
// 空注册表 = 空声明面(授权面同步为空)
|
|
18
|
-
assert.ok(renderSdkFromRegistry(new Map()).includes('declare const tools: {\n};'))
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
test('SDK 渲染: 声明面/授权面一致性——同一 Map 快照两处消费', () => {
|
|
22
|
-
// 红线的可执行化:渲染函数只接受注册表自身(无独立声明通道),结构上不可能漂移
|
|
23
|
-
const reg = new Map<string, ToolBridge>([['only', { run: async () => {} }]])
|
|
24
|
-
const sdk = renderSdkFromRegistry(reg as never)
|
|
25
|
-
assert.ok(sdk.includes('"only"'))
|
|
26
|
-
assert.ok(!sdk.includes('"hidden"'), '未注册工具不可能出现在声明面')
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
// ==================== 授权门集成(authorizationId 幂等) ====================
|
|
30
|
-
|
|
31
|
-
test('PTC 授权门: 拒绝 = 无 worker 启动 + ptc-denied 审计留痕', async () => {
|
|
32
|
-
const audits: Array<{ kind: string; authorizationId?: string }> = []
|
|
33
|
-
const r = await runCode(
|
|
34
|
-
{ code: 'return 1', description: '拒绝路径验证', toolCallId: 'tc-42', budget: { maxWallMs: 1000 } },
|
|
35
|
-
{ tools: new Map(), audit: d => audits.push(d as never), authorize: async () => false },
|
|
36
|
-
)
|
|
37
|
-
assert.equal(r.ok, false)
|
|
38
|
-
assert.match(r.error!, /authorization-denied/)
|
|
39
|
-
const denied = audits.find(a => a.kind === 'ptc-denied')
|
|
40
|
-
assert.ok(denied, '拒绝落审计')
|
|
41
|
-
assert.equal(denied!.authorizationId, 'ptc-tc-42', '幂等键 = ptc-+toolCallId')
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
test('PTC 授权门: 放行正常执行(authorize 回调看到 description 人审凭据)', async () => {
|
|
45
|
-
let seenDescription = ''
|
|
46
|
-
const r = await runCode(
|
|
47
|
-
{ code: 'return 7', description: '求和放行', toolCallId: 'tc-43', budget: { maxWallMs: 5000 } },
|
|
48
|
-
{ tools: new Map(), authorize: async req => { seenDescription = req.description; return true } },
|
|
49
|
-
)
|
|
50
|
-
assert.equal(r.ok, true)
|
|
51
|
-
assert.equal(r.result, 7)
|
|
52
|
-
assert.equal(seenDescription, '求和放行')
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
// ==================== secrets 出站覆盖(F12 × worker 输出) ====================
|
|
56
|
-
|
|
57
|
-
test('F12 × PTC: worker 输出含 API Key → 回填上下文前强制脱敏', async () => {
|
|
58
|
-
const audits: Array<{ kind: string; hits?: number }> = []
|
|
59
|
-
const r = await runCode(
|
|
60
|
-
{ code: 'return { note: "key is sk-proj-abcdefghij0123456789abcd keep it safe" }', description: '出站脱敏验证', toolCallId: 'tc-44', budget: { maxWallMs: 5000 } },
|
|
61
|
-
{ tools: new Map(), audit: d => audits.push(d as never) },
|
|
62
|
-
)
|
|
63
|
-
assert.equal(r.ok, true)
|
|
64
|
-
assert.equal(r.secretsRedacted, 1)
|
|
65
|
-
assert.ok(!JSON.stringify(r.result).includes('sk-proj-abcdefghij0123456789abcd'), '明文不可见于返回结果')
|
|
66
|
-
assert.ok(JSON.stringify(r.result).includes('****'), '遮蔽格式生效')
|
|
67
|
-
assert.ok(audits.some(a => a.kind === 'ptc-secrets-redacted'), '脱敏事件落审计')
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
// ==================== registry × HostGateway tool 面(S12 留白接线) ====================
|
|
71
|
-
|
|
72
|
-
test('registry × facade: tool_list/tool_call 接线(S12 留白补齐)', async () => {
|
|
73
|
-
// 直接验证 registry 判定与 facade tool 语义的分界已由 s13 覆盖;此处验证 makePtcToolDefinition 的
|
|
74
|
-
// audit 流与 registry decisionId 一样具备可落链结构(均为 {kind, ...payload} 纯数据)
|
|
75
|
-
const audits: Array<Record<string, unknown>> = []
|
|
76
|
-
const ptc = makePtcToolDefinition({ tools: new Map([['add', { run: async (a: unknown) => (a as { x: number }).x + 1 }]]), audit: d => audits.push(d) })
|
|
77
|
-
await ptc.run({ code: 'return await tools.add({ x: 41 })', description: '注册表快照执行' })
|
|
78
|
-
assert.ok(audits.every(a => typeof a.kind === 'string'), '审计事件结构化(可落哈希链)')
|
|
79
|
-
assert.equal(audits[0].kind, 'ptc-start')
|
|
80
|
-
})
|
package/test/s16.spec.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { jsonSchemaToTs } from '../src/ptc/schemaRender.ts'
|
|
4
|
-
import { renderSdkFromRegistry } from '../src/ptc/sdk.ts'
|
|
5
|
-
import { McpGateway, type ClientTransport, type JsonRpcRequest, type JsonRpcResponse } from '../src/mcp/gateway.ts'
|
|
6
|
-
import type { ToolBridge } from '../src/ptc/runCode.ts'
|
|
7
|
-
|
|
8
|
-
// ==================== jsonSchemaToTs:深度转换矩阵 ====================
|
|
9
|
-
|
|
10
|
-
test('schema→TS: 基础标量 + integer 归一 + 可空联合', () => {
|
|
11
|
-
assert.equal(jsonSchemaToTs({ type: 'string' }), 'string')
|
|
12
|
-
assert.equal(jsonSchemaToTs({ type: 'integer' }), 'number')
|
|
13
|
-
assert.equal(jsonSchemaToTs({ type: ['string', 'null'] }), 'string | null')
|
|
14
|
-
assert.equal(jsonSchemaToTs({ type: 'boolean' }), 'boolean')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('schema→TS: const/enum 字面量联合', () => {
|
|
18
|
-
assert.equal(jsonSchemaToTs({ const: 'fixed' }), '"fixed"')
|
|
19
|
-
assert.equal(jsonSchemaToTs({ enum: ['a', 'b', 3, true] }), '"a" | "b" | 3 | true')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('schema→TS: 嵌套 object(required 必填 / 缺省可选 ?)', () => {
|
|
23
|
-
const ts = jsonSchemaToTs({
|
|
24
|
-
type: 'object',
|
|
25
|
-
properties: {
|
|
26
|
-
path: { type: 'string' },
|
|
27
|
-
encoding: { type: 'string', enum: ['utf8', 'gbk'] },
|
|
28
|
-
depth: { type: 'integer' },
|
|
29
|
-
},
|
|
30
|
-
required: ['path'],
|
|
31
|
-
})
|
|
32
|
-
assert.equal(ts, '{ path: string; encoding?: "utf8" | "gbk"; depth?: number }')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
test('schema→TS: 数组 / 元组(prefixItems) / Record(additionalProperties)', () => {
|
|
36
|
-
assert.equal(jsonSchemaToTs({ type: 'array', items: { type: 'number' } }), 'number[]')
|
|
37
|
-
assert.equal(jsonSchemaToTs({ type: 'array', prefixItems: [{ type: 'string' }, { type: 'number' }] }), '[string, number]')
|
|
38
|
-
assert.equal(jsonSchemaToTs({ type: 'object', additionalProperties: { type: 'string' } }), '{ [k: string]: string }')
|
|
39
|
-
assert.equal(jsonSchemaToTs({ type: 'object' }), 'Record<string, unknown>')
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('schema→TS: 深层嵌套对象递归', () => {
|
|
43
|
-
const ts = jsonSchemaToTs({
|
|
44
|
-
type: 'object',
|
|
45
|
-
properties: {
|
|
46
|
-
filter: {
|
|
47
|
-
type: 'object',
|
|
48
|
-
properties: { tags: { type: 'array', items: { type: 'string' } } },
|
|
49
|
-
required: ['tags'],
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
required: ['filter'],
|
|
53
|
-
})
|
|
54
|
-
assert.equal(ts, '{ filter: { tags: string[] } }')
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test('schema→TS: 组合器 anyOf 联合 / allOf 交叉', () => {
|
|
58
|
-
assert.equal(jsonSchemaToTs({ anyOf: [{ type: 'string' }, { type: 'number' }] }), 'string | number')
|
|
59
|
-
assert.equal(jsonSchemaToTs({ allOf: [{ type: 'object', properties: { a: { type: 'string' } } }, { type: 'object', properties: { b: { type: 'number' } } }] }),
|
|
60
|
-
'{ a?: string } & { b?: number }')
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test('schema→TS: fail-visible——$ref/not/畸形/超深落 unknown 且带原因标记(零静默)', () => {
|
|
64
|
-
assert.equal(jsonSchemaToTs({ $ref: '#/$defs/x' }), 'unknown /* unsupported:$ref */')
|
|
65
|
-
assert.equal(jsonSchemaToTs({ not: { type: 'string' } }), 'unknown /* unsupported:not */')
|
|
66
|
-
assert.equal(jsonSchemaToTs(null), 'unknown /* malformed */')
|
|
67
|
-
assert.equal(jsonSchemaToTs('garbage'), 'unknown /* malformed */')
|
|
68
|
-
// 超深递归:MAX_DEPTH=6,第 8 层触发标记
|
|
69
|
-
let deep: Record<string, unknown> = { type: 'string' }
|
|
70
|
-
for (let i = 0; i < 8; i++) deep = { type: 'object', properties: { nest: deep }, required: ['nest'] }
|
|
71
|
-
const ts = jsonSchemaToTs(deep)
|
|
72
|
-
assert.ok(ts.includes('unknown /* depth>6 */'), '超深节点带标记: ' + ts.slice(-60))
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
// ==================== SDK 渲染集成(优先级 + 一致性红线不回退) ====================
|
|
76
|
-
|
|
77
|
-
test('SDK 渲染: inputSchema 深度转换生效;paramHint 手写声明优先;无声明落 unknown', () => {
|
|
78
|
-
const reg = new Map<string, ToolBridge & { description?: string; paramHint?: string; inputSchema?: unknown }>([
|
|
79
|
-
['fs_read', { run: async () => {}, inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } }],
|
|
80
|
-
['fs_write', { run: async () => {}, paramHint: 'args: { data: string }', inputSchema: { type: 'object' } }],
|
|
81
|
-
['legacy', { run: async () => {} }],
|
|
82
|
-
])
|
|
83
|
-
const sdk = renderSdkFromRegistry(reg)
|
|
84
|
-
assert.ok(sdk.includes('"fs_read": ({ path: string }) => Promise<unknown>'), 'inputSchema 深渲生效')
|
|
85
|
-
assert.ok(sdk.includes('"fs_write": (args: { data: string }) => Promise<unknown>'), 'paramHint 优先于 inputSchema')
|
|
86
|
-
assert.ok(sdk.includes('"legacy": (args: unknown) => Promise<unknown>'), '无声明落 unknown(fail-visible 默认不变)')
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
test('SDK 渲染: inputSchema 深渲中 fail-visible 标记直接进声明面(模型可见)', () => {
|
|
90
|
-
const reg = new Map<string, ToolBridge & { inputSchema?: unknown }>([
|
|
91
|
-
['ref_tool', { run: async () => {}, inputSchema: { type: 'object', properties: { cfg: { $ref: '#/$defs/Config' } } } }],
|
|
92
|
-
])
|
|
93
|
-
const sdk = renderSdkFromRegistry(reg)
|
|
94
|
-
assert.ok(sdk.includes('cfg?: unknown /* unsupported:$ref */'), '不可判定路径模型可见(不静默截断)')
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
// ==================== McpGateway inputSchema 捕获(DEC-4 消费源接线) ====================
|
|
98
|
-
|
|
99
|
-
function schemaTransport(): ClientTransport {
|
|
100
|
-
return {
|
|
101
|
-
async send(req: JsonRpcRequest): Promise<JsonRpcResponse> {
|
|
102
|
-
if (req.method === 'tools/list') {
|
|
103
|
-
return {
|
|
104
|
-
jsonrpc: '2.0', id: req.id,
|
|
105
|
-
result: { tools: [
|
|
106
|
-
{
|
|
107
|
-
name: 'ts_query', description: 'deep schema tool', sideEffect: 'readonly',
|
|
108
|
-
inputSchema: {
|
|
109
|
-
type: 'object',
|
|
110
|
-
properties: { q: { type: 'string' }, limit: { type: 'integer' }, tags: { type: 'array', items: { type: 'string' } } },
|
|
111
|
-
required: ['q'],
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
{ name: 'ts_plain' },
|
|
115
|
-
] },
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
throw new Error('unknown method ' + req.method)
|
|
119
|
-
},
|
|
120
|
-
alive() { return true },
|
|
121
|
-
close() {},
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
test('McpGateway: tools/list inputSchema 原样捕获进能力矩阵(不丢弃)', async () => {
|
|
126
|
-
const gw = new McpGateway()
|
|
127
|
-
await gw.register({ serverId: 'ts1', transport: schemaTransport() })
|
|
128
|
-
const matrix = gw.listTools()
|
|
129
|
-
const q = matrix.find(t => t.name === 'ts_query')!
|
|
130
|
-
assert.ok(q.inputSchema && typeof q.inputSchema === 'object', 'inputSchema 已捕获')
|
|
131
|
-
const plain = matrix.find(t => t.name === 'ts_plain')!
|
|
132
|
-
assert.equal(plain.inputSchema, undefined, '未声明 inputSchema 保持 undefined(渲染层落 unknown)')
|
|
133
|
-
})
|
package/test/s17.spec.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { resolvePackage, resolveCandidateChain, type RegistryConfig, type ExcludedSource } from '../src/load/registry.ts'
|
|
4
|
-
import { enforceSignature } from '../src/load/verifier.ts'
|
|
5
|
-
import { createCounters, assertZeroContent } from '../src/telemetry/metrics.ts'
|
|
6
|
-
|
|
7
|
-
const corp: RegistryConfig = {
|
|
8
|
-
registries: [
|
|
9
|
-
{ url: 'https://a.corp.local', priority: 1, signed: true },
|
|
10
|
-
{ url: 'https://b.corp.local', priority: 2, signed: false },
|
|
11
|
-
{ url: 'https://c.corp.local', priority: 3, signed: false },
|
|
12
|
-
],
|
|
13
|
-
allowNpmFallback: true,
|
|
14
|
-
offline: false,
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// ==================== T-5 定稿②:exclude 通道(类型上禁校验失败换源) ====================
|
|
18
|
-
|
|
19
|
-
test('registry exclude: 网络失败源被跳过,按 priority 继续下一源', () => {
|
|
20
|
-
const exclude: ExcludedSource[] = [{ url: 'https://a.corp.local', reason: 'network' }]
|
|
21
|
-
const r = resolvePackage({ name: 'pkg', version: '1.0.0' }, corp, undefined, { exclude })
|
|
22
|
-
assert.equal(r.source, 'registry')
|
|
23
|
-
assert.equal(r.url, 'https://b.corp.local')
|
|
24
|
-
assert.match(r.detail, /excluded=1/)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('registry exclude: 候选耗尽 → 兜底(T-5 定稿②修正语义:非「白名单为空」)', () => {
|
|
28
|
-
const exclude: ExcludedSource[] = corp.registries.map(r => ({ url: r.url, reason: 'network' as const }))
|
|
29
|
-
const r = resolvePackage({ name: 'pkg', version: '1.0.0' }, corp, undefined, { exclude })
|
|
30
|
-
assert.equal(r.source, 'npm-fallback')
|
|
31
|
-
assert.match(r.detail, /candidates exhausted/)
|
|
32
|
-
// 兜底关闭 → 候选耗尽 = fail-closed 拒绝
|
|
33
|
-
const closed = resolvePackage({ name: 'pkg', version: '1.0.0' }, { ...corp, allowNpmFallback: false }, undefined, { exclude })
|
|
34
|
-
assert.equal(closed.source, 'rejected')
|
|
35
|
-
assert.match(closed.detail, /候选耗尽且兜底关闭/)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
test('registry exclude: 显式绑定源网络失败 → 落审计后按 priority 继续(T-5 ②网络路径)', () => {
|
|
39
|
-
const audits: string[] = []
|
|
40
|
-
const r = resolvePackage(
|
|
41
|
-
{ name: 'pkg', version: '1.0.0', pinnedRegistry: 'https://a.corp.local' }, corp,
|
|
42
|
-
d => audits.push(String((d as { detail: string }).detail)),
|
|
43
|
-
{ exclude: [{ url: 'https://a.corp.local', reason: 'network' }] },
|
|
44
|
-
)
|
|
45
|
-
assert.equal(r.source, 'registry')
|
|
46
|
-
assert.equal(r.url, 'https://b.corp.local', 'pinned 网络失败 ≠ 信任否定,继续剩余白名单')
|
|
47
|
-
assert.equal(audits.length, 1)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test('registry exclude: 显式绑定不在白名单 = 硬失败不换源(既有语义保持)', () => {
|
|
51
|
-
const r = resolvePackage({ name: 'pkg', version: '1.0.0', pinnedRegistry: 'https://evil.example.com' }, corp)
|
|
52
|
-
assert.equal(r.source, 'rejected')
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
test('registry: resolveCandidateChain 只读预览(零审计副作用)', () => {
|
|
56
|
-
const audits: unknown[] = []
|
|
57
|
-
const c = resolveCandidateChain(corp)
|
|
58
|
-
assert.deepEqual(c.chain, ['https://a.corp.local', 'https://b.corp.local', 'https://c.corp.local'])
|
|
59
|
-
assert.equal(c.fallback, true)
|
|
60
|
-
assert.equal(audits.length, 0)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
// ==================== S17 采集面(零内容计数器) ====================
|
|
64
|
-
|
|
65
|
-
test('telemetry: 3 Counter 零内容——snapshot 仅枚举 labels,无内容字段', () => {
|
|
66
|
-
const c = createCounters()
|
|
67
|
-
c.onCount('car_load_total', { result: 'ok' })
|
|
68
|
-
c.onCount('car_load_total', { result: 'ok' })
|
|
69
|
-
c.onCount('car_load_total', { result: 'failed' })
|
|
70
|
-
c.onCount('car_unsigned_confirmed', { confirmed: 'no' })
|
|
71
|
-
c.onCount('car_registry_decision', { source: 'registry' })
|
|
72
|
-
const snap = c.snapshot()
|
|
73
|
-
assert.deepEqual(snap, {
|
|
74
|
-
'car_load_total|result=ok': 2,
|
|
75
|
-
'car_load_total|result=failed': 1,
|
|
76
|
-
'car_unsigned_confirmed|confirmed=no': 1,
|
|
77
|
-
'car_registry_decision|source=registry': 1,
|
|
78
|
-
})
|
|
79
|
-
assert.equal(assertZeroContent(snap), true, '零内容红线(无 sk-/路径/长串)')
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
test('telemetry: Q-08 兼容——未接遥测时仅进程内累计(无出站面)', () => {
|
|
83
|
-
const c = createCounters()
|
|
84
|
-
c.onCount('car_registry_decision', { source: 'npm-fallback' })
|
|
85
|
-
// snapshot 即登记表人工填报兜底通道数据源;本层无任何出站调用面
|
|
86
|
-
assert.ok(Object.keys(c.snapshot()).length === 1)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
// ==================== verifier × 采集面(unsigned warn 计数) ====================
|
|
90
|
-
|
|
91
|
-
test('verifier onCount: unsigned warn 路径计数(confirmed=no);enforce 路径不产生 unsigned 计数', () => {
|
|
92
|
-
const c = createCounters()
|
|
93
|
-
const deps = { mode: 'warn' as const, trustRootPublicKey: 'x' }
|
|
94
|
-
const r = enforceSignature('hash-abc', undefined, deps, (n, l) => c.onCount(n, l))
|
|
95
|
-
assert.equal(r.allowed, true)
|
|
96
|
-
assert.ok(r.warning)
|
|
97
|
-
assert.equal(c.snapshot()['car_unsigned_confirmed|confirmed=no'], 1)
|
|
98
|
-
// enforce 模式:缺失 = 拒绝(无 unsigned 计数——不是放行路径)
|
|
99
|
-
const r2 = enforceSignature('hash-abc', undefined, { ...deps, mode: 'enforce' }, (n, l) => c.onCount(n, l))
|
|
100
|
-
assert.equal(r2.allowed, false)
|
|
101
|
-
assert.equal(c.snapshot()['car_unsigned_confirmed|confirmed=no'], 1, '计数不增(拒绝路径非放行路径)')
|
|
102
|
-
})
|
package/test/s18.spec.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { spawn } from 'node:child_process'
|
|
4
|
-
import { join, dirname } from 'node:path'
|
|
5
|
-
import { fileURLToPath } from 'node:url'
|
|
6
|
-
|
|
7
|
-
const CLI = join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'cli.ts')
|
|
8
|
-
|
|
9
|
-
/** 真实子进程 stdio 对话(E-6 预演:非 PassThrough 模拟,是 spawn 进程级验证) */
|
|
10
|
-
function talk(host: string, requests: string[]): Promise<{ outs: any[]; stderr: string }> {
|
|
11
|
-
return new Promise((resolve, reject) => {
|
|
12
|
-
const child = spawn(process.execPath, ['--experimental-transform-types', CLI, 'mcp-serve', '--host', host], { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
13
|
-
let out = ''
|
|
14
|
-
let err = ''
|
|
15
|
-
child.stdout.on('data', d => { out += d })
|
|
16
|
-
child.stderr.on('data', d => { err += d })
|
|
17
|
-
child.on('error', reject)
|
|
18
|
-
child.on('exit', () => {
|
|
19
|
-
try { resolve({ outs: out.split('\n').filter(Boolean).map(l => JSON.parse(l)), stderr: err }) } catch (e) { reject(e) }
|
|
20
|
-
})
|
|
21
|
-
for (const r of requests) child.stdin.write(r + '\n')
|
|
22
|
-
child.stdin.end()
|
|
23
|
-
})
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
test('S18: mcp-serve 真实进程——tools/list + session_start + turn + verify 全链路', async () => {
|
|
27
|
-
const { outs } = await talk('claude-code', [
|
|
28
|
-
JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }),
|
|
29
|
-
JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'session_start', arguments: { hostSessionId: 'cc-e2e' } } }),
|
|
30
|
-
])
|
|
31
|
-
assert.equal(outs[0].result.tools.length, 10)
|
|
32
|
-
const sid = JSON.parse(outs[1].result.content[0].text).sessionId
|
|
33
|
-
assert.match(sid, /^SH-[0-9a-f]{24}$/)
|
|
34
|
-
|
|
35
|
-
// 同一进程第二轮(另起进程验证幂等派生一致性)
|
|
36
|
-
const { outs: outs2 } = await talk('claude-code', [
|
|
37
|
-
JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'session_start', arguments: { hostSessionId: 'cc-e2e' } } }),
|
|
38
|
-
])
|
|
39
|
-
assert.equal(JSON.parse(outs2[0].result.content[0].text).sessionId, sid, '跨进程确定性派生(幂等)')
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('S18: 采集窗口——会话结束 stderr 输出零内容快照(登记表兜底通道实化)', async () => {
|
|
43
|
-
const { stderr } = await talk('claude-code', [
|
|
44
|
-
JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'session_start', arguments: { hostSessionId: 's' } } }),
|
|
45
|
-
JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'session_turn', arguments: { sessionId: 'SH-nonexistent', input: { events: [] } } } }),
|
|
46
|
-
])
|
|
47
|
-
assert.match(stderr, /\[car mcp-serve\].*snapshot=/)
|
|
48
|
-
assert.match(stderr, /zeroContent=true/, '零内容红线自检通过')
|
|
49
|
-
const snap = JSON.parse(/snapshot=(\{.*?\}) zeroContent/.exec(stderr)![1])
|
|
50
|
-
assert.ok('car_registry_decision|source=registry' in snap)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test('S18+: 1.1-S4 边界——未指定 --plugin 时装载计数不出现(装载接线不改变缺省协议面与快照键集)', async () => {
|
|
54
|
-
const { stderr } = await talk('claude-code', [
|
|
55
|
-
JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'session_start', arguments: { hostSessionId: 's' } } }),
|
|
56
|
-
])
|
|
57
|
-
const snap = JSON.parse(/snapshot=(\{.*?\}) zeroContent/.exec(stderr)![1])
|
|
58
|
-
assert.ok(!('car_load_total|result=ok' in snap), '无 --plugin = 无装载计数(1.1 装载接线为显式 opt-in)')
|
|
59
|
-
assert.ok(!('car_unsigned_confirmed|confirmed=no' in snap))
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test('S18: codex 宿主接入(同一入口不同 hostId)', async () => {
|
|
63
|
-
const { outs } = await talk('codex', [
|
|
64
|
-
JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'session_start', arguments: { hostSessionId: 'cx-e2e' } } }),
|
|
65
|
-
])
|
|
66
|
-
const sid = JSON.parse(outs[0].result.content[0].text).sessionId
|
|
67
|
-
assert.match(sid, /^SH-[0-9a-f]{24}$/)
|
|
68
|
-
const cc = await talk('claude-code', [
|
|
69
|
-
JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'session_start', arguments: { hostSessionId: 'cx-e2e' } } }),
|
|
70
|
-
])
|
|
71
|
-
assert.notEqual(JSON.parse(cc.outs[0].result.content[0].text).sessionId, sid, '跨宿主隔离经真实进程验证')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test('S18: 未知 hostId 显式拒绝(exit 2)', async () => {
|
|
75
|
-
const code = await new Promise<number>(resolve => {
|
|
76
|
-
const child = spawn(process.execPath, ['--experimental-transform-types', CLI, 'mcp-serve', '--host', 'unknown-host'])
|
|
77
|
-
child.on('exit', c => resolve(c ?? -1))
|
|
78
|
-
child.stdin.end()
|
|
79
|
-
})
|
|
80
|
-
assert.equal(code, 2)
|
|
81
|
-
})
|