@danceiny/gotry 0.0.1-rc.20 → 0.0.1-rc.21
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 +14 -3
- package/README.zh-CN.md +15 -4
- package/bin/gotry-bootstrap.js +53 -8
- package/cordis.gotry-patch.yml +12 -5
- package/dist/capabilities/doctor.js +88 -16
- package/dist/capabilities/session/health-watch.js +10 -3
- package/dist/scripts/benchmark-environment-bridge-e2e.js +53 -15
- package/dist/scripts/benchmark-environment-bridge-tests.js +450 -28
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +211 -2
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +132 -2
- package/dist/scripts/bootstrap-tests.js +2 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +9 -1
- package/dist/scripts/map-tools-vendor-package-proof.js +276 -0
- package/dist/scripts/persona-surface-guard-tests.js +8 -3
- package/dist/src/benchmark-agent-conformance.js +155 -5
- package/dist/src/benchmark-environment-bridge.js +53 -71
- package/dist/src/booking-surface/dsh-planner.js +24 -8
- package/extension/manifest.json +2 -2
- package/package.json +2 -1
- package/ts/capabilities/session/health-watch.ts +9 -2
- package/ts/package.json +15 -7
- package/ts/scripts/map-tools-vendor-package-proof.ts +268 -0
- package/ts/src/benchmark-agent-conformance.ts +135 -4
- package/ts/src/benchmark-environment-bridge.ts +25 -40
- package/ts/src/booking-surface/dsh-planner.ts +25 -13
|
@@ -384,9 +384,11 @@ function recoverFinalResponseDecision(response, task) {
|
|
|
384
384
|
}).slice(0, 600));
|
|
385
385
|
return null;
|
|
386
386
|
}
|
|
387
|
-
repairPlannerFactRefs(action);
|
|
387
|
+
const repairedRefs = repairPlannerFactRefs(action);
|
|
388
|
+
const repairedValidation = validateBookingReadAction(action);
|
|
389
|
+
if (!repairedValidation.ok) return null;
|
|
388
390
|
try {
|
|
389
|
-
assertPlannerSafeRefs(action);
|
|
391
|
+
assertPlannerSafeRefs(action, repairedRefs);
|
|
390
392
|
} catch (error) {
|
|
391
393
|
console.error('[booking-copilot] finalResponse recovery rejected (unsafe ref):', JSON.stringify({
|
|
392
394
|
actionId: action.actionId,
|
|
@@ -411,10 +413,17 @@ function recoverFinalResponseDecision(response, task) {
|
|
|
411
413
|
const PLANNER_SAFE_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9:._-]*$/;
|
|
412
414
|
function repairPlannerFactRefs(action) {
|
|
413
415
|
const factRefs = action.factRefs;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
+
const repairedRefs = new Set();
|
|
417
|
+
if (!Array.isArray(factRefs)) return repairedRefs;
|
|
418
|
+
action.factRefs = factRefs.map((ref)=>{
|
|
419
|
+
if (typeof ref !== 'string' || PLANNER_SAFE_REF_PATTERN.test(ref)) return ref;
|
|
420
|
+
const alias = `modelref:${createHash('sha256').update(ref, 'utf8').digest('hex')}`;
|
|
421
|
+
repairedRefs.add(alias);
|
|
422
|
+
return alias;
|
|
423
|
+
});
|
|
424
|
+
return repairedRefs;
|
|
416
425
|
}
|
|
417
|
-
function assertPlannerSafeRefs(action) {
|
|
426
|
+
function assertPlannerSafeRefs(action, repairedRefs) {
|
|
418
427
|
const actionId = action.actionId;
|
|
419
428
|
if (typeof actionId !== 'string' || !PLANNER_SAFE_REF_PATTERN.test(actionId)) {
|
|
420
429
|
throw new Error(`planner_invalid_action:unsafe_action_id:${String(actionId).slice(0, 60)}`);
|
|
@@ -422,7 +431,7 @@ function assertPlannerSafeRefs(action) {
|
|
|
422
431
|
const factRefs = action.factRefs;
|
|
423
432
|
if (Array.isArray(factRefs)) {
|
|
424
433
|
for (const ref of factRefs){
|
|
425
|
-
if (typeof ref !== 'string' || !PLANNER_SAFE_REF_PATTERN.test(ref)) {
|
|
434
|
+
if (typeof ref !== 'string' || !PLANNER_SAFE_REF_PATTERN.test(ref) || ref.startsWith('modelref:') && !repairedRefs.has(ref) || ref.length > 512) {
|
|
426
435
|
throw new Error(`planner_invalid_action:unsafe_fact_ref:${String(ref).slice(0, 60)}`);
|
|
427
436
|
}
|
|
428
437
|
}
|
|
@@ -483,8 +492,15 @@ function parseToolDecision(event, task) {
|
|
|
483
492
|
}).slice(0, 1200));
|
|
484
493
|
throw new Error(`planner_invalid_action:${validation.errors.join('; ')}`);
|
|
485
494
|
}
|
|
486
|
-
repairPlannerFactRefs(decision.action);
|
|
487
|
-
|
|
495
|
+
const repairedRefs = repairPlannerFactRefs(decision.action);
|
|
496
|
+
const repairedValidation = validateBookingReadAction(decision.action);
|
|
497
|
+
if (!repairedValidation.ok) {
|
|
498
|
+
console.error(`[booking-copilot] repaired action rejected:`, JSON.stringify({
|
|
499
|
+
errors: repairedValidation.errors.slice(0, 8)
|
|
500
|
+
}).slice(0, 800));
|
|
501
|
+
throw new Error(`planner_invalid_action:${repairedValidation.errors.join('; ')}`);
|
|
502
|
+
}
|
|
503
|
+
assertPlannerSafeRefs(decision.action, repairedRefs);
|
|
488
504
|
const action = decision.action;
|
|
489
505
|
const capability = TOOL_TO_CAPABILITY.get(name);
|
|
490
506
|
if (!capability || !actionsForEmbeddedCapability(capability).includes(action.kind)) {
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "GoTry Session Bridge",
|
|
4
|
-
"version": "0.0.1.
|
|
4
|
+
"version": "0.0.1.21",
|
|
5
5
|
"description": "GoTry 会话检索桥(issue #21):在你自己的登录态里只读嗅探检索回包(机票/酒店/火车)与登录票据 cookie 名;零凭证经手,登录在官网由你完成,检索只读不写。",
|
|
6
6
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnuzBX8rT8+RZKEUip5VRA0cY056pExy9PNdN9YCiuY3dgglRbcIBhjuFJoR6EX3/MaUv82vJVyItRVrowXLTwDk4WZof0vwFgKL0zBK304PC46pUaAKTSu1PgXeL1+j5KTAGz/9a9+RnPdbj7jND5rM/MOmBHNPVPpp1NlwxRGIgF9OxHSOfXORcH4Iyte1aJSFUJbFtNS/DaE5D+KjyupPLyIqVN/dCiKnNsTCEHQB9ngeli665PFp76hkccHQQZ4sVuMLS4zBPcazUsp1kcnW+xsQoUATkODTCY+Pzk3zUVF1QOESTjRCVnbNi8tkO43RMRs2q63gX/3inFivcEwIDAQAB",
|
|
7
7
|
"permissions": [
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"run_at": "document_start"
|
|
47
47
|
}
|
|
48
48
|
],
|
|
49
|
-
"version_name": "0.0.1-rc.
|
|
49
|
+
"version_name": "0.0.1-rc.21"
|
|
50
50
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danceiny/gotry",
|
|
3
|
-
"version": "0.0.1-rc.
|
|
3
|
+
"version": "0.0.1-rc.21",
|
|
4
4
|
"description": "GoTry — 从出发到下一次出发的 AI 旅行 Agent(dsh 插件)。npm/源码入口共用锁定的 dsh runtime + README 安装路径。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "ts/src/index.ts",
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
"extension/",
|
|
91
91
|
"ts/package.json",
|
|
92
92
|
"ts/dsh-runtime/vendor/dsh-map-tools",
|
|
93
|
+
"ts/scripts/map-tools-vendor-package-proof.ts",
|
|
93
94
|
"cordis.gotry-patch.yml",
|
|
94
95
|
"README.md",
|
|
95
96
|
"README.zh-CN.md",
|
|
@@ -155,8 +155,15 @@ export function startExtensionHealthWatch(opts: HealthWatchOptions = {}): Health
|
|
|
155
155
|
const probe = opts.probe ?? (async () => {
|
|
156
156
|
// 先确保桥存在 + keepBridge
|
|
157
157
|
const bridge = await import('./extension-bridge.ts')
|
|
158
|
-
await bridge.getOrCreateSessionBridge({ keepBridge: opts.keepBridge === true })
|
|
159
|
-
|
|
158
|
+
const created = await bridge.getOrCreateSessionBridge({ keepBridge: opts.keepBridge === true, ports: opts.ports })
|
|
159
|
+
// The default path must keep scanning the canonical pool: another GoTry
|
|
160
|
+
// process may already own the extension-connected bridge. An explicit
|
|
161
|
+
// port override is an isolation boundary, so probe only the bridge that
|
|
162
|
+
// this process actually bound (including an ephemeral `0` request).
|
|
163
|
+
const probeOpts = created.ok && opts.ports
|
|
164
|
+
? { ...opts, ports: [created.bridge.port] }
|
|
165
|
+
: opts
|
|
166
|
+
return combinedProbe(probeOpts, businessProbe)
|
|
160
167
|
})
|
|
161
168
|
const now = opts.now ?? Date.now
|
|
162
169
|
const state: InternalState = { cancelled: false, readyCount: 0, resolveOutcome: null }
|
package/ts/package.json
CHANGED
|
@@ -18,19 +18,27 @@
|
|
|
18
18
|
"better-sqlite3": "^13.0.3",
|
|
19
19
|
"z3-solver": "^5.2.0"
|
|
20
20
|
},
|
|
21
|
-
"devDependencies": {
|
|
22
|
-
"@types/node": "^22.0.0",
|
|
23
|
-
"playwright-core": "^1.62.1",
|
|
24
|
-
"puppeteer-core": "^25.9.0",
|
|
25
|
-
"tsx": "^4.19.0",
|
|
26
|
-
"typescript": "^5.6.0"
|
|
27
|
-
},
|
|
28
21
|
"overrides": {
|
|
22
|
+
"@deepseek-ai/dsh-agent": "0.1.2-alpha.3",
|
|
29
23
|
"@deepseek-ai/dsh-brand": "0.1.2-alpha.3",
|
|
24
|
+
"@deepseek-ai/dsh-code-runtime": "0.1.2-alpha.3",
|
|
25
|
+
"@deepseek-ai/dsh-invariants": "0.1.2-alpha.3",
|
|
30
26
|
"@deepseek-ai/dsh-llm": "0.1.2-alpha.3",
|
|
27
|
+
"@deepseek-ai/dsh-scope": "0.1.2-alpha.3",
|
|
28
|
+
"@deepseek-ai/dsh-session": "0.1.2-alpha.3",
|
|
29
|
+
"@deepseek-ai/dsh-session-projection": "0.1.2-alpha.3",
|
|
30
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.2-alpha.3",
|
|
31
31
|
"@deepseek-ai/dsh-timeout": "0.1.2-alpha.3",
|
|
32
32
|
"@deepseek-ai/dsh-typert-protocol": "0.1.2-alpha.3",
|
|
33
|
+
"@deepseek-ai/dsh-user-approval": "0.1.2-alpha.3",
|
|
33
34
|
"@deepseek-ai/dsh-util-crypto": "0.1.2-alpha.3",
|
|
34
35
|
"@deepseek-ai/dsh-util-values": "0.1.2-alpha.3"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"playwright-core": "^1.62.1",
|
|
40
|
+
"puppeteer-core": "^25.9.0",
|
|
41
|
+
"tsx": "^4.19.0",
|
|
42
|
+
"typescript": "^5.6.0"
|
|
35
43
|
}
|
|
36
44
|
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package proof for issue #202.
|
|
3
|
+
*
|
|
4
|
+
* The full suite passes the executable from its clean tarball install; focused
|
|
5
|
+
* runs pack/unpack locally and attach the already-installed root closure. Both
|
|
6
|
+
* paths load the plugin from the artifact, never from the checkout vendor path.
|
|
7
|
+
* No map provider or paid endpoint is contacted; inline-coordinate geocoding is
|
|
8
|
+
* asserted with fetch disabled.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execFileSync } from 'node:child_process'
|
|
12
|
+
import { createHash } from 'node:crypto'
|
|
13
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, symlinkSync } from 'node:fs'
|
|
14
|
+
import { createRequire } from 'node:module'
|
|
15
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
17
|
+
import assert from 'node:assert/strict'
|
|
18
|
+
|
|
19
|
+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
|
20
|
+
const upstreamLicenseSha256 = 'b6bbb0c73a02cf8d2c304e9f208b41c32f0a810fabe366986e2b14ac3338f618'
|
|
21
|
+
const upstreamVendorFileCount = 32
|
|
22
|
+
const upstreamVendorAggregateSha256 = 'd82a38adace8bfe574dcecb4222da5c994a1b9e8792f78dc2dad3a7f50c9925a'
|
|
23
|
+
const expectedTools = [
|
|
24
|
+
'map_bicycling_route',
|
|
25
|
+
'map_driving_route',
|
|
26
|
+
'map_geocode',
|
|
27
|
+
'map_poi_search',
|
|
28
|
+
'map_reverse_geocode',
|
|
29
|
+
'map_transit_route',
|
|
30
|
+
'map_walking_route',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
function sha256(path: string): string {
|
|
34
|
+
return createHash('sha256').update(readFileSync(path)).digest('hex')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function vendorAggregate(root: string): { count: number; sha256: string } {
|
|
38
|
+
const files: string[] = []
|
|
39
|
+
const walk = (directory: string) => {
|
|
40
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
41
|
+
const path = join(directory, entry.name)
|
|
42
|
+
if (entry.isDirectory()) walk(path)
|
|
43
|
+
else files.push(path.slice(root.length + 1).replaceAll('\\', '/'))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
walk(root)
|
|
47
|
+
files.sort()
|
|
48
|
+
const digest = createHash('sha256')
|
|
49
|
+
for (const relative of files) {
|
|
50
|
+
digest.update(relative)
|
|
51
|
+
digest.update('\0')
|
|
52
|
+
digest.update(readFileSync(join(root, relative)))
|
|
53
|
+
digest.update('\0')
|
|
54
|
+
}
|
|
55
|
+
return { count: files.length, sha256: digest.digest('hex') }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertAlpha3LockClosure(): { packages: number; version: string } {
|
|
59
|
+
const manifest = JSON.parse(readFileSync(join(repoRoot, 'ts/package.json'), 'utf8')) as {
|
|
60
|
+
dependencies?: Record<string, string>
|
|
61
|
+
}
|
|
62
|
+
assert.equal(manifest.dependencies?.['dsh-map-tools'], undefined, 'external map-tools dependency must stay removed')
|
|
63
|
+
|
|
64
|
+
const lock = JSON.parse(readFileSync(join(repoRoot, 'ts/package-lock.json'), 'utf8')) as {
|
|
65
|
+
packages?: Record<string, { version?: string; dependencies?: Record<string, string> }>
|
|
66
|
+
}
|
|
67
|
+
assert.equal(lock.packages?.['']?.dependencies?.['dsh-map-tools'], undefined, 'lock root must not resolve external map-tools')
|
|
68
|
+
assert.equal(lock.packages?.['node_modules/dsh-map-tools'], undefined, 'lock must not carry external map-tools')
|
|
69
|
+
const dshEntries = Object.entries(lock.packages ?? {}).filter(([path]) =>
|
|
70
|
+
/(?:^|\/)node_modules\/@deepseek-ai\/(?:dsh|dsh-[^/]+)$/.test(path),
|
|
71
|
+
)
|
|
72
|
+
assert.ok(dshEntries.length >= 10, 'expected dsh-tools peer closure in the ts lock')
|
|
73
|
+
for (const [path, entry] of dshEntries) {
|
|
74
|
+
assert.equal(entry.version, '0.1.2-alpha.3', `${path} drifted outside the alpha.3 closure`)
|
|
75
|
+
}
|
|
76
|
+
return { packages: dshEntries.length, version: '0.1.2-alpha.3' }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function findPackageRoot(entry: string): string {
|
|
80
|
+
let cursor = dirname(entry)
|
|
81
|
+
for (;;) {
|
|
82
|
+
const manifestPath = join(cursor, 'package.json')
|
|
83
|
+
if (existsSync(manifestPath)) {
|
|
84
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { name?: string }
|
|
85
|
+
if (manifest.name === '@danceiny/gotry') return cursor
|
|
86
|
+
}
|
|
87
|
+
const parent = dirname(cursor)
|
|
88
|
+
if (parent === cursor) throw new Error(`cannot locate @danceiny/gotry package root from ${entry}`)
|
|
89
|
+
cursor = parent
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assertBinTargetsPackage(binPath: string, expectedTarget: string): void {
|
|
94
|
+
const resolvedExpectedTarget = realpathSync(expectedTarget)
|
|
95
|
+
if (realpathSync(binPath) === resolvedExpectedTarget) return
|
|
96
|
+
|
|
97
|
+
// pnpm writes an executable shell shim instead of a symlink. Its final
|
|
98
|
+
// machine-readable marker is safer to verify than accepting any occurrence
|
|
99
|
+
// of the package path in an otherwise unrelated executable.
|
|
100
|
+
const shim = readFileSync(binPath, 'utf8')
|
|
101
|
+
assert.ok(Buffer.byteLength(shim, 'utf8') <= 32_768, 'clean-install gotry shim is unexpectedly large')
|
|
102
|
+
const lines = shim.trimEnd().split(/\r?\n/)
|
|
103
|
+
const markers = lines.filter(line => line.startsWith('# cmd-shim-target='))
|
|
104
|
+
assert.equal(markers.length, 1, 'pnpm gotry shim must contain exactly one target marker')
|
|
105
|
+
assert.equal(lines.at(-1), markers[0], 'pnpm gotry shim target marker must be the final line')
|
|
106
|
+
const markerTarget = markers[0].slice('# cmd-shim-target='.length)
|
|
107
|
+
assert.ok(isAbsolute(markerTarget), 'pnpm gotry shim target must be absolute')
|
|
108
|
+
assert.equal(
|
|
109
|
+
realpathSync(markerTarget),
|
|
110
|
+
resolvedExpectedTarget,
|
|
111
|
+
'GOTRY_MAP_TOOLS_E2E_BIN shim must target this installed GoTry package',
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function prepareProofPackage(cleanInstalledBin?: string): { packageRoot: string; proofRoot?: string } {
|
|
116
|
+
if (cleanInstalledBin) {
|
|
117
|
+
const binPath = resolve(cleanInstalledBin)
|
|
118
|
+
assert.equal(basename(binPath), 'gotry', 'clean-install executable must use the gotry bin name')
|
|
119
|
+
assert.equal(basename(dirname(binPath)), '.bin', 'clean-install executable must come from node_modules/.bin')
|
|
120
|
+
assert.equal(basename(dirname(dirname(binPath))), 'node_modules', 'clean-install executable must come from a consumer install')
|
|
121
|
+
const consumerRoot = dirname(dirname(dirname(binPath)))
|
|
122
|
+
const packageMain = createRequire(join(consumerRoot, 'package.json')).resolve('@danceiny/gotry')
|
|
123
|
+
const packageRoot = findPackageRoot(packageMain)
|
|
124
|
+
const installedManifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')) as {
|
|
125
|
+
name?: string
|
|
126
|
+
bin?: string | Record<string, string>
|
|
127
|
+
}
|
|
128
|
+
assert.equal(installedManifest.name, '@danceiny/gotry', 'clean-install executable must belong to GoTry')
|
|
129
|
+
const declaredBin = typeof installedManifest.bin === 'string'
|
|
130
|
+
? installedManifest.bin
|
|
131
|
+
: installedManifest.bin?.gotry
|
|
132
|
+
assert.equal(declaredBin, 'bin/gotry-inner.js', 'installed package must declare the production gotry bin')
|
|
133
|
+
assertBinTargetsPackage(binPath, join(packageRoot, declaredBin))
|
|
134
|
+
return { packageRoot }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const proofRoot = mkdtempSync(join(repoRoot, '.tmp-map-tools-proof-'))
|
|
138
|
+
try {
|
|
139
|
+
const packOutput = execFileSync('npm', [
|
|
140
|
+
'pack', '--ignore-scripts', '--json', '--pack-destination', proofRoot,
|
|
141
|
+
], { cwd: repoRoot, encoding: 'utf8' })
|
|
142
|
+
const packInfo = JSON.parse(packOutput) as Array<{ filename: string }>
|
|
143
|
+
assert.equal(packInfo.length, 1, 'npm pack must produce one artifact')
|
|
144
|
+
const tarball = join(proofRoot, packInfo[0].filename)
|
|
145
|
+
assert.ok(existsSync(tarball), `packed artifact missing: ${tarball}`)
|
|
146
|
+
const unpackRoot = join(proofRoot, 'unpacked')
|
|
147
|
+
mkdirSync(unpackRoot)
|
|
148
|
+
execFileSync('tar', ['-xzf', tarball, '-C', unpackRoot])
|
|
149
|
+
const packageRoot = join(unpackRoot, 'package')
|
|
150
|
+
symlinkSync(join(repoRoot, 'node_modules'), join(packageRoot, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir')
|
|
151
|
+
return { packageRoot, proofRoot }
|
|
152
|
+
} catch (error) {
|
|
153
|
+
rmSync(proofRoot, { recursive: true, force: true })
|
|
154
|
+
throw error
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const cleanInstalledBin = process.env.GOTRY_MAP_TOOLS_E2E_BIN
|
|
159
|
+
const { packageRoot, proofRoot } = prepareProofPackage(cleanInstalledBin)
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const lockedDsh = assertAlpha3LockClosure()
|
|
163
|
+
const vendorRoot = join(packageRoot, 'ts/dsh-runtime/vendor/dsh-map-tools')
|
|
164
|
+
const requiredFiles = [
|
|
165
|
+
'LICENSE',
|
|
166
|
+
'package.json',
|
|
167
|
+
'cordis.patch.yml',
|
|
168
|
+
'lib/index.js',
|
|
169
|
+
'lib/settings-ns.js',
|
|
170
|
+
'lib/config.js',
|
|
171
|
+
'lib/tools/geocode.js',
|
|
172
|
+
'lib/tools/routes.js',
|
|
173
|
+
'lib/tools/poi.js',
|
|
174
|
+
]
|
|
175
|
+
for (const relative of requiredFiles) {
|
|
176
|
+
assert.ok(existsSync(join(vendorRoot, relative)), `required vendor file missing: ${relative}`)
|
|
177
|
+
}
|
|
178
|
+
const vendorTree = vendorAggregate(vendorRoot)
|
|
179
|
+
assert.equal(vendorTree.count, upstreamVendorFileCount, 'vendored payload file count drifted')
|
|
180
|
+
assert.equal(vendorTree.sha256, upstreamVendorAggregateSha256, 'vendored payload aggregate drifted')
|
|
181
|
+
const vendorPackage = JSON.parse(readFileSync(join(vendorRoot, 'package.json'), 'utf8')) as {
|
|
182
|
+
name?: string
|
|
183
|
+
version?: string
|
|
184
|
+
license?: string
|
|
185
|
+
}
|
|
186
|
+
assert.equal(vendorPackage.name, 'dsh-map-tools')
|
|
187
|
+
assert.equal(vendorPackage.version, '0.5.1')
|
|
188
|
+
assert.equal(vendorPackage.license, 'MIT')
|
|
189
|
+
assert.equal(sha256(join(vendorRoot, 'LICENSE')), upstreamLicenseSha256, 'upstream MIT license changed')
|
|
190
|
+
|
|
191
|
+
const packageRequire = createRequire(join(packageRoot, 'package.json'))
|
|
192
|
+
const dshTools = JSON.parse(readFileSync(packageRequire.resolve('@deepseek-ai/dsh-tools/package.json'), 'utf8')) as { version: string }
|
|
193
|
+
const dshSettings = JSON.parse(readFileSync(packageRequire.resolve('@deepseek-ai/dsh-settings/package.json'), 'utf8')) as { version: string }
|
|
194
|
+
assert.equal(dshTools.version, '0.1.2-alpha.3')
|
|
195
|
+
assert.equal(dshSettings.version, '0.1.2-alpha.3')
|
|
196
|
+
const settingsApi = await import(pathToFileURL(packageRequire.resolve('@deepseek-ai/dsh-settings')).href) as {
|
|
197
|
+
SettingsProvider?: { prototype?: { installSection?: unknown } }
|
|
198
|
+
}
|
|
199
|
+
assert.equal(
|
|
200
|
+
typeof settingsApi.SettingsProvider?.prototype?.installSection,
|
|
201
|
+
'function',
|
|
202
|
+
'real alpha.3 SettingsProvider.installSection API must be present',
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
const plugin = await import(pathToFileURL(join(vendorRoot, 'lib/index.js')).href)
|
|
206
|
+
const registered: Array<Record<string, unknown>> = []
|
|
207
|
+
const settingsCalls: unknown[][] = []
|
|
208
|
+
const context = {
|
|
209
|
+
tools: {
|
|
210
|
+
register(tool: Record<string, unknown>) {
|
|
211
|
+
registered.push(tool)
|
|
212
|
+
return () => undefined
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
effect(effect: () => (() => void) | void) {
|
|
216
|
+
effect()
|
|
217
|
+
},
|
|
218
|
+
inject(dependencies: string[], callback: (scope: Record<string, unknown>) => void) {
|
|
219
|
+
if (dependencies.includes('settings')) {
|
|
220
|
+
callback({ settings: { installSection: (...args: unknown[]) => { settingsCalls.push(args) } } })
|
|
221
|
+
} else if (dependencies.includes('webServer')) {
|
|
222
|
+
callback({ webServer: { register: () => undefined } })
|
|
223
|
+
} else {
|
|
224
|
+
throw new Error(`unexpected dependency injection: ${dependencies.join(',')}`)
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
plugin.apply(context, {
|
|
229
|
+
provider: 'osm', amapKey: '', timeoutMs: 1000, maxQps: 2,
|
|
230
|
+
defaultMode: 'driving', language: 'zh',
|
|
231
|
+
})
|
|
232
|
+
assert.deepEqual(registered.map(tool => tool.name).sort(), expectedTools, 'exactly seven map tools must register')
|
|
233
|
+
assert.equal(settingsCalls.length, 1, 'alpha.3 settings section must be installed once')
|
|
234
|
+
assert.equal(settingsCalls[0][1], 'dsh-map-tools', 'settings namespace must be stable')
|
|
235
|
+
assert.equal(typeof settingsCalls[0][4], 'object', 'alpha.3 installSection hooks must be supplied')
|
|
236
|
+
|
|
237
|
+
const originalFetch = globalThis.fetch
|
|
238
|
+
globalThis.fetch = (async () => { throw new Error('network forbidden by package proof') }) as typeof fetch
|
|
239
|
+
try {
|
|
240
|
+
const geocode = registered.find(tool => tool.name === 'map_geocode')
|
|
241
|
+
assert.ok(geocode, 'map_geocode must be registered')
|
|
242
|
+
const result = await (geocode.execute as (args: unknown, execution: unknown) => Promise<unknown>)(
|
|
243
|
+
{ address: '116.397428,39.90923' },
|
|
244
|
+
{ signal: new AbortController().signal },
|
|
245
|
+
) as { provider?: string; location?: number[] }
|
|
246
|
+
assert.equal(result.provider, 'inline')
|
|
247
|
+
assert.deepEqual(result.location, [116.397428, 39.90923])
|
|
248
|
+
} finally {
|
|
249
|
+
globalThis.fetch = originalFetch
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log(JSON.stringify({
|
|
253
|
+
proof: 'map-tools-vendor-package',
|
|
254
|
+
status: 'PASS',
|
|
255
|
+
artifactMode: cleanInstalledBin ? 'clean-installed-tarball' : 'packed-unpacked-focused',
|
|
256
|
+
vendored: {
|
|
257
|
+
sourcePackage: 'dsh-map-tools@0.5.1',
|
|
258
|
+
licenseSha256: upstreamLicenseSha256, vendorFileCount: vendorTree.count,
|
|
259
|
+
vendorAggregateSha256: vendorTree.sha256,
|
|
260
|
+
},
|
|
261
|
+
artifactVendor: vendorRoot,
|
|
262
|
+
dshClosure: { tools: dshTools.version, settings: dshSettings.version, ...lockedDsh },
|
|
263
|
+
tools: registered.map(tool => tool.name).sort(),
|
|
264
|
+
inlineCoordinate: { provider: 'inline', network: 'forbidden' },
|
|
265
|
+
}, null, 2))
|
|
266
|
+
} finally {
|
|
267
|
+
if (proofRoot) rmSync(proofRoot, { recursive: true, force: true })
|
|
268
|
+
}
|
|
@@ -25,9 +25,35 @@ const MAX_TERMINAL_BYTES = 1024 * 1024
|
|
|
25
25
|
const BENCHMARK_TOOL_RESULT_SCHEMA_VERSION = 'gotry_benchmark_tool_result_v1'
|
|
26
26
|
const BENCHMARK_DOMAIN_RECOVERIES = new Set(['none', 'retry_same', 'revise_arguments', 'choose_alternative'])
|
|
27
27
|
|
|
28
|
+
// Round 12(issue #215):terminal body schema 是「无数据值的 closed 结构合同」——
|
|
29
|
+
// 只允许结构关键字(type/properties/required/additionalProperties/items),enum/
|
|
30
|
+
// const/example/default 等可夹带数据值的注解面一律禁止,对象必须 additionalProperties:
|
|
31
|
+
// false,大小/深度/节点数有界。校验语义 fail-closed:不补键、不删多余键、不转类型。
|
|
32
|
+
const TERMINAL_BODY_SCHEMA_MAX_BYTES = 16 * 1024
|
|
33
|
+
const TERMINAL_BODY_SCHEMA_MAX_DEPTH = 8
|
|
34
|
+
const TERMINAL_BODY_SCHEMA_MAX_NODES = 256
|
|
35
|
+
const TERMINAL_BODY_SCHEMA_MAX_PROPERTIES = 256
|
|
36
|
+
const TERMINAL_BODY_SCALAR_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'null'])
|
|
37
|
+
const TERMINAL_BODY_VALUE_MAX_NODES = 10_000
|
|
38
|
+
const TERMINAL_BODY_VALUE_MAX_DEPTH = 24
|
|
39
|
+
|
|
40
|
+
/** Structural-only, closed JSON schema for the terminal body (no data-bearing keywords). */
|
|
41
|
+
export type TerminalBodySchema = {
|
|
42
|
+
type: 'object'
|
|
43
|
+
properties: Record<string, TerminalBodySchema>
|
|
44
|
+
required: string[]
|
|
45
|
+
additionalProperties: false
|
|
46
|
+
} | {
|
|
47
|
+
type: 'array'
|
|
48
|
+
items: TerminalBodySchema
|
|
49
|
+
} | {
|
|
50
|
+
type: 'string' | 'number' | 'integer' | 'boolean' | 'null'
|
|
51
|
+
}
|
|
52
|
+
|
|
28
53
|
export interface TerminalOutputConfig {
|
|
29
54
|
tag: string
|
|
30
55
|
max_bytes: number
|
|
56
|
+
body_schema: TerminalBodySchema
|
|
31
57
|
}
|
|
32
58
|
|
|
33
59
|
export type TerminalOutputValue =
|
|
@@ -80,13 +106,113 @@ function plainObject(value: unknown): value is Record<string, unknown> {
|
|
|
80
106
|
export function validateTerminalOutputConfig(value: unknown): value is TerminalOutputConfig {
|
|
81
107
|
if (!plainObject(value)) return false
|
|
82
108
|
const keys = Object.keys(value).sort()
|
|
83
|
-
return JSON.stringify(keys) === JSON.stringify(['max_bytes', 'tag'])
|
|
109
|
+
return JSON.stringify(keys) === JSON.stringify(['body_schema', 'max_bytes', 'tag'])
|
|
84
110
|
&& typeof value.tag === 'string'
|
|
85
111
|
&& IDENTIFIER.test(value.tag)
|
|
86
112
|
&& typeof value.max_bytes === 'number'
|
|
87
113
|
&& Number.isInteger(value.max_bytes)
|
|
88
114
|
&& value.max_bytes >= 1
|
|
89
115
|
&& value.max_bytes <= MAX_TERMINAL_BYTES
|
|
116
|
+
&& validateTerminalBodySchema(value.body_schema)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Accept only the closed structural dialect: exact keyword sets per node, bounded size. */
|
|
120
|
+
export function validateTerminalBodySchema(value: unknown): value is TerminalBodySchema {
|
|
121
|
+
let serialized: string
|
|
122
|
+
try {
|
|
123
|
+
serialized = JSON.stringify(value)
|
|
124
|
+
} catch {
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
if (typeof serialized !== 'string' || Buffer.byteLength(serialized, 'utf8') > TERMINAL_BODY_SCHEMA_MAX_BYTES) return false
|
|
128
|
+
const pending: Array<{ node: unknown; depth: number }> = [{ node: value, depth: 0 }]
|
|
129
|
+
const seen = new Set<object>()
|
|
130
|
+
let nodes = 0
|
|
131
|
+
let properties = 0
|
|
132
|
+
while (pending.length > 0) {
|
|
133
|
+
const { node, depth } = pending.pop()!
|
|
134
|
+
if (!plainObject(node) || seen.has(node) || ++nodes > TERMINAL_BODY_SCHEMA_MAX_NODES || depth > TERMINAL_BODY_SCHEMA_MAX_DEPTH) return false
|
|
135
|
+
seen.add(node)
|
|
136
|
+
const type = node.type
|
|
137
|
+
if (type === 'object') {
|
|
138
|
+
if (!exactKeys(node, ['type', 'properties', 'required', 'additionalProperties'])
|
|
139
|
+
|| !plainObject(node.properties)
|
|
140
|
+
|| !Array.isArray(node.required)
|
|
141
|
+
|| node.additionalProperties !== false) return false
|
|
142
|
+
const entries = Object.entries(node.properties)
|
|
143
|
+
properties += entries.length
|
|
144
|
+
if (properties > TERMINAL_BODY_SCHEMA_MAX_PROPERTIES
|
|
145
|
+
|| entries.some(([key, child]) => key.length === 0 || key.length > 64 || !plainObject(child))) return false
|
|
146
|
+
if (node.required.length > entries.length
|
|
147
|
+
|| new Set(node.required).size !== node.required.length
|
|
148
|
+
|| node.required.some(key => typeof key !== 'string' || !Object.hasOwn(node.properties as Record<string, unknown>, key))) return false
|
|
149
|
+
for (const [, child] of entries) pending.push({ node: child, depth: depth + 1 })
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
if (type === 'array') {
|
|
153
|
+
if (!exactKeys(node, ['type', 'items']) || !plainObject(node.items)) return false
|
|
154
|
+
pending.push({ node: node.items, depth: depth + 1 })
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if (typeof type !== 'string' || !TERMINAL_BODY_SCALAR_TYPES.has(type) || !exactKeys(node, ['type'])) return false
|
|
158
|
+
}
|
|
159
|
+
return plainObject(value) && value.type === 'object'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function exactKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
163
|
+
return Object.keys(value).every(key => allowed.includes(key))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Strict structural validation: exact keys, exact types, no coercion, no autofix. */
|
|
167
|
+
export function validateTerminalBodyValue(value: unknown, schema: TerminalBodySchema): boolean {
|
|
168
|
+
const pending: Array<{ value: unknown; schema: TerminalBodySchema; depth: number }> = [{ value, schema, depth: 0 }]
|
|
169
|
+
let nodes = 0
|
|
170
|
+
while (pending.length > 0) {
|
|
171
|
+
const current = pending.pop()!
|
|
172
|
+
if (++nodes > TERMINAL_BODY_VALUE_MAX_NODES || current.depth > TERMINAL_BODY_VALUE_MAX_DEPTH) return false
|
|
173
|
+
const { schema: node } = current
|
|
174
|
+
if (node.type === 'object') {
|
|
175
|
+
const record = current.value
|
|
176
|
+
if (!plainObject(record)) return false
|
|
177
|
+
const properties = node.properties as Record<string, TerminalBodySchema>
|
|
178
|
+
for (const key of node.required) {
|
|
179
|
+
if (!Object.hasOwn(record, key)) return false
|
|
180
|
+
}
|
|
181
|
+
for (const [key, child] of Object.entries(record)) {
|
|
182
|
+
const childSchema = properties[key]
|
|
183
|
+
if (!childSchema) return false
|
|
184
|
+
pending.push({ value: child, schema: childSchema, depth: current.depth + 1 })
|
|
185
|
+
}
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
if (node.type === 'array') {
|
|
189
|
+
if (!Array.isArray(current.value)) return false
|
|
190
|
+
for (const item of current.value) pending.push({ value: item, schema: node.items, depth: current.depth + 1 })
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
if (node.type === 'string') { if (typeof current.value !== 'string') return false; continue }
|
|
194
|
+
if (node.type === 'boolean') { if (typeof current.value !== 'boolean') return false; continue }
|
|
195
|
+
if (node.type === 'null') { if (current.value !== null) return false; continue }
|
|
196
|
+
if (node.type === 'integer') { if (typeof current.value !== 'number' || !Number.isSafeInteger(current.value)) return false; continue }
|
|
197
|
+
if (typeof current.value !== 'number' || !Number.isFinite(current.value)) return false
|
|
198
|
+
}
|
|
199
|
+
return true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Deterministic single-source structural outline projected into system prompt and correction alike. */
|
|
203
|
+
export function terminalSchemaOutline(schema: TerminalBodySchema): string {
|
|
204
|
+
const render = (node: TerminalBodySchema, depth: number): string => {
|
|
205
|
+
if (depth > TERMINAL_BODY_SCHEMA_MAX_DEPTH) return '…'
|
|
206
|
+
if (node.type === 'object') {
|
|
207
|
+
const properties = node.properties as Record<string, TerminalBodySchema>
|
|
208
|
+
const required = new Set(node.required)
|
|
209
|
+
const parts = Object.entries(properties).map(([key, child]) => `${required.has(key) ? '' : '?'}${key}:${render(child, depth + 1)}`)
|
|
210
|
+
return `object{${parts.join(',')}}`
|
|
211
|
+
}
|
|
212
|
+
if (node.type === 'array') return `array<${render(node.items, depth + 1)}>`
|
|
213
|
+
return node.type
|
|
214
|
+
}
|
|
215
|
+
return render(schema, 0)
|
|
90
216
|
}
|
|
91
217
|
|
|
92
218
|
function invalidTerminal(): TerminalOutputValue {
|
|
@@ -116,7 +242,11 @@ export function parseBenchmarkTerminal(raw: string, config: TerminalOutputConfig
|
|
|
116
242
|
|
|
117
243
|
try {
|
|
118
244
|
const value: unknown = JSON.parse(body)
|
|
119
|
-
|
|
245
|
+
if (!plainObject(value)) return invalidTerminal()
|
|
246
|
+
// Round 12(#215):同一份 closed body schema 在接受终态前 fail-closed 校验——
|
|
247
|
+
// 结构不合法的终态永不进入正式计分链,不做任何 autofix。
|
|
248
|
+
if (!validateTerminalBodyValue(value, config.body_schema)) return invalidTerminal()
|
|
249
|
+
return { ok: true, value }
|
|
120
250
|
} catch {
|
|
121
251
|
return invalidTerminal()
|
|
122
252
|
}
|
|
@@ -363,7 +493,7 @@ function requireDisposer(value: unknown, capability: string): Disposer {
|
|
|
363
493
|
function correctionMessage(mode: Exclude<RetryMode, 'none'>, projection: BenchmarkBridgeProjection) {
|
|
364
494
|
const text = mode === 'call'
|
|
365
495
|
? `BENCHMARK_CONFORMANCE_CALL: Execute exactly one native ${projection.toolName} action:"call" now. Describing an intended CLI, shell, or Python command does not execute it.`
|
|
366
|
-
: `BENCHMARK_CONFORMANCE_TERMINAL: Reuse the existing successful tool result. Do not call any tool. Reply only <${projection.terminal.tag}>{
|
|
496
|
+
: `BENCHMARK_CONFORMANCE_TERMINAL: Reuse the existing successful tool result. Do not call any tool. Reply only <${projection.terminal.tag}> with one JSON object matching exactly ${terminalSchemaOutline(projection.terminal.body_schema)} — no extra keys, no missing keys, exact types.`
|
|
367
497
|
return createUserMessage({
|
|
368
498
|
content: [{ type: 'text' as const, text }],
|
|
369
499
|
source: { kind: 'plugin' as const, plugin: 'gotry-benchmark-agent-conformance' },
|
|
@@ -372,6 +502,7 @@ function correctionMessage(mode: Exclude<RetryMode, 'none'>, projection: Benchma
|
|
|
372
502
|
|
|
373
503
|
function systemSection(projection: BenchmarkBridgeProjection): { name: string; text: string } {
|
|
374
504
|
const allowed = projection.allowedTools.join(', ')
|
|
505
|
+
const outline = terminalSchemaOutline(projection.terminal.body_schema)
|
|
375
506
|
return {
|
|
376
507
|
name: 'benchmark:agent-conformance',
|
|
377
508
|
text: [
|
|
@@ -379,7 +510,7 @@ function systemSection(projection: BenchmarkBridgeProjection): { name: string; t
|
|
|
379
510
|
`- Translate every task instruction to use a CLI, shell, Python, or agent_env.cli into the native tool ${projection.toolName}; do not merely describe the intended command.`,
|
|
380
511
|
`- Call it with exactly {"action":"call","tool":"<one of: ${allowed}>","arguments":{...}}.`,
|
|
381
512
|
'- action:"tools" is discovery only and does not satisfy the required environment call.',
|
|
382
|
-
`- After a successful tool result, reply only <${projection.terminal.tag}>
|
|
513
|
+
`- After a successful tool result, reply only <${projection.terminal.tag}> with one JSON object matching exactly ${outline} — no extra keys, no missing keys, exact types; no prose or code fence.`,
|
|
383
514
|
'- If a terminal-format correction arrives, reuse the existing result and do not call the tool again.',
|
|
384
515
|
].join('\n'),
|
|
385
516
|
}
|