@ljwei-stak/dsh-model-router 0.13.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 (37) hide show
  1. package/.dsh-plugin/client.js +3092 -0
  2. package/.dsh-plugin/index.mjs +1651 -0
  3. package/.dsh-plugin/official-tools-remote-service.mjs +104 -0
  4. package/.dsh-plugin/shared/harness-plan.mjs +179 -0
  5. package/.dsh-plugin/shared/livebench.mjs +264 -0
  6. package/.dsh-plugin/shared/model-profiles.mjs +142 -0
  7. package/.dsh-plugin/shared/official-team-runtime.mjs +411 -0
  8. package/.dsh-plugin/shared/official-tool-executor.mjs +801 -0
  9. package/.dsh-plugin/shared/official-tool-registry.mjs +138 -0
  10. package/.dsh-plugin/shared/official-tools-remote.mjs +173 -0
  11. package/.dsh-plugin/shared/official-tools-runtime.mjs +642 -0
  12. package/.dsh-plugin/shared/router-state.mjs +207 -0
  13. package/.dsh-plugin/shared/router.mjs +1134 -0
  14. package/.dsh-plugin/shared/routing-presets.mjs +49 -0
  15. package/.dsh-plugin/shared/run-ledger.mjs +348 -0
  16. package/.dsh-plugin/shared/security-boundaries.mjs +54 -0
  17. package/.dsh-plugin/shared/subscription-billing.mjs +340 -0
  18. package/.dsh-plugin/shared/task-executors.mjs +1154 -0
  19. package/.dsh-plugin/shared/tool-health.mjs +311 -0
  20. package/.dsh-plugin/shared/vendor-mimo-grok-adapter.mjs +308 -0
  21. package/.dsh-plugin/shared/vendor-minimax-adapter.mjs +247 -0
  22. package/.dsh-plugin/shared/zcode-bundle.mjs +208 -0
  23. package/.dsh-plugin/shared/zcode-installer.mjs +247 -0
  24. package/CHANGELOG.md +36 -0
  25. package/INSTALLATION_GUIDE.zh.md +134 -0
  26. package/LICENSE +21 -0
  27. package/MIGRATION.md +53 -0
  28. package/README.i18n.yaml +3 -0
  29. package/README.md +424 -0
  30. package/README.zh.md +413 -0
  31. package/cordis.patch.yml +12 -0
  32. package/docs/assets/candidate-pruning.svg +80 -0
  33. package/docs/assets/desktop-official-tools-0.9.0.png +0 -0
  34. package/docs/assets/router-only-0.12.0.png +0 -0
  35. package/docs/assets/routing-workflow.svg +96 -0
  36. package/docs/assets/workbench-usage.svg +119 -0
  37. package/package.json +161 -0
@@ -0,0 +1,801 @@
1
+ /**
2
+ * Bounded, non-interactive execution of verified official CLI entry points.
3
+ *
4
+ * The Host must authorize the request and create an isolated workspace before
5
+ * offering the write mode. The caller supplies a registry id, task text, and
6
+ * an absolute workspace, never a command or arbitrary arguments. Write mode
7
+ * additionally needs a trusted isolation root supplied by the Host;
8
+ * this module rejects a workspace outside that root.
9
+ *
10
+ * Claude: https://code.claude.com/docs/en/headless
11
+ * https://code.claude.com/docs/en/cli-reference
12
+ * Codex: https://learn.chatgpt.com/docs/non-interactive-mode
13
+ * Kimi's `-p` enables auto permissions:
14
+ * https://moonshotai.github.io/kimi-code/en/reference/kimi-command.html
15
+ */
16
+ import { spawn } from 'node:child_process'
17
+ import { createHash } from 'node:crypto'
18
+ import { access, readFile, realpath, stat } from 'node:fs/promises'
19
+ import { constants } from 'node:fs'
20
+ import { StringDecoder } from 'node:string_decoder'
21
+ import { basename, delimiter, isAbsolute, join, relative, resolve, sep } from 'node:path'
22
+ import { getOfficialTool, OFFICIAL_TOOLS } from './official-tool-registry.mjs'
23
+ import { ensureNpmPrefixOnPath } from './official-tools-runtime.mjs'
24
+ import { buildMiniMaxInvocation, createMiniMaxStreamParser } from './vendor-minimax-adapter.mjs'
25
+ import { discoverZCodeBundle } from './zcode-bundle.mjs'
26
+ import { resolveMiMoGrokLaunch, createMiMoGrokParser } from './vendor-mimo-grok-adapter.mjs'
27
+ import { usageFromEvents, usageFromOutput } from './task-executors.mjs'
28
+
29
+ const IS_WINDOWS = process.platform === 'win32'
30
+ const MAX_TASK_BYTES = 64_000
31
+ const MAX_OUTPUT_BYTES = 32_000_000
32
+ const OUTPUT_TAIL_CHARS = 32_000
33
+ const DEFAULT_TIMEOUT_MS = 45 * 60_000
34
+ const MAX_TIMEOUT_MS = 2 * 60 * 60_000
35
+ const STOP_GRACE_MS = 5_000
36
+ const CLAUDE_MIN_RESTRICTED_VERSION = [2, 1, 259]
37
+ const CLAUDE_SIGNER = 'Anthropic, PBC'
38
+ const CODEX_SIGNER = 'OpenAI OpCo, LLC'
39
+ // SHA-256 of package/cli.js in the official @minimax-ai/code@0.5.5 npm tarball.
40
+ // The Windows installer-managed release carries the exact same entry file.
41
+ const MINIMAX_055_CLI_SHA256 = '8d36dec74e93beddf392459557a7afd8655bcdc874121981608d2c82658814ab'
42
+ const WINDOWS_SYSTEM32 = join(process.env.SystemRoot ?? 'C:\\Windows', 'System32')
43
+ const ENVIRONMENT_KEYS = Object.freeze([
44
+ 'PATH', 'Path', 'PATHEXT', 'SystemRoot', 'windir', 'ComSpec',
45
+ 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA',
46
+ 'PROGRAMDATA', 'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL', 'TERM',
47
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
48
+ 'SSL_CERT_FILE', 'NODE_EXTRA_CA_CERTS',
49
+ ])
50
+
51
+ const CAPABILITIES = Object.freeze({
52
+ 'claude-code': Object.freeze(IS_WINDOWS
53
+ ? { supported: true, modes: Object.freeze(['read-only', 'workspace-write']) }
54
+ : { supported: false, reason: 'Claude 的受限原生入口目前仅在 Windows 完成适配。' }),
55
+ codex: Object.freeze(IS_WINDOWS
56
+ ? { supported: true, modes: Object.freeze(['read-only', 'workspace-write']) }
57
+ : { supported: false, reason: 'Codex 的签名原生入口目前仅在 Windows 完成适配。' }),
58
+ 'kimi-code': Object.freeze({
59
+ supported: true, modes: Object.freeze(['workspace-write']),
60
+ }),
61
+ 'minimax-code': Object.freeze({ supported: true, modes: Object.freeze(['workspace-write']) }),
62
+ 'mimo-code': Object.freeze(IS_WINDOWS
63
+ ? { supported: true, modes: Object.freeze(['read-only', 'workspace-write']) }
64
+ : { supported: false, reason: 'MiMo 原生执行入口目前仅在 Windows 完成适配。' }),
65
+ 'grok-build': Object.freeze(IS_WINDOWS
66
+ ? { supported: true, modes: Object.freeze(['read-only', 'workspace-write']) }
67
+ : { supported: false, reason: 'Grok 原生执行入口目前仅在 Windows 完成适配。' }),
68
+ zcode: Object.freeze(IS_WINDOWS
69
+ ? { supported: true, modes: Object.freeze(['workspace-write']) }
70
+ : { supported: false, reason: 'ZCode 固定桌面发行版目前仅核验了 Windows x64。' }),
71
+ gemini: Object.freeze({
72
+ supported: false,
73
+ reason: 'Gemini 由无界面适配器执行(gemini -p),不走签名沙箱入口;未安装或失败时回退模型目录 API。',
74
+ }),
75
+ })
76
+
77
+ /** A UI/Host can show this without implying that installation means execution readiness. */
78
+ export function officialToolExecutionCapabilities() {
79
+ return OFFICIAL_TOOLS.map(tool => ({ id: tool.id, ...CAPABILITIES[tool.id] }))
80
+ }
81
+
82
+ function inside(parent, child) {
83
+ const part = relative(parent, child)
84
+ return part !== '' && part !== '..' && !part.startsWith(`..${sep}`) && !isAbsolute(part)
85
+ }
86
+
87
+ async function checkedWorkspace(workspace) {
88
+ if (typeof workspace !== 'string' || !isAbsolute(workspace) || workspace.includes('\0')) {
89
+ throw new TypeError('workspace must be an absolute directory path')
90
+ }
91
+ const canonical = await realpath(workspace)
92
+ if (!(await stat(canonical)).isDirectory()) throw new TypeError('workspace must be a directory')
93
+ if (resolve(canonical) === resolve(canonical, '..')) {
94
+ throw new TypeError('a filesystem root is not an acceptable workspace')
95
+ }
96
+ return canonical
97
+ }
98
+
99
+ async function checkedWriteWorkspace(workspace, isolatedRoot) {
100
+ if (typeof isolatedRoot !== 'string' || !isAbsolute(isolatedRoot)) {
101
+ throw new TypeError('workspace-write requires a trusted absolute isolatedRoot')
102
+ }
103
+ const root = await checkedWorkspace(isolatedRoot)
104
+ if (!inside(root, workspace)) {
105
+ throw new TypeError('workspace-write is allowed only inside isolatedRoot')
106
+ }
107
+ }
108
+
109
+ function checkedMode(mode) {
110
+ if (mode !== 'read-only' && mode !== 'workspace-write') {
111
+ throw new TypeError('mode must be read-only or workspace-write')
112
+ }
113
+ return mode
114
+ }
115
+
116
+ function checkedTask(task) {
117
+ if (typeof task !== 'string' || !task.trim() || task.includes('\0') || Buffer.byteLength(task, 'utf8') > MAX_TASK_BYTES) {
118
+ throw new TypeError(`task must be nonempty text of at most ${MAX_TASK_BYTES} UTF-8 bytes`)
119
+ }
120
+ return task
121
+ }
122
+
123
+ function checkedModel(modelId) {
124
+ if (modelId === undefined || modelId === null || modelId === '') return null
125
+ if (typeof modelId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,119}$/.test(modelId)) {
126
+ throw new TypeError('modelId must be a bounded model identifier')
127
+ }
128
+ return modelId
129
+ }
130
+
131
+ function checkedTimeout(timeoutMs) {
132
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1_000 || timeoutMs > MAX_TIMEOUT_MS) {
133
+ throw new TypeError(`timeoutMs must be an integer between 1000 and ${MAX_TIMEOUT_MS}`)
134
+ }
135
+ return timeoutMs
136
+ }
137
+
138
+ /** API-key variables removed when a run must use the CLI's own subscription login. */
139
+ const API_KEY_VARIABLES = Object.freeze(['ANTHROPIC_API_KEY', 'CODEX_API_KEY', 'OPENAI_API_KEY', 'KIMI_API_KEY',
140
+ 'MOONSHOT_API_KEY', 'MINIMAX_API_KEY', 'MIMO_API_KEY', 'XAI_API_KEY', 'ZAI_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_API_KEY'])
141
+
142
+ function executionEnvironment(toolId, { sessionOnly = false } = {}) {
143
+ const vendorKeys = {
144
+ 'claude-code': ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_CONFIG_DIR'],
145
+ codex: ['CODEX_API_KEY', 'OPENAI_API_KEY', 'CODEX_HOME'],
146
+ 'kimi-code': ['KIMI_CODE_HOME', 'KIMI_API_KEY', 'MOONSHOT_API_KEY'],
147
+ 'minimax-code': ['MINIMAX_API_KEY', 'MINIMAX_BASE_URL'],
148
+ 'mimo-code': ['MIMO_API_KEY'],
149
+ 'grok-build': ['XAI_API_KEY', 'GROK_HOME'],
150
+ zcode: ['ZAI_API_KEY', 'ZCODE_HOME'],
151
+ }
152
+ const keys = [...ENVIRONMENT_KEYS, ...(vendorKeys[toolId] ?? [])]
153
+ .filter(key => !sessionOnly || !API_KEY_VARIABLES.includes(key))
154
+ const env = {}
155
+ for (const key of keys) {
156
+ if (process.env[key] !== undefined) env[key] = process.env[key]
157
+ }
158
+ return env
159
+ }
160
+
161
+ function pathDirectories() {
162
+ const value = process.env.PATH ?? process.env.Path ?? ''
163
+ return [...new Set(value.split(delimiter).map(part => part.trim().replace(/^"|"$/g, '')).filter(Boolean))]
164
+ }
165
+
166
+ function versionAtLeast(value, minimum) {
167
+ const found = /^(\d+)\.(\d+)\.(\d+)(?:$|-)/.exec(String(value ?? ''))
168
+ if (!found) return false
169
+ for (let index = 0; index < 3; index += 1) {
170
+ const difference = Number(found[index + 1]) - minimum[index]
171
+ if (difference !== 0) return difference > 0
172
+ }
173
+ return true
174
+ }
175
+
176
+ /** Resolve the package's real `bin` file, never its Windows .cmd shim. */
177
+ async function findGlobalPackageEntries(packageName, binName, expectedBin, workspace) {
178
+ await ensureNpmPrefixOnPath()
179
+ const packageParts = packageName.split('/')
180
+ const found = []
181
+ const seen = new Set()
182
+ for (const directory of pathDirectories()) {
183
+ const moduleParents = [join(directory, 'node_modules')]
184
+ if (basename(directory).toLowerCase() === '.bin') moduleParents.push(resolve(directory, '..'))
185
+ if (!IS_WINDOWS) moduleParents.push(resolve(directory, '..', 'lib', 'node_modules'))
186
+ for (const modules of moduleParents) {
187
+ const packageDirectory = join(modules, ...packageParts)
188
+ try {
189
+ const manifest = JSON.parse(await readFile(join(packageDirectory, 'package.json'), 'utf8'))
190
+ if (manifest.name !== packageName || manifest.bin?.[binName] !== expectedBin) continue
191
+ const packageRoot = await realpath(packageDirectory)
192
+ if (packageRoot === workspace || inside(workspace, packageRoot)) continue
193
+ if (seen.has(packageRoot)) continue
194
+ const entry = await realpath(join(packageRoot, expectedBin))
195
+ if (!inside(packageRoot, entry) || !(await stat(entry)).isFile()) continue
196
+ seen.add(packageRoot)
197
+ found.push({ entry, packageRoot, version: manifest.version, source: 'verified-npm-package' })
198
+ } catch { /* this PATH entry is not the requested package */ }
199
+ }
200
+ }
201
+ return found
202
+ }
203
+
204
+ /** The official MiniMax Windows installer uses a versioned releases tree rather than npm -g. */
205
+ export async function findManagedMiniMaxEntry(workspace,
206
+ directories = pathDirectories().flatMap(directory => [directory, join(directory, '.minimax-code')]),
207
+ trustedCliSha256 = MINIMAX_055_CLI_SHA256) {
208
+ if (!IS_WINDOWS) return null
209
+ const version = getOfficialTool('minimax-code').version
210
+ for (const directory of directories) {
211
+ try {
212
+ const root = await realpath(directory)
213
+ if (root === workspace || inside(workspace, root)) continue
214
+ if (!(await stat(join(root, 'mcode.cmd'))).isFile()) continue
215
+ const current = (await readFile(join(root, 'current'), 'utf8')).trim()
216
+ if (current !== version) continue
217
+ const releaseRoot = await realpath(join(root, 'releases', version))
218
+ if (!inside(root, releaseRoot)) continue
219
+ const packageRoot = await realpath(join(releaseRoot, 'node_modules', '@minimax-ai', 'code'))
220
+ if (!inside(releaseRoot, packageRoot)) continue
221
+ const manifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'))
222
+ if (manifest.name !== '@minimax-ai/code' || manifest.version !== version
223
+ || String(manifest.bin?.mcode).replace(/^\.\//, '') !== 'cli.js') continue
224
+ const entry = await realpath(join(packageRoot, 'cli.js'))
225
+ if (!inside(packageRoot, entry) || !(await stat(entry)).isFile()) continue
226
+ const digest = createHash('sha256').update(await readFile(entry)).digest('hex')
227
+ if (digest !== trustedCliSha256) continue
228
+ const native = await realpath(join(packageRoot, 'node_modules', 'better-sqlite3',
229
+ 'build', 'Release', 'better_sqlite3.node'))
230
+ if (!inside(packageRoot, native) || !(await stat(native)).isFile()) continue
231
+ return { entry, packageRoot, version, source: 'verified-official-windows-installer-bundle' }
232
+ } catch { /* inspect the next PATH directory */ }
233
+ }
234
+ return null
235
+ }
236
+
237
+ async function findCodexExecutable(workspace) {
238
+ await ensureNpmPrefixOnPath()
239
+ const filename = IS_WINDOWS ? 'codex.exe' : 'codex'
240
+ for (const directory of pathDirectories()) {
241
+ try {
242
+ const pathRoot = await realpath(directory)
243
+ if (pathRoot === workspace || inside(workspace, pathRoot)) continue
244
+ const entry = await realpath(join(directory, filename))
245
+ if (inside(workspace, entry) || !(await stat(entry)).isFile()) continue
246
+ await access(entry, IS_WINDOWS ? constants.F_OK : constants.X_OK)
247
+ if (IS_WINDOWS && !(await hasOfficialWindowsSignature(entry, CODEX_SIGNER))) continue
248
+ return { entry, source: IS_WINDOWS ? 'signed-native-executable' : 'path-native-executable' }
249
+ } catch { /* try the next PATH entry */ }
250
+ }
251
+ return null
252
+ }
253
+
254
+ function nodeVersion(entry) {
255
+ return new Promise(resolveVersion => {
256
+ let child
257
+ try { child = spawn(entry, ['--version'], { shell: false, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] }) }
258
+ catch { resolveVersion(null); return }
259
+ let output = ''
260
+ let settled = false
261
+ const finish = value => {
262
+ if (settled) return
263
+ settled = true
264
+ clearTimeout(timer)
265
+ resolveVersion(value)
266
+ }
267
+ const timer = setTimeout(() => { child.kill('SIGTERM'); finish(null) }, 4_000)
268
+ child.stdout.on('data', bytes => { output = tail(output, String(bytes)) })
269
+ child.on('error', () => finish(null))
270
+ child.on('close', code => finish(code === 0 ? /^v(\d+\.\d+\.\d+)/.exec(output.trim())?.[1] ?? null : null))
271
+ })
272
+ }
273
+
274
+ async function findNodeExecutable(workspace, minimum = [22, 19, 0], acceptedVersion = () => true) {
275
+ await ensureNpmPrefixOnPath()
276
+ const candidates = [process.execPath, ...pathDirectories().map(directory => join(directory, IS_WINDOWS ? 'node.exe' : 'node'))]
277
+ for (const candidate of candidates) {
278
+ if (basename(candidate).toLowerCase() !== (IS_WINDOWS ? 'node.exe' : 'node')) continue
279
+ try {
280
+ const entry = await realpath(candidate)
281
+ if (inside(workspace, entry) || !(await stat(entry)).isFile()) continue
282
+ await access(entry, IS_WINDOWS ? constants.F_OK : constants.X_OK)
283
+ if (IS_WINDOWS && !(await hasOfficialWindowsSignature(entry, 'OpenJS Foundation'))) continue
284
+ const version = await nodeVersion(entry)
285
+ if (!versionAtLeast(version, minimum) || !acceptedVersion(version)) continue
286
+ return entry
287
+ } catch { /* inspect next candidate */ }
288
+ }
289
+ return null
290
+ }
291
+
292
+ /** Resolve the signed native binary carried by the official npm wrapper. */
293
+ async function findSignedPackagedCodex(packageRoot, workspace) {
294
+ if (!IS_WINDOWS) return null
295
+ const target = process.arch === 'arm64' ? ['codex-win32-arm64', 'aarch64-pc-windows-msvc']
296
+ : process.arch === 'x64' ? ['codex-win32-x64', 'x86_64-pc-windows-msvc'] : null
297
+ if (!target) return null
298
+ const candidate = join(packageRoot, 'node_modules', '@openai', target[0], 'vendor', target[1], 'bin', 'codex.exe')
299
+ try {
300
+ const entry = await realpath(candidate)
301
+ if (!inside(packageRoot, entry) || inside(workspace, entry) || !(await stat(entry)).isFile()) return null
302
+ if (!(await hasOfficialWindowsSignature(entry, CODEX_SIGNER))) return null
303
+ return { entry, source: 'signed-official-npm-native' }
304
+ } catch { return null }
305
+ }
306
+
307
+ /** PATH or package metadata alone is insufficient: verify the Windows EXE publisher. */
308
+ function hasOfficialWindowsSignature(entry, expectedSigner) {
309
+ // Some launch environments inherit a PowerShell module path where the
310
+ // built-in Security module does not autoload. Import its fixed system path
311
+ // explicitly, and fail closed if that import or the signature check fails.
312
+ const modulePath = join(WINDOWS_SYSTEM32, 'WindowsPowerShell', 'v1.0', 'Modules',
313
+ 'Microsoft.PowerShell.Security', 'Microsoft.PowerShell.Security.psd1')
314
+ const script = '$ErrorActionPreference="Stop"; '
315
+ + 'Import-Module -Name $env:MODEL_ROUTER_SECURITY_MODULE -ErrorAction Stop; '
316
+ + '$s=Get-AuthenticodeSignature -LiteralPath $env:MODEL_ROUTER_VERIFY_PATH; '
317
+ + '@{status=[string]$s.Status;subject=[string]$s.SignerCertificate.Subject} | ConvertTo-Json -Compress'
318
+ const powershell = join(WINDOWS_SYSTEM32, 'WindowsPowerShell', 'v1.0', 'powershell.exe')
319
+ return new Promise(resolveSignature => {
320
+ let child
321
+ try {
322
+ child = spawn(powershell, ['-NoProfile', '-NonInteractive', '-Command', script], {
323
+ shell: false,
324
+ windowsHide: true,
325
+ stdio: ['ignore', 'pipe', 'ignore'],
326
+ env: { ...process.env, MODEL_ROUTER_VERIFY_PATH: entry,
327
+ MODEL_ROUTER_SECURITY_MODULE: modulePath },
328
+ })
329
+ } catch { resolveSignature(false); return }
330
+ let output = ''
331
+ let done = false
332
+ const settle = valid => {
333
+ if (done) return
334
+ done = true
335
+ clearTimeout(timer)
336
+ resolveSignature(valid)
337
+ }
338
+ const timer = setTimeout(() => { child.kill('SIGTERM'); settle(false) }, 8_000)
339
+ child.stdout.on('data', bytes => { output = tail(output, String(bytes)).slice(-4_096) })
340
+ child.on('error', () => settle(false))
341
+ child.on('close', code => {
342
+ if (code !== 0) { settle(false); return }
343
+ try {
344
+ const signature = JSON.parse(output.trim().replace(/^\uFEFF/, ''))
345
+ settle(signature.status === 'Valid' && String(signature.subject).includes(expectedSigner))
346
+ } catch { settle(false) }
347
+ })
348
+ })
349
+ }
350
+
351
+ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readiness.') {
352
+ if (toolId === 'claude-code') {
353
+ const entries = await findGlobalPackageEntries('@anthropic-ai/claude-code', 'claude', 'bin/claude.exe', workspace)
354
+ const compatible = entries.filter(entry => versionAtLeast(entry.version, CLAUDE_MIN_RESTRICTED_VERSION))
355
+ let found = null
356
+ for (const entry of compatible) {
357
+ if (!IS_WINDOWS || await hasOfficialWindowsSignature(entry.entry, CLAUDE_SIGNER)) {
358
+ found = entry
359
+ break
360
+ }
361
+ }
362
+ if (!found) return { unsupported: compatible.length > 0
363
+ ? '未找到具有有效 Anthropic 签名的 claude.exe。'
364
+ : entries.length > 0 ? 'Claude Code 版本过旧,受限执行所需参数要求 >= 2.1.259。'
365
+ : '未找到官方 @anthropic-ai/claude-code 包的原生 claude.exe;不会调用 .cmd 包装器。' }
366
+ return {
367
+ file: found.entry,
368
+ args: [
369
+ '--no-session-persistence',
370
+ ...(mode === 'read-only' ? [
371
+ '--restricted', '--disable-slash-commands', '--strict-mcp-config',
372
+ '--tools', 'Read,Glob,Grep', '--disallowedTools', 'mcp__*',
373
+ ] : []),
374
+ '--permission-mode', mode === 'workspace-write' ? 'bypassPermissions' : 'dontAsk',
375
+ ...(modelId ? ['--model', modelId] : []),
376
+ '-p', '--output-format', 'json',
377
+ mode === 'workspace-write'
378
+ ? 'Carry out the task supplied on standard input. Work inside this isolated Git workspace. You may use your normal official tools, including shell, skills and configured MCP. Report what changed, commands run, and what still needs verification.'
379
+ : 'Read the task supplied on standard input. Analyze the workspace without changing files and give a clear final answer.',
380
+ ],
381
+ format: 'claude-json',
382
+ source: found.source,
383
+ }
384
+ }
385
+ if (toolId === 'codex') {
386
+ if (!IS_WINDOWS) return { unsupported: CAPABILITIES.codex.reason }
387
+ const packages = await findGlobalPackageEntries('@openai/codex', 'codex', 'bin/codex.js', workspace)
388
+ let found = null
389
+ for (const entry of packages) {
390
+ found = await findSignedPackagedCodex(entry.packageRoot, workspace)
391
+ if (found) break
392
+ }
393
+ found ??= await findCodexExecutable(workspace)
394
+ if (!found) return { unsupported: '未找到具有 OpenAI 有效签名的 codex.exe。' }
395
+ return {
396
+ file: found.entry,
397
+ args: [
398
+ '--ask-for-approval', 'never', 'exec',
399
+ ...(modelId ? ['--model', modelId] : []),
400
+ '--sandbox', mode, '--json', '-',
401
+ ],
402
+ format: 'codex-jsonl',
403
+ source: found.source,
404
+ }
405
+ }
406
+ if (toolId === 'kimi-code') {
407
+ const tool = getOfficialTool(toolId)
408
+ const entries = await findGlobalPackageEntries(tool.package, 'kimi', 'dist/main.mjs', workspace)
409
+ const found = entries.find(entry => entry.version === tool.version)
410
+ if (!found) return { unsupported: `未找到固定版本 ${tool.package}@${tool.version} 的官方入口。` }
411
+ const nodeExecutable = await findNodeExecutable(workspace)
412
+ if (!nodeExecutable) return { unsupported: '未找到工作区外的 Node.js 可执行文件。' }
413
+ if (Buffer.byteLength(task, 'utf8') > 16_000) {
414
+ return { unsupported: 'Kimi 单次 -p 参数超过 16 KB;请先拆分任务。' }
415
+ }
416
+ return {
417
+ file: nodeExecutable,
418
+ args: [found.entry, ...(modelId ? ['--model', modelId] : []),
419
+ '--prompt', task, '--output-format', 'stream-json'],
420
+ format: 'kimi-stream-json', source: found.source, stdinTask: false,
421
+ }
422
+ }
423
+ if (toolId === 'minimax-code') {
424
+ const tool = getOfficialTool(toolId)
425
+ const entries = await findGlobalPackageEntries(tool.package, 'mcode', 'cli.js', workspace)
426
+ const found = entries.find(entry => entry.version === tool.version)
427
+ ?? await findManagedMiniMaxEntry(workspace)
428
+ if (!found) return { unsupported: `未找到固定版本 ${tool.package}@${tool.version} 的官方入口。` }
429
+ const nodeExecutable = await findNodeExecutable(workspace, [22, 19, 0], version => {
430
+ const major = Number(String(version).split('.')[0])
431
+ return major === 22 || (major >= 24 && major < 27)
432
+ })
433
+ if (!nodeExecutable) return { unsupported: '未找到工作区外的 Node.js 可执行文件。' }
434
+ if (modelId && !modelId.includes('/')) {
435
+ return { unsupported: 'MiniMax CLI 要求 provider/model;当前模型目录 ID 不能直接传入,请在 MiniMax 中配置别名。' }
436
+ }
437
+ return { ...buildMiniMaxInvocation({
438
+ nodeExecutable, cliEntry: found.entry, workspace, mode,
439
+ modelReference: modelId, permission: mode === 'workspace-write' ? 'full' : 'smart',
440
+ }), source: found.source }
441
+ }
442
+ if (toolId === 'mimo-code' || toolId === 'grok-build') {
443
+ try {
444
+ return await resolveMiMoGrokLaunch({ toolId, workspace, mode, modelId, task })
445
+ } catch (error) {
446
+ return { unsupported: `官方原生入口校验失败:${String(error?.message ?? error).slice(0, 240)}` }
447
+ }
448
+ }
449
+ if (toolId === 'zcode') {
450
+ const found = await discoverZCodeBundle()
451
+ if (!found) return { unsupported: '未找到已签名、版本匹配且 CLI 脚本哈希正确的 ZCode 桌面版。' }
452
+ const nodeExecutable = await findNodeExecutable(workspace, [24, 14, 0], version =>
453
+ Number(String(version).split('.')[0]) === 24)
454
+ if (!nodeExecutable) return { unsupported: '未找到工作区外的 Node.js 可执行文件。' }
455
+ if (Buffer.byteLength(task, 'utf8') > 16_000) {
456
+ return { unsupported: 'ZCode 单次 --prompt 参数超过 16 KB;请先拆分任务。' }
457
+ }
458
+ return {
459
+ file: nodeExecutable,
460
+ args: [found.cliEntry, '--prompt', task, '--cwd', workspace,
461
+ '--mode', mode === 'workspace-write' ? 'yolo' : 'plan', '--output-format', 'stream-json'],
462
+ format: 'zcode-stream-json', source: found.source, stdinTask: false,
463
+ requestedModel: null,
464
+ modelNotice: modelId
465
+ ? 'ZCode 3.14.3 的 CLI 没有 --model 参数;本次使用 ZCode 已配置的默认模型,不能保证与 Harness 建议模型一致。'
466
+ : '本次使用 ZCode 已配置的默认模型。',
467
+ }
468
+ }
469
+ return { unsupported: CAPABILITIES[toolId]?.reason ?? '此官方工具尚无已核验的安全执行适配器。' }
470
+ }
471
+
472
+ /** Check the actual trusted launch entry without starting an account call. */
473
+ export async function officialToolReadiness(toolId, workspace = process.cwd()) {
474
+ const tool = getOfficialTool(toolId)
475
+ if (!tool) return { id: String(toolId), ready: false, reason: '工具不在固定官方注册表中。' }
476
+ const capability = CAPABILITIES[tool.id]
477
+ if (!capability?.supported) return { id: tool.id, ready: false, reason: capability?.reason ?? '当前平台不支持托管执行。' }
478
+ try {
479
+ const directory = await checkedWorkspace(workspace)
480
+ const spec = await launchSpec(tool.id, directory, capability.modes[0], null)
481
+ return spec.unsupported
482
+ ? { id: tool.id, ready: false, reason: spec.unsupported }
483
+ : { id: tool.id, ready: true, source: spec.source, modes: capability.modes }
484
+ } catch (error) {
485
+ return { id: tool.id, ready: false, reason: String(error?.message ?? error).slice(0, 300) }
486
+ }
487
+ }
488
+
489
+ function tail(current, addition) {
490
+ const next = current + addition
491
+ return next.length > OUTPUT_TAIL_CHARS ? next.slice(-OUTPUT_TAIL_CHARS) : next
492
+ }
493
+
494
+ function stopProcessTree(child) {
495
+ if (child.pid === undefined) return null
496
+ if (IS_WINDOWS) {
497
+ try {
498
+ const killer = spawn(join(WINDOWS_SYSTEM32, 'taskkill.exe'), ['/PID', String(child.pid), '/T', '/F'], {
499
+ shell: false, windowsHide: true, stdio: 'ignore',
500
+ })
501
+ killer.on('error', () => { try { child.kill('SIGTERM') } catch { /* gone */ } })
502
+ killer.on('close', code => {
503
+ if (code !== 0) try { child.kill('SIGTERM') } catch { /* gone */ }
504
+ })
505
+ } catch { try { child.kill('SIGTERM') } catch { /* gone */ } }
506
+ return null
507
+ } else {
508
+ try { process.kill(-child.pid, 'SIGTERM') } catch { try { child.kill('SIGTERM') } catch { /* gone */ } }
509
+ const timer = setTimeout(() => {
510
+ try { process.kill(-child.pid, 'SIGKILL') } catch { /* gone */ }
511
+ }, 2_000)
512
+ return timer
513
+ }
514
+ }
515
+
516
+ function captureProcess(spec, task, workspace, signal, timeoutMs, toolId, sessionOnly = false) {
517
+ return new Promise(resolveResult => {
518
+ let child
519
+ try {
520
+ child = spawn(spec.file, spec.args, {
521
+ cwd: workspace,
522
+ shell: false,
523
+ windowsHide: true,
524
+ detached: !IS_WINDOWS,
525
+ stdio: ['pipe', 'pipe', 'pipe'],
526
+ env: executionEnvironment(toolId, { sessionOnly }),
527
+ })
528
+ } catch (error) {
529
+ resolveResult({ status: 'failed', error: String(error?.message ?? error), exitCode: null })
530
+ return
531
+ }
532
+
533
+ let finished = false
534
+ let stopReason = null
535
+ let stdout = ''
536
+ let stdoutTail = ''
537
+ let stderrTail = ''
538
+ let outputBytes = 0
539
+ let jsonlBuffer = ''
540
+ let codexUsageLine = ''
541
+ let reportedUsage = null
542
+ let terminal = null
543
+ let failedEvent = false
544
+ let finalText = ''
545
+ let protocolError = null
546
+ let zcodeResult = null
547
+ let zcodeSession = null
548
+ const zcodeStarted = new Set()
549
+ const zcodeCompleted = new Set()
550
+ const miniMaxParser = spec.format === 'minimax-stream-json' ? createMiniMaxStreamParser() : null
551
+ const nativeParser = spec.format === 'mimo-jsonl' || spec.format === 'grok-jsonl'
552
+ ? createMiMoGrokParser(toolId) : null
553
+ let kimiAssistant = false
554
+ const stdoutDecoder = new StringDecoder('utf8')
555
+ const stderrDecoder = new StringDecoder('utf8')
556
+
557
+ const readJsonlLine = line => {
558
+ if (!line.trim()) return
559
+ if (nativeParser) { nativeParser.push(line); return }
560
+ try {
561
+ const event = JSON.parse(line)
562
+ if (spec.format === 'zcode-stream-json') {
563
+ if (!event || typeof event !== 'object' || Array.isArray(event)
564
+ || typeof event.type !== 'string' || typeof event.sessionId !== 'string'
565
+ || !event.sessionId) {
566
+ protocolError = 'ZCode 返回了无效事件。'
567
+ return
568
+ }
569
+ if (zcodeResult) {
570
+ protocolError = 'ZCode 在最终结果之后仍返回事件。'
571
+ return
572
+ }
573
+ if (zcodeSession && zcodeSession !== event.sessionId) {
574
+ protocolError = 'ZCode 混入了其他会话的事件。'
575
+ return
576
+ }
577
+ zcodeSession = event.sessionId
578
+ if (event.type === 'result') {
579
+ zcodeResult = event
580
+ return
581
+ }
582
+ if (event.type === 'turn.failed') failedEvent = true
583
+ if (event.type === 'turn.started' || event.type === 'turn.completed') {
584
+ if (typeof event.turnId !== 'string' || !event.turnId) {
585
+ protocolError = 'ZCode 回合事件缺少 turnId。'
586
+ return
587
+ }
588
+ if (event.type === 'turn.started') zcodeStarted.add(event.turnId)
589
+ else if (event.payload?.resultType === 'success') zcodeCompleted.add(event.turnId)
590
+ else failedEvent = true
591
+ }
592
+ return
593
+ }
594
+ if (spec.format === 'kimi-stream-json') {
595
+ if (event.role === 'meta' && /error|failed/i.test(String(event.type ?? ''))) failedEvent = true
596
+ if (event.role === 'assistant' && !event.tool_calls?.length) {
597
+ const content = typeof event.content === 'string' ? event.content
598
+ : Array.isArray(event.content) ? event.content.filter(part => part?.type === 'text')
599
+ .map(part => String(part.text ?? '')).join('') : ''
600
+ if (content.trim()) { finalText = content; kimiAssistant = true }
601
+ }
602
+ return
603
+ }
604
+ if (event.type === 'turn.completed' && event.usage) codexUsageLine = line
605
+ if (event.type === 'turn.completed' && !failedEvent) terminal = 'completed'
606
+ // Top-level `error` events include transient reconnect notices; only turn.failed is terminal.
607
+ if (event.type === 'turn.failed') {
608
+ failedEvent = true
609
+ terminal = 'failed'
610
+ }
611
+ if (event.type === 'item.completed' && event.item?.type === 'agent_message'
612
+ && typeof event.item.text === 'string') finalText = event.item.text
613
+ } catch { protocolError = `${toolId} 输出了无效 JSONL。` }
614
+ }
615
+ const readJsonl = chunk => {
616
+ jsonlBuffer += chunk
617
+ for (;;) {
618
+ const newline = jsonlBuffer.indexOf('\n')
619
+ if (newline < 0) break
620
+ readJsonlLine(jsonlBuffer.slice(0, newline))
621
+ jsonlBuffer = jsonlBuffer.slice(newline + 1)
622
+ }
623
+ if (jsonlBuffer.length > 4_000_000) protocolError = `${toolId} JSONL 单行超过限制。`
624
+ }
625
+
626
+ let timeoutTimer
627
+ let graceTimer
628
+ let escalationTimer
629
+ const onAbort = () => requestStop('cancelled')
630
+ const settle = result => {
631
+ if (finished) return
632
+ finished = true
633
+ clearTimeout(timeoutTimer)
634
+ clearTimeout(graceTimer)
635
+ clearTimeout(escalationTimer)
636
+ signal?.removeEventListener('abort', onAbort)
637
+ resolveResult({ stdoutTail, stderrTail, terminalEvent: terminal, finalText, ...result })
638
+ }
639
+ function requestStop(reason) {
640
+ if (finished || stopReason) return
641
+ stopReason = reason
642
+ escalationTimer = stopProcessTree(child)
643
+ graceTimer = setTimeout(() => settle({ status: reason, exitCode: null, error: '进程终止等待超时。' }), STOP_GRACE_MS)
644
+ graceTimer.unref?.()
645
+ }
646
+
647
+ child.stdout.on('data', bytes => {
648
+ if (finished || stopReason) return
649
+ outputBytes += bytes.length
650
+ const chunk = stdoutDecoder.write(bytes)
651
+ stdoutTail = tail(stdoutTail, chunk)
652
+ if (spec.format === 'claude-json') stdout += chunk
653
+ else if (miniMaxParser) miniMaxParser.push(chunk)
654
+ else readJsonl(chunk)
655
+ if (outputBytes > MAX_OUTPUT_BYTES) requestStop('output-limit')
656
+ })
657
+ child.stderr.on('data', bytes => {
658
+ if (finished || stopReason) return
659
+ outputBytes += bytes.length
660
+ stderrTail = tail(stderrTail, stderrDecoder.write(bytes))
661
+ if (outputBytes > MAX_OUTPUT_BYTES) requestStop('output-limit')
662
+ })
663
+ child.stdin.on('error', () => { /* a process can exit before reading stdin */ })
664
+ child.on('error', error => settle({ status: stopReason ?? 'failed', exitCode: null, error: error.message }))
665
+ child.on('close', (code, exitSignal) => {
666
+ if (finished) return
667
+ const finalStdout = stdoutDecoder.end()
668
+ if (finalStdout) {
669
+ stdoutTail = tail(stdoutTail, finalStdout)
670
+ if (spec.format === 'claude-json') stdout += finalStdout
671
+ else if (miniMaxParser) miniMaxParser.push(finalStdout)
672
+ else readJsonl(finalStdout)
673
+ }
674
+ stderrTail = tail(stderrTail, stderrDecoder.end())
675
+ if (jsonlBuffer.trim()) readJsonlLine(jsonlBuffer)
676
+ if (stopReason) {
677
+ settle({ status: stopReason, exitCode: code, exitSignal })
678
+ return
679
+ }
680
+ if (miniMaxParser) {
681
+ const outcome = miniMaxParser.finish(code)
682
+ const reported = usageFromEvents('minimax-result', outcome)
683
+ settle({ ...outcome, usage: reported?.usage ?? null })
684
+ return
685
+ }
686
+ if (nativeParser) {
687
+ settle({ exitCode: code, ...nativeParser.finish(code) })
688
+ return
689
+ }
690
+ if (code !== 0) {
691
+ settle({ status: 'failed', exitCode: code, exitSignal, error: '官方 CLI 退出码非零。' })
692
+ return
693
+ }
694
+ if (spec.format === 'claude-json') {
695
+ try {
696
+ const result = JSON.parse(stdout)
697
+ if (result.type !== 'result' || result.is_error === true || result.subtype !== 'success'
698
+ || typeof result.result !== 'string') {
699
+ settle({ status: 'failed', exitCode: code, error: 'Claude 未返回成功终态。' })
700
+ return
701
+ }
702
+ terminal = 'completed'
703
+ finalText = result.result
704
+ reportedUsage = usageFromOutput('claude-json', stdout)
705
+ } catch {
706
+ settle({ status: 'failed', exitCode: code, error: 'Claude 未返回有效的结果 JSON。' })
707
+ return
708
+ }
709
+ } else if (spec.format === 'zcode-stream-json') {
710
+ const turnId = zcodeResult?.turnId
711
+ if (protocolError || failedEvent || !zcodeResult || typeof turnId !== 'string'
712
+ || !turnId || !zcodeStarted.has(turnId) || !zcodeCompleted.has(turnId)
713
+ || zcodeResult.projection?.status !== 'idle'
714
+ || typeof zcodeResult.response !== 'string' || !zcodeResult.response.trim()) {
715
+ settle({ status: 'failed', exitCode: code,
716
+ error: protocolError ?? 'ZCode 未返回匹配回合的成功终态和最终答复。' })
717
+ return
718
+ }
719
+ terminal = 'completed'
720
+ finalText = zcodeResult.response
721
+ } else if (spec.format === 'kimi-stream-json') {
722
+ if (protocolError || failedEvent || !kimiAssistant) {
723
+ settle({ status: 'failed', exitCode: code, error: protocolError ?? 'Kimi 未返回完整的助手答复。' })
724
+ return
725
+ }
726
+ terminal = 'completed'
727
+ } else if (protocolError || terminal !== 'completed' || !finalText.trim()) {
728
+ settle({ status: 'failed', exitCode: code, error: protocolError ?? 'Codex 未返回完整成功终态和回答。' })
729
+ return
730
+ }
731
+ if (!reportedUsage && codexUsageLine) reportedUsage = usageFromOutput('codex-jsonl', codexUsageLine)
732
+ settle({ status: 'succeeded', exitCode: code,
733
+ ...(reportedUsage?.usage ? { usage: reportedUsage.usage } : {}),
734
+ ...(reportedUsage?.reportedCostUsd ? { reportedCostUsd: reportedUsage.reportedCostUsd } : {}) })
735
+ })
736
+
737
+ timeoutTimer = setTimeout(() => requestStop('timed-out'), timeoutMs)
738
+ signal?.addEventListener('abort', onAbort, { once: true })
739
+ if (signal?.aborted) requestStop('cancelled')
740
+ if (!stopReason) child.stdin.end(spec.stdinTask === false ? '' : task, 'utf8')
741
+ })
742
+ }
743
+
744
+ /**
745
+ * Run one official CLI. The Host must authorize `workspace` against its
746
+ * session; for write mode, it must supply its own isolated workspace root and
747
+ * obtain the appropriate approval before calling this function. No arbitrary
748
+ * executable, shell command, or CLI flag can be supplied by the caller.
749
+ */
750
+ export async function runOfficialTool({
751
+ toolId, task, workspace, mode = 'read-only', isolatedRoot, modelId, signal, sandbox, timeoutMs = DEFAULT_TIMEOUT_MS,
752
+ sessionOnly = false,
753
+ }) {
754
+ const tool = getOfficialTool(toolId)
755
+ if (!tool) return { toolId, status: 'unsupported', reason: '未知的官方工具注册表 ID。' }
756
+ const capability = CAPABILITIES[tool.id]
757
+ if (!capability?.supported) return { toolId: tool.id, status: 'unsupported', reason: capability?.reason ?? '尚无执行适配器。' }
758
+ const prompt = checkedTask(task)
759
+ const requestedModel = checkedModel(modelId)
760
+ const cwd = await checkedWorkspace(workspace)
761
+ const executionMode = checkedMode(mode)
762
+ if (!capability.modes.includes(executionMode)) {
763
+ return { toolId: tool.id, status: 'unsupported', reason: `${tool.label} 的无界面模式会自动执行工具;当前仅允许经审批的独立 Git 工作区可编辑执行。` }
764
+ }
765
+ if (executionMode === 'workspace-write') await checkedWriteWorkspace(cwd, isolatedRoot)
766
+ const timeout = checkedTimeout(timeoutMs)
767
+ if (signal?.aborted) return { toolId: tool.id, status: 'cancelled', workspace: cwd }
768
+ const spec = await launchSpec(tool.id, cwd, executionMode, requestedModel, prompt)
769
+ if (spec.unsupported) return { toolId: tool.id, status: 'unsupported', reason: spec.unsupported }
770
+ if (typeof sandbox?.confine !== 'function') {
771
+ return { toolId: tool.id, status: 'unsupported', reason: '官方 Harness 进程沙箱不可用,拒绝直接启动 CLI。' }
772
+ }
773
+ let confined
774
+ try {
775
+ confined = await sandbox.confine([spec.file, ...spec.args], {
776
+ mode: executionMode, workspaceRoot: cwd,
777
+ }, signal)
778
+ } catch (error) {
779
+ return { toolId: tool.id, status: 'unsupported', reason: `官方进程沙箱拒绝启动:${String(error?.message ?? error).slice(0, 300)}` }
780
+ }
781
+ if (!Array.isArray(confined?.argv) || confined.argv.length < 2 || !['full', 'partial'].includes(confined.enforcement)) {
782
+ return { toolId: tool.id, status: 'unsupported', reason: '官方进程沙箱未返回有效的受限启动参数。' }
783
+ }
784
+ const startedAt = Date.now()
785
+ const outcome = await captureProcess({ ...spec, file: confined.argv[0], args: confined.argv.slice(1) },
786
+ prompt, cwd, signal, timeout, tool.id, sessionOnly === true)
787
+ return {
788
+ toolId: tool.id,
789
+ mode: executionMode,
790
+ requestedModel: spec.requestedModel === undefined ? requestedModel : spec.requestedModel,
791
+ ...(spec.modelNotice ? { modelNotice: spec.modelNotice } : {}),
792
+ workspace: cwd,
793
+ source: spec.source,
794
+ sandboxEnforcement: confined.enforcement,
795
+ startedAt,
796
+ finishedAt: Date.now(),
797
+ ...outcome,
798
+ }
799
+ }
800
+
801
+ export const runOfficialToolTask = runOfficialTool