fix: skip repeated OpenCode binary validation
OpenCode startup on Linux ARM could run a --version probe before launching the configured Runtime and recheck registered remote binary metadata. Execution now trusts the selected path and registered manifest identity after setup-time verification, so switching and cold launch avoid duplicate binary work. Release note: 优化 Linux ARM 上切换 OpenCode 的启动速度;已安装或已登记的 Runtime 在执行时不再重复启动或检查二进制。
This commit is contained in:
@@ -81,7 +81,9 @@ Detached GoodBuddy Agent
|
||||
- Host 管理独占 Agent/Runtime 的包准备、更新和完整 payload 验证。已有项目的打开和切换
|
||||
只更新本地项目选择。Workspace 和执行路径按需读取 Host current registry,
|
||||
执行固定 `attach-or-bootstrap` 并复用当前 Agent 连接;同一进程内复用已确认 identity。
|
||||
它们不取得安装包、不发布组件,也不通过 SFTP 重读或哈希 payload。
|
||||
它们不取得安装包、不发布组件,也不通过 SFTP 重读或哈希 payload;已登记 Runtime
|
||||
的能力读取和 Prompt 启动只核对 registry 与 manifest 元数据,不再次执行、读取或检查
|
||||
OpenCode 二进制。
|
||||
- Agent 的固定 attach/按需启动命令只读取 Host 管理已提交的 installation registry 和
|
||||
与其 digest 匹配的有界 manifest 元数据,不在每次项目切换时重新验签或哈希完整 payload。
|
||||
完整验签、payload 扫描和 registry 写入仍只发生在显式 Host 准备/更新流程。
|
||||
|
||||
@@ -213,11 +213,8 @@ describe('Remote Runtime bundle verifier', () => {
|
||||
Buffer.from('changed')
|
||||
])
|
||||
)
|
||||
await expect(capabilities()).resolves.toEqual([])
|
||||
expect(reportError).toHaveBeenCalledWith(
|
||||
'Verified Runtime unavailable: opencode',
|
||||
expect.any(Error)
|
||||
)
|
||||
await expect(capabilities()).resolves.toHaveLength(1)
|
||||
expect(reportError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('adopts signed published metadata without rehashing unrelated payload', async () => {
|
||||
|
||||
@@ -678,6 +678,7 @@ async function defaultDetectBinary(
|
||||
return detectRuntimeBinary({
|
||||
binaryPath: configuredPath,
|
||||
bundledPath,
|
||||
validation: 'path-only',
|
||||
binaryNames: [runtime],
|
||||
label: 'OpenCode CLI'
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { realpath } from 'node:fs/promises'
|
||||
import { basename, dirname } from 'node:path'
|
||||
import { basename, dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
@@ -93,6 +93,29 @@ describe('runtime discovery', () => {
|
||||
expect(detection.version).toMatch(/^\d+\.\d+\.\d+/u)
|
||||
})
|
||||
|
||||
it('trusts a configured execution path without reading or launching it', async () => {
|
||||
process.env.PATH = ''
|
||||
process.env.Path = ''
|
||||
const candidate = resolve(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
'runtime-that-does-not-exist'
|
||||
)
|
||||
|
||||
const detection = await detectRuntimeBinary({
|
||||
binaryPath: candidate,
|
||||
validation: 'path-only',
|
||||
binaryNames: ['binary-that-does-not-exist'],
|
||||
label: 'Managed Runtime'
|
||||
})
|
||||
|
||||
expect(detection).toEqual({
|
||||
available: true,
|
||||
path: candidate,
|
||||
source: 'configured',
|
||||
detail: 'Managed Runtime 已就绪'
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects relative configured paths without resolving them from cwd', async () => {
|
||||
process.env.PATH = ''
|
||||
process.env.Path = ''
|
||||
|
||||
@@ -25,6 +25,7 @@ const VERSION_TERMINATION_WAIT_MS = 500
|
||||
export type RuntimeBinaryDiscoveryInput = {
|
||||
binaryPath: string
|
||||
bundledPath?: string
|
||||
validation?: 'execute' | 'canonical-file' | 'path-only'
|
||||
bundledValidation?: 'execute' | 'canonical-file'
|
||||
bundledVersion?: string
|
||||
allowAutomaticDiscovery?: boolean
|
||||
@@ -333,11 +334,24 @@ export async function detectRuntimeBinary(
|
||||
if (!bundledPath) {
|
||||
return undefined
|
||||
}
|
||||
if (input.validation === 'path-only') {
|
||||
return isAbsolute(bundledPath)
|
||||
? availableDetection(
|
||||
input.label,
|
||||
normalize(bundledPath),
|
||||
input.bundledVersion,
|
||||
'bundled'
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
const canonicalPath = await canonicalFile(bundledPath)
|
||||
if (!canonicalPath) {
|
||||
return undefined
|
||||
}
|
||||
if (input.bundledValidation === 'canonical-file') {
|
||||
if (
|
||||
(input.bundledValidation ?? input.validation) ===
|
||||
'canonical-file'
|
||||
) {
|
||||
return availableDetection(
|
||||
input.label,
|
||||
canonicalPath,
|
||||
@@ -359,10 +373,24 @@ export async function detectRuntimeBinary(
|
||||
if (configuredPath) {
|
||||
if (!isAbsolute(configuredPath)) {
|
||||
configuredPathProblem = 'relative'
|
||||
} else if (input.validation === 'path-only') {
|
||||
return availableDetection(
|
||||
input.label,
|
||||
normalize(configuredPath),
|
||||
undefined,
|
||||
'configured'
|
||||
)
|
||||
} else {
|
||||
const canonicalPath = await canonicalFile(configuredPath)
|
||||
if (!canonicalPath) {
|
||||
configuredPathProblem = 'invalid'
|
||||
} else if (input.validation === 'canonical-file') {
|
||||
return availableDetection(
|
||||
input.label,
|
||||
canonicalPath,
|
||||
undefined,
|
||||
'configured'
|
||||
)
|
||||
} else {
|
||||
const validation = await validateRuntimeVersion(canonicalPath)
|
||||
if (validation.valid) {
|
||||
@@ -391,6 +419,15 @@ export async function detectRuntimeBinary(
|
||||
continue
|
||||
}
|
||||
foundAutomaticCandidate = true
|
||||
if (
|
||||
input.validation === 'canonical-file' ||
|
||||
input.validation === 'path-only'
|
||||
) {
|
||||
return availableDetection(
|
||||
input.label,
|
||||
canonicalPath
|
||||
)
|
||||
}
|
||||
const validation = await validateRuntimeVersion(canonicalPath)
|
||||
if (validation.valid) {
|
||||
return availableDetection(
|
||||
|
||||
@@ -233,7 +233,7 @@ export async function verifyPublishedRuntimeBundle(
|
||||
/**
|
||||
* Reconstructs a Runtime identity that was fully verified when it was
|
||||
* registered. Normal capabilities and prompt launches use this bounded
|
||||
* metadata path instead of hashing the complete Runtime payload again.
|
||||
* metadata path instead of re-verifying or executing the Runtime binary.
|
||||
*/
|
||||
export async function loadRegisteredRuntimeBundle(
|
||||
bundleDirectoryInput: string,
|
||||
@@ -242,12 +242,50 @@ export async function loadRegisteredRuntimeBundle(
|
||||
const registered = runtimeRegistryEntrySchema.parse(
|
||||
options.registered
|
||||
)
|
||||
const verified = await verifyPublishedRuntimeBundle(
|
||||
bundleDirectoryInput,
|
||||
const bundleDirectory = assertAbsoluteManagedPath(
|
||||
resolve(bundleDirectoryInput)
|
||||
)
|
||||
const rootStat = await lstat(bundleDirectory)
|
||||
assertOwnedDirectory(
|
||||
rootStat,
|
||||
'Runtime bundle directory',
|
||||
options.uid
|
||||
)
|
||||
if (
|
||||
shouldEnforceMode(options) &&
|
||||
modeString(rootStat.mode) !== '0700'
|
||||
) {
|
||||
throw new Error('Runtime bundle directory permissions must be 0700')
|
||||
}
|
||||
const manifestBytes = await readMetadataFile(
|
||||
join(bundleDirectory, MANIFEST_FILE_NAME),
|
||||
'Runtime manifest',
|
||||
options
|
||||
)
|
||||
const { manifest, manifestDigest } = verified
|
||||
let value: unknown
|
||||
try {
|
||||
value = JSON.parse(manifestBytes.toString('utf8'))
|
||||
} catch (error) {
|
||||
throw new Error('Runtime manifest is invalid JSON', {
|
||||
cause: error
|
||||
})
|
||||
}
|
||||
const manifest = remoteRuntimeBundleManifestSchema.parse(value)
|
||||
if (!canonicalRuntimeManifestBytes(manifest).equals(manifestBytes)) {
|
||||
throw new Error(
|
||||
'Runtime manifest is not in canonical deterministic form'
|
||||
)
|
||||
}
|
||||
assertRuntimeManifestMatchesLock(
|
||||
manifest,
|
||||
remoteRuntimeLockSchema.parse(options.runtimeLock),
|
||||
options.architecture
|
||||
)
|
||||
const manifestDigest =
|
||||
await digestRemoteRuntimeBundleManifest(manifest)
|
||||
if (
|
||||
basename(bundleDirectory) !==
|
||||
registered.bundleDigest.slice('sha256:'.length) ||
|
||||
manifest.runtimeId !== registered.runtimeId ||
|
||||
manifest.runtimeVersion !== registered.runtimeVersion ||
|
||||
manifest.architecture !== registered.architecture ||
|
||||
@@ -265,7 +303,15 @@ export async function loadRegisteredRuntimeBundle(
|
||||
)
|
||||
}
|
||||
|
||||
return verified
|
||||
return {
|
||||
bundleDirectory,
|
||||
executablePath: join(
|
||||
bundleDirectory,
|
||||
...manifest.entrypoint.path.split('/')
|
||||
),
|
||||
manifest,
|
||||
manifestDigest
|
||||
}
|
||||
}
|
||||
|
||||
export function canonicalRuntimeManifestBytes(
|
||||
|
||||
Reference in New Issue
Block a user