@ljwei-stak/dsh-model-router 0.14.0-beta.3 → 0.14.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.
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Registry-attested file digests for unsigned files shipped in official npm
3
+ * packages (MiMo's mimo.exe, Grok's grok.exe.br, MiniMax's cli.js).
4
+ *
5
+ * These vendors do not Authenticode-sign their binaries, so a publisher check
6
+ * is impossible. Instead of a per-version hash compiled into the plugin, the
7
+ * expected digest is derived at runtime from the official npm registry for the
8
+ * exact installed version: the version document (over TLS from
9
+ * registry.npmjs.org) gives the tarball's sha512 `dist.integrity`; the tarball
10
+ * (from npm's local cache when present, otherwise downloaded from the same
11
+ * registry) must match it; the file is then hashed straight out of the tarball.
12
+ * Installed files must equal those digests. Results are cached per
13
+ * package@version, so only the first use of a new version needs network.
14
+ */
15
+ import { createHash } from 'node:crypto'
16
+ import { createReadStream } from 'node:fs'
17
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
18
+ import { homedir } from 'node:os'
19
+ import { dirname, join } from 'node:path'
20
+ import { Readable } from 'node:stream'
21
+ import { createGunzip } from 'node:zlib'
22
+ import { resolveStateHome } from './router-state.mjs'
23
+ import { NPM_REGISTRY, isReleaseVersion } from './latest-versions.mjs'
24
+
25
+ const PACKAGE_NAME = /^@[a-z0-9][a-z0-9._-]{0,100}\/[a-z0-9][a-z0-9._-]{0,100}$|^[a-z0-9][a-z0-9._-]{0,100}$/
26
+ const MAX_TARBALL_BYTES = 400_000_000
27
+ const METADATA_TIMEOUT_MS = 15_000
28
+ const TARBALL_TIMEOUT_MS = 180_000
29
+ /** Files that run as code: what the executor must not trust without attestation. */
30
+ export const CODE_FILE = /\.(?:js|cjs|mjs|node|exe|dll|br|wasm)$/i
31
+
32
+ export function attestationCachePath(env = process.env) {
33
+ return join(resolveStateHome(env), 'model-router', 'npm-attestations.json')
34
+ }
35
+
36
+ /** npm's content-addressed cache location for one sha512 integrity string. */
37
+ export function npmCacheContentPath(cacheDir, integrity) {
38
+ const match = /^sha512-([A-Za-z0-9+/]+={0,2})$/.exec(String(integrity ?? ''))
39
+ if (!match || !cacheDir) return null
40
+ const hex = Buffer.from(match[1], 'base64').toString('hex')
41
+ if (hex.length !== 128) return null
42
+ return join(cacheDir, '_cacache', 'content-v2', 'sha512', hex.slice(0, 2), hex.slice(2, 4), hex.slice(4))
43
+ }
44
+
45
+ export function defaultNpmCacheDirectory(env = process.env, platform = process.platform) {
46
+ const configured = env.npm_config_cache ?? env.NPM_CONFIG_CACHE
47
+ if (typeof configured === 'string' && configured.trim()) return configured.trim()
48
+ if (platform === 'win32') return env.LOCALAPPDATA ? join(env.LOCALAPPDATA, 'npm-cache') : null
49
+ return join(homedir(), '.npm')
50
+ }
51
+
52
+ /** Parse one 512-byte tar header; returns null for the end-of-archive block. */
53
+ function tarHeader(block) {
54
+ if (block.every(byte => byte === 0)) return null
55
+ const field = (start, length) => {
56
+ const raw = block.subarray(start, start + length)
57
+ const end = raw.indexOf(0)
58
+ return raw.subarray(0, end === -1 ? length : end).toString('utf8')
59
+ }
60
+ const sizeField = block.subarray(124, 136)
61
+ let size
62
+ if (sizeField[0] & 0x80) {
63
+ size = 0
64
+ for (let index = 1; index < 12; index += 1) size = size * 256 + sizeField[index]
65
+ } else {
66
+ size = Number.parseInt(field(124, 12).trim() || '0', 8)
67
+ }
68
+ const magic = field(257, 6)
69
+ const prefix = magic.startsWith('ustar') ? field(345, 155) : ''
70
+ const name = field(0, 100)
71
+ return { name: prefix ? `${prefix}/${name}` : name, size, type: String.fromCharCode(block[156] || 48) }
72
+ }
73
+
74
+ function paxPath(buffer) {
75
+ let offset = 0
76
+ while (offset < buffer.length) {
77
+ const space = buffer.indexOf(0x20, offset)
78
+ if (space === -1) break
79
+ const length = Number.parseInt(buffer.subarray(offset, space).toString('utf8'), 10)
80
+ if (!Number.isSafeInteger(length) || length <= 0) break
81
+ const record = buffer.subarray(space + 1, offset + length - 1).toString('utf8')
82
+ if (record.startsWith('path=')) return record.slice(5)
83
+ offset += length
84
+ }
85
+ return null
86
+ }
87
+
88
+ /**
89
+ * Stream a .tgz and return a Map of every regular member below `package/`
90
+ * (path relative to the package root) to { sha256: 'sha256-<base64>', size }.
91
+ */
92
+ export async function digestTarMembers(tgzStream) {
93
+ const gunzip = tgzStream.pipe(createGunzip())
94
+ const members = new Map()
95
+ let pending = Buffer.alloc(0)
96
+ let state = { kind: 'header' }
97
+ let nextName = null
98
+ for await (const chunk of gunzip) {
99
+ pending = pending.length ? Buffer.concat([pending, chunk]) : chunk
100
+ for (;;) {
101
+ if (state.kind === 'done') break
102
+ if (state.kind === 'header') {
103
+ if (pending.length < 512) break
104
+ const header = tarHeader(pending.subarray(0, 512))
105
+ pending = pending.subarray(512)
106
+ if (!header) { state = { kind: 'done' }; break }
107
+ const padded = Math.ceil(header.size / 512) * 512
108
+ const name = nextName ?? header.name
109
+ nextName = null
110
+ if (header.type === 'x' || header.type === 'L') {
111
+ state = { kind: 'meta', type: header.type, remaining: header.size, skip: padded - header.size, parts: [] }
112
+ } else if (header.type === '0' || header.type === '\0') {
113
+ state = { kind: 'data', name, remaining: header.size, skip: padded - header.size, hash: createHash('sha256'), size: header.size }
114
+ } else {
115
+ state = { kind: 'skip', remaining: padded }
116
+ }
117
+ continue
118
+ }
119
+ if (state.kind === 'meta' || state.kind === 'data') {
120
+ if (state.remaining > 0) {
121
+ if (pending.length === 0) break
122
+ const take = Math.min(state.remaining, pending.length)
123
+ const piece = pending.subarray(0, take)
124
+ if (state.kind === 'meta') state.parts.push(Buffer.from(piece)); else state.hash.update(piece)
125
+ state.remaining -= take
126
+ pending = pending.subarray(take)
127
+ if (state.remaining > 0) break
128
+ }
129
+ if (state.kind === 'meta') {
130
+ const body = Buffer.concat(state.parts)
131
+ nextName = state.type === 'x' ? paxPath(body) : body.toString('utf8').replace(/\0+$/, '')
132
+ } else {
133
+ // npm packs every file under one top-level directory (usually "package/").
134
+ const relativePath = state.name.replace(/^\.?\/?[^/]+\//, '')
135
+ if (relativePath && !relativePath.split('/').includes('..')) {
136
+ members.set(relativePath, { sha256: `sha256-${state.hash.digest('base64')}`, size: state.size })
137
+ }
138
+ }
139
+ state = { kind: 'skip', remaining: state.skip }
140
+ continue
141
+ }
142
+ if (state.kind === 'skip') {
143
+ const take = Math.min(state.remaining, pending.length)
144
+ state.remaining -= take
145
+ pending = pending.subarray(take)
146
+ if (state.remaining > 0) break
147
+ state = { kind: 'header' }
148
+ }
149
+ }
150
+ if (state.kind === 'done') break
151
+ }
152
+ gunzip.destroy()
153
+ return members
154
+ }
155
+
156
+ async function sha512Of(stream) {
157
+ const hash = createHash('sha512')
158
+ for await (const chunk of stream) hash.update(chunk)
159
+ return `sha512-${hash.digest('base64')}`
160
+ }
161
+
162
+ /** Digest of a local file in the same `sha256-<base64>` form. */
163
+ export async function localFileDigest(path) {
164
+ const hash = createHash('sha256')
165
+ let size = 0
166
+ for await (const chunk of createReadStream(path)) { hash.update(chunk); size += chunk.length }
167
+ return { sha256: `sha256-${hash.digest('base64')}`, size }
168
+ }
169
+
170
+ export function createNpmAttestor({
171
+ fetchImpl = (...args) => globalThis.fetch(...args),
172
+ cachePath = () => attestationCachePath(),
173
+ npmCacheDirectory = async () => defaultNpmCacheDirectory(),
174
+ now = Date.now,
175
+ persist = true,
176
+ } = {}) {
177
+ let entries = null
178
+ const inflight = new Map()
179
+
180
+ async function load() {
181
+ if (entries) return entries
182
+ entries = {}
183
+ if (!persist) return entries
184
+ try {
185
+ const saved = JSON.parse(await readFile(cachePath(), 'utf8'))
186
+ if (saved?.entries && typeof saved.entries === 'object') {
187
+ for (const [key, value] of Object.entries(saved.entries)) {
188
+ if (value && typeof value.files === 'object' && /^sha512-/.test(value.integrity ?? '')) entries[key] = value
189
+ }
190
+ }
191
+ } catch { /* empty */ }
192
+ return entries
193
+ }
194
+
195
+ async function save() {
196
+ if (!persist) return
197
+ try {
198
+ const path = cachePath()
199
+ await mkdir(dirname(path), { recursive: true })
200
+ const temporary = `${path}.${process.pid}.tmp`
201
+ await writeFile(temporary, `${JSON.stringify({ version: 1, entries }, null, 2)}\n`, 'utf8')
202
+ await rename(temporary, path)
203
+ } catch { /* memory cache still applies */ }
204
+ }
205
+
206
+ async function registryTarball(name, version) {
207
+ const response = await fetchImpl(`${NPM_REGISTRY}/${name}/${version}`, {
208
+ headers: { accept: 'application/json' }, signal: AbortSignal.timeout(METADATA_TIMEOUT_MS),
209
+ })
210
+ if (!response?.ok) throw new Error(`npm 元数据 HTTP ${response?.status ?? '失败'}`)
211
+ const document = await response.json()
212
+ const integrity = document?.dist?.integrity
213
+ const tarball = document?.dist?.tarball
214
+ if (document?.name !== name || document?.version !== version || !/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(integrity ?? '')) {
215
+ throw new Error('npm 元数据缺少 sha512 完整性信息')
216
+ }
217
+ const url = new URL(tarball)
218
+ if (url.protocol !== 'https:' || url.hostname !== 'registry.npmjs.org') throw new Error('npm tarball 不在官方 registry 上')
219
+ return { integrity, url: url.href }
220
+ }
221
+
222
+ async function verifiedTarballStream({ integrity, url }) {
223
+ const cached = npmCacheContentPath(await npmCacheDirectory().catch(() => null), integrity)
224
+ if (cached) {
225
+ try {
226
+ if (await sha512Of(createReadStream(cached)) === integrity) return createReadStream(cached)
227
+ } catch { /* not in npm's cache */ }
228
+ }
229
+ const response = await fetchImpl(url, { signal: AbortSignal.timeout(TARBALL_TIMEOUT_MS) })
230
+ if (!response?.ok || !response.body) throw new Error(`npm tarball HTTP ${response?.status ?? '失败'}`)
231
+ const chunks = []
232
+ let length = 0
233
+ const hash = createHash('sha512')
234
+ for await (const chunk of Readable.fromWeb(response.body)) {
235
+ length += chunk.length
236
+ if (length > MAX_TARBALL_BYTES) throw new Error('npm tarball 超出大小上限')
237
+ hash.update(chunk)
238
+ chunks.push(chunk)
239
+ }
240
+ if (`sha512-${hash.digest('base64')}` !== integrity) throw new Error('npm tarball 与 registry 完整性值不符')
241
+ return Readable.from([Buffer.concat(chunks)])
242
+ }
243
+
244
+ /** Every file of name@version → { sha256, size }, attested by the npm registry. */
245
+ async function attestPackage(name, version) {
246
+ if (!PACKAGE_NAME.test(String(name)) || !isReleaseVersion(version)) throw new TypeError('无效的 npm 包或版本')
247
+ const key = `${name}@${version}`
248
+ const cache = await load()
249
+ if (cache[key]) return cache[key]
250
+ if (inflight.has(key)) return inflight.get(key)
251
+ const pending = (async () => {
252
+ try {
253
+ const source = await registryTarball(name, version)
254
+ const members = await digestTarMembers(await verifiedTarballStream(source))
255
+ if (members.size === 0) throw new Error(`官方 npm 包 ${key} 为空`)
256
+ cache[key] = { integrity: source.integrity, attestedAt: now(), files: Object.fromEntries(members) }
257
+ await save()
258
+ return cache[key]
259
+ } finally {
260
+ inflight.delete(key)
261
+ }
262
+ })()
263
+ inflight.set(key, pending)
264
+ return pending
265
+ }
266
+
267
+ async function attest(name, version, path) {
268
+ if (typeof path !== 'string' || !path || path.split('/').includes('..')) throw new TypeError('无效的文件路径')
269
+ const entry = (await attestPackage(name, version)).files[path]
270
+ if (!entry) throw new Error(`官方 npm 包 ${name}@${version} 中没有 ${path}`)
271
+ return entry
272
+ }
273
+
274
+ return {
275
+ attest,
276
+ attestPackage,
277
+ /** Does one local file equal the registry-attested member? Never throws. */
278
+ async matches(localPath, name, version, path) {
279
+ try {
280
+ const expected = await attest(name, version, path)
281
+ const actual = await localFileDigest(localPath)
282
+ return actual.size === expected.size && actual.sha256 === expected.sha256
283
+ ? { ok: true, expected }
284
+ : { ok: false, reason: `本地文件与官方 npm ${name}@${version} 的 ${path} 摘要不符` }
285
+ } catch (error) {
286
+ return { ok: false, reason: String(error?.message ?? error).slice(0, 200) }
287
+ }
288
+ },
289
+ /**
290
+ * Compare every attested member selected by `include` (default: code files)
291
+ * with the installed package directory. Missing or different files fail.
292
+ */
293
+ async matchesPackage(packageRoot, name, version, include = CODE_FILE) {
294
+ try {
295
+ const { files } = await attestPackage(name, version)
296
+ const selected = Object.entries(files).filter(([path]) => include.test(path))
297
+ if (selected.length === 0) return { ok: false, reason: `官方 npm 包 ${name}@${version} 没有可核验的代码文件` }
298
+ for (const [path, expected] of selected) {
299
+ let actual
300
+ try { actual = await localFileDigest(join(packageRoot, ...path.split('/'))) }
301
+ catch { return { ok: false, reason: `已安装的 ${name}@${version} 缺少 ${path}` } }
302
+ if (actual.size !== expected.size || actual.sha256 !== expected.sha256) {
303
+ return { ok: false, reason: `已安装的 ${name}@${version} 中 ${path} 与官方 npm 发行文件不符` }
304
+ }
305
+ }
306
+ return { ok: true, files: selected.length }
307
+ } catch (error) {
308
+ return { ok: false, reason: String(error?.message ?? error).slice(0, 200) }
309
+ }
310
+ },
311
+ reset() { entries = null; inflight.clear() },
312
+ }
313
+ }
314
+
315
+ export const npmAttestor = createNpmAttestor()
@@ -14,7 +14,6 @@
14
14
  * https://moonshotai.github.io/kimi-code/en/reference/kimi-command.html
15
15
  */
16
16
  import { spawn } from 'node:child_process'
17
- import { createHash } from 'node:crypto'
18
17
  import { access, readFile, realpath, stat } from 'node:fs/promises'
19
18
  import { constants } from 'node:fs'
20
19
  import { StringDecoder } from 'node:string_decoder'
@@ -23,6 +22,8 @@ import { getOfficialTool, OFFICIAL_TOOLS } from './official-tool-registry.mjs'
23
22
  import { ensureNpmPrefixOnPath } from './official-tools-runtime.mjs'
24
23
  import { buildMiniMaxInvocation, createMiniMaxStreamParser } from './vendor-minimax-adapter.mjs'
25
24
  import { discoverZCodeBundle } from './zcode-bundle.mjs'
25
+ import { npmAttestor } from './npm-attestation.mjs'
26
+ import { compareReleaseVersions, isReleaseVersion } from './latest-versions.mjs'
26
27
  import { resolveMiMoGrokLaunch, createMiMoGrokParser } from './vendor-mimo-grok-adapter.mjs'
27
28
  import { emptyOutputError, outputIsEmpty, usageFromEvents, usageFromOutput } from './task-executors.mjs'
28
29
 
@@ -36,9 +37,6 @@ const STOP_GRACE_MS = 5_000
36
37
  const CLAUDE_MIN_RESTRICTED_VERSION = [2, 1, 259]
37
38
  const CLAUDE_SIGNER = 'Anthropic, PBC'
38
39
  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
40
  const WINDOWS_SYSTEM32 = join(process.env.SystemRoot ?? 'C:\\Windows', 'System32')
43
41
  const ENVIRONMENT_KEYS = Object.freeze([
44
42
  'PATH', 'Path', 'PATHEXT', 'SystemRoot', 'windir', 'ComSpec',
@@ -201,19 +199,23 @@ async function findGlobalPackageEntries(packageName, binName, expectedBin, works
201
199
  return found
202
200
  }
203
201
 
204
- /** The official MiniMax Windows installer uses a versioned releases tree rather than npm -g. */
202
+ /**
203
+ * The official MiniMax Windows installer uses a versioned releases tree rather
204
+ * than npm -g. Any release named by its `current` file is accepted, but every
205
+ * code file of that release must equal the official npm tarball of the same
206
+ * version (MiniMax does not sign its CLI, so the npm registry is the authority).
207
+ */
205
208
  export async function findManagedMiniMaxEntry(workspace,
206
209
  directories = pathDirectories().flatMap(directory => [directory, join(directory, '.minimax-code')]),
207
- trustedCliSha256 = MINIMAX_055_CLI_SHA256) {
210
+ attestor = npmAttestor) {
208
211
  if (!IS_WINDOWS) return null
209
- const version = getOfficialTool('minimax-code').version
210
212
  for (const directory of directories) {
211
213
  try {
212
214
  const root = await realpath(directory)
213
215
  if (root === workspace || inside(workspace, root)) continue
214
216
  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 version = (await readFile(join(root, 'current'), 'utf8')).trim()
218
+ if (!isReleaseVersion(version)) continue
217
219
  const releaseRoot = await realpath(join(root, 'releases', version))
218
220
  if (!inside(root, releaseRoot)) continue
219
221
  const packageRoot = await realpath(join(releaseRoot, 'node_modules', '@minimax-ai', 'code'))
@@ -223,17 +225,22 @@ export async function findManagedMiniMaxEntry(workspace,
223
225
  || String(manifest.bin?.mcode).replace(/^\.\//, '') !== 'cli.js') continue
224
226
  const entry = await realpath(join(packageRoot, 'cli.js'))
225
227
  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 attested = await attestor.matchesPackage(packageRoot, '@minimax-ai/code', version)
229
+ if (!attested.ok) continue
228
230
  const native = await realpath(join(packageRoot, 'node_modules', 'better-sqlite3',
229
231
  'build', 'Release', 'better_sqlite3.node'))
230
232
  if (!inside(packageRoot, native) || !(await stat(native)).isFile()) continue
231
- return { entry, packageRoot, version, source: 'verified-official-windows-installer-bundle' }
233
+ return { entry, packageRoot, version, source: 'official-windows-installer-npm-attested' }
232
234
  } catch { /* inspect the next PATH directory */ }
233
235
  }
234
236
  return null
235
237
  }
236
238
 
239
+ /** Newest installed copy first; versions are not pinned. */
240
+ function newestFirst(entries) {
241
+ return [...entries].sort((left, right) => (compareReleaseVersions(right.version, left.version) ?? 0))
242
+ }
243
+
237
244
  async function findCodexExecutable(workspace) {
238
245
  await ensureNpmPrefixOnPath()
239
246
  const filename = IS_WINDOWS ? 'codex.exe' : 'codex'
@@ -304,6 +311,17 @@ async function findSignedPackagedCodex(packageRoot, workspace) {
304
311
  } catch { return null }
305
312
  }
306
313
 
314
+ /**
315
+ * Publisher-only acceptance: a valid Authenticode signature whose signer is the
316
+ * vendor. No version or file hash is involved, so newer releases are accepted.
317
+ */
318
+ export function signatureAccepted(signature, expectedSigner) {
319
+ return signature?.status === 'Valid' && typeof expectedSigner === 'string' && expectedSigner.length > 0
320
+ && String(signature.subject ?? '').includes(expectedSigner)
321
+ }
322
+
323
+ export const OFFICIAL_SIGNERS = Object.freeze({ codex: CODEX_SIGNER, 'claude-code': CLAUDE_SIGNER })
324
+
307
325
  /** PATH or package metadata alone is insufficient: verify the Windows EXE publisher. */
308
326
  function hasOfficialWindowsSignature(entry, expectedSigner) {
309
327
  // Some launch environments inherit a PowerShell module path where the
@@ -342,7 +360,7 @@ function hasOfficialWindowsSignature(entry, expectedSigner) {
342
360
  if (code !== 0) { settle(false); return }
343
361
  try {
344
362
  const signature = JSON.parse(output.trim().replace(/^\uFEFF/, ''))
345
- settle(signature.status === 'Valid' && String(signature.subject).includes(expectedSigner))
363
+ settle(signatureAccepted(signature, expectedSigner))
346
364
  } catch { settle(false) }
347
365
  })
348
366
  })
@@ -402,8 +420,8 @@ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readin
402
420
  if (toolId === 'kimi-code') {
403
421
  const tool = getOfficialTool(toolId)
404
422
  const entries = await findGlobalPackageEntries(tool.package, 'kimi', 'dist/main.mjs', workspace)
405
- const found = entries.find(entry => entry.version === tool.version)
406
- if (!found) return { unsupported: `未找到固定版本 ${tool.package}@${tool.version} 的官方入口。` }
423
+ const found = newestFirst(entries)[0]
424
+ if (!found) return { unsupported: `未找到官方 npm 包 ${tool.package} 的 kimi 入口(dist/main.mjs)。` }
407
425
  const nodeExecutable = await findNodeExecutable(workspace)
408
426
  if (!nodeExecutable) return { unsupported: '未找到工作区外的 Node.js 可执行文件。' }
409
427
  if (Buffer.byteLength(task, 'utf8') > 16_000) {
@@ -419,9 +437,8 @@ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readin
419
437
  if (toolId === 'minimax-code') {
420
438
  const tool = getOfficialTool(toolId)
421
439
  const entries = await findGlobalPackageEntries(tool.package, 'mcode', 'cli.js', workspace)
422
- const found = entries.find(entry => entry.version === tool.version)
423
- ?? await findManagedMiniMaxEntry(workspace)
424
- if (!found) return { unsupported: `未找到固定版本 ${tool.package}@${tool.version} 的官方入口。` }
440
+ const found = newestFirst(entries)[0] ?? await findManagedMiniMaxEntry(workspace)
441
+ if (!found) return { unsupported: `未找到官方 npm 包 ${tool.package} 的 mcode 入口(cli.js)。` }
425
442
  const nodeExecutable = await findNodeExecutable(workspace, [22, 19, 0], version => {
426
443
  const major = Number(String(version).split('.')[0])
427
444
  return major === 22 || (major >= 24 && major < 27)
@@ -444,7 +461,7 @@ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readin
444
461
  }
445
462
  if (toolId === 'zcode') {
446
463
  const found = await discoverZCodeBundle()
447
- if (!found) return { unsupported: '未找到已签名、版本匹配且 CLI 脚本哈希正确的 ZCode 桌面版。' }
464
+ if (!found) return { unsupported: '未找到具有智谱有效签名、且 CLI 脚本与首次核验记录一致的 ZCode 桌面版。' }
448
465
  const nodeExecutable = await findNodeExecutable(workspace, [24, 14, 0], version =>
449
466
  Number(String(version).split('.')[0]) === 24)
450
467
  if (!nodeExecutable) return { unsupported: '未找到工作区外的 Node.js 可执行文件。' }
@@ -458,7 +475,7 @@ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readin
458
475
  format: 'zcode-stream-json', source: found.source, stdinTask: false,
459
476
  requestedModel: null,
460
477
  modelNotice: modelId
461
- ? 'ZCode 3.14.3 的 CLI 没有 --model 参数;本次使用 ZCode 已配置的默认模型,不能保证与 Harness 建议模型一致。'
478
+ ? 'ZCode 的 CLI 没有 --model 参数;本次使用 ZCode 已配置的默认模型,不能保证与 Harness 建议模型一致。'
462
479
  : '本次使用 ZCode 已配置的默认模型。',
463
480
  }
464
481
  }
@@ -3,9 +3,12 @@
3
3
  * (probe + install) and the Desktop panel (display). Pure data: no Node or
4
4
  * browser APIs, so both sides and the test suite import it unchanged.
5
5
  *
6
- * The registry is fail-closed by design: versions and installer sources are
7
- * pinned to the releases checked for this registry, and clients can only name a
8
- * registry id — never an arbitrary package or command.
6
+ * The registry is fail-closed by design: installer sources are fixed (the
7
+ * official npm registry, the vendor's signed installer) and clients can only
8
+ * name a registry id — never an arbitrary package or command. Versions are not
9
+ * pinned: installs take each vendor's latest release (`@latest`), and the
10
+ * executor trusts entries by publisher signature or registry-attested digest
11
+ * rather than by version.
9
12
  */
10
13
 
11
14
  export const OFFICIAL_TOOLS = Object.freeze([
@@ -15,9 +18,8 @@ export const OFFICIAL_TOOLS = Object.freeze([
15
18
  vendor: 'Moonshot AI',
16
19
  purpose: 'Kimi 官方编程 CLI,提供 kimi 命令与 ACP 会话。',
17
20
  package: '@moonshot-ai/kimi-code',
18
- version: '2.1.1',
19
21
  manager: 'npm',
20
- installArgs: ['install', '-g', '@moonshot-ai/kimi-code@2.1.1', '--registry=https://registry.npmjs.org/'],
22
+ installArgs: ['install', '-g', '@moonshot-ai/kimi-code@latest', '--registry=https://registry.npmjs.org/'],
21
23
  probeExecutables: ['kimi'],
22
24
  probeNote: 'kimi 与旧 Python 版 kimi-cli 同名;请核对可执行文件来源和版本。',
23
25
  providerHints: ['moonshot', 'kimi'],
@@ -28,9 +30,8 @@ export const OFFICIAL_TOOLS = Object.freeze([
28
30
  vendor: 'Anthropic',
29
31
  purpose: 'Anthropic 官方编程 CLI,提供 claude 命令。',
30
32
  package: '@anthropic-ai/claude-code',
31
- version: '2.1.283',
32
33
  manager: 'npm',
33
- installArgs: ['install', '-g', '@anthropic-ai/claude-code@2.1.283', '--registry=https://registry.npmjs.org/'],
34
+ installArgs: ['install', '-g', '@anthropic-ai/claude-code@latest', '--registry=https://registry.npmjs.org/'],
34
35
  probeExecutables: ['claude'],
35
36
  providerHints: ['anthropic', 'claude'],
36
37
  }),
@@ -40,11 +41,10 @@ export const OFFICIAL_TOOLS = Object.freeze([
40
41
  vendor: 'OpenAI',
41
42
  purpose: 'OpenAI 官方编程 CLI,提供 codex 命令。',
42
43
  package: '@openai/codex',
43
- version: '0.157.1',
44
- installArgs: ['install', '-g', '@openai/codex@0.157.1', '--registry=https://registry.npmjs.org/'],
44
+ installArgs: ['install', '-g', '@openai/codex@latest', '--registry=https://registry.npmjs.org/'],
45
45
  manager: 'npm',
46
46
  probeExecutables: ['codex'],
47
- probeNote: 'Codex 版本横幅由适配层宽匹配;安装时固定版本。',
47
+ probeNote: 'Codex 版本横幅由适配层宽匹配;安装时取 npm 最新版。',
48
48
  providerHints: ['openai', 'gpt', 'codex'],
49
49
  }),
50
50
  Object.freeze({
@@ -53,9 +53,8 @@ export const OFFICIAL_TOOLS = Object.freeze([
53
53
  vendor: 'MiniMax',
54
54
  purpose: 'MiniMax 官方编程 CLI,提供 mcode 命令。',
55
55
  package: '@minimax-ai/code',
56
- version: '0.5.5',
57
56
  manager: 'npm',
58
- installArgs: ['install', '-g', '@minimax-ai/code@0.5.5', '--registry=https://registry.npmjs.org/', '--ignore-scripts=false', '--include=optional', '--allow-scripts=@minimax-ai/code,better-sqlite3'],
57
+ installArgs: ['install', '-g', '@minimax-ai/code@latest', '--registry=https://registry.npmjs.org/', '--ignore-scripts=false', '--include=optional', '--allow-scripts=@minimax-ai/code,better-sqlite3'],
59
58
  probeExecutables: ['mcode'],
60
59
  providerHints: ['minimax'],
61
60
  }),
@@ -65,9 +64,8 @@ export const OFFICIAL_TOOLS = Object.freeze([
65
64
  vendor: 'XiaoMi',
66
65
  purpose: '小米 MiMo 官方编程 CLI,提供 mimo 命令。',
67
66
  package: '@mimo-ai/cli',
68
- version: '0.1.15',
69
67
  manager: 'npm',
70
- installArgs: ['install', '-g', '@mimo-ai/cli@0.1.15', '--registry=https://registry.npmjs.org/'],
68
+ installArgs: ['install', '-g', '@mimo-ai/cli@latest', '--registry=https://registry.npmjs.org/'],
71
69
  probeExecutables: ['mimo'],
72
70
  providerHints: ['mimo', 'xiaomi'],
73
71
  }),
@@ -77,9 +75,8 @@ export const OFFICIAL_TOOLS = Object.freeze([
77
75
  vendor: 'xAI',
78
76
  purpose: 'xAI 官方编程 CLI,提供 grok 命令与 ACP 会话。',
79
77
  package: '@xai-official/grok',
80
- version: '1.0.41',
81
78
  manager: 'npm',
82
- installArgs: ['install', '-g', '@xai-official/grok@1.0.41', '--registry=https://registry.npmjs.org/'],
79
+ installArgs: ['install', '-g', '@xai-official/grok@latest', '--registry=https://registry.npmjs.org/'],
83
80
  probeExecutables: ['grok'],
84
81
  providerHints: ['xai', 'grok'],
85
82
  }),
@@ -89,9 +86,8 @@ export const OFFICIAL_TOOLS = Object.freeze([
89
86
  vendor: 'Google',
90
87
  purpose: 'Google 官方 Gemini CLI,无界面模式使用 gemini -p。',
91
88
  package: '@google/gemini-cli',
92
- version: '0.62.0',
93
89
  manager: 'npm',
94
- installArgs: ['install', '-g', '@google/gemini-cli@0.62.0', '--registry=https://registry.npmjs.org/'],
90
+ installArgs: ['install', '-g', '@google/gemini-cli@latest', '--registry=https://registry.npmjs.org/'],
95
91
  probeExecutables: ['gemini'],
96
92
  providerHints: ['gemini', 'google'],
97
93
  // Headless runs go through the task adapter. The signed sandbox runner does
@@ -103,7 +99,6 @@ export const OFFICIAL_TOOLS = Object.freeze([
103
99
  label: 'ZCode',
104
100
  vendor: 'Z.ai',
105
101
  purpose: '智谱官方 ZCode 桌面版,内含 GLM 编程代理。Windows 安装器可选择 D 盘目录。',
106
- version: '3.14.3',
107
102
  manager: 'signed-windows-installer',
108
103
  installArgs: [],
109
104
  probeExecutables: [],