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 验证。已有项目的打开和切换
|
- Host 管理独占 Agent/Runtime 的包准备、更新和完整 payload 验证。已有项目的打开和切换
|
||||||
只更新本地项目选择。Workspace 和执行路径按需读取 Host current registry,
|
只更新本地项目选择。Workspace 和执行路径按需读取 Host current registry,
|
||||||
执行固定 `attach-or-bootstrap` 并复用当前 Agent 连接;同一进程内复用已确认 identity。
|
执行固定 `attach-or-bootstrap` 并复用当前 Agent 连接;同一进程内复用已确认 identity。
|
||||||
它们不取得安装包、不发布组件,也不通过 SFTP 重读或哈希 payload。
|
它们不取得安装包、不发布组件,也不通过 SFTP 重读或哈希 payload;已登记 Runtime
|
||||||
|
的能力读取和 Prompt 启动只核对 registry 与 manifest 元数据,不再次执行、读取或检查
|
||||||
|
OpenCode 二进制。
|
||||||
- Agent 的固定 attach/按需启动命令只读取 Host 管理已提交的 installation registry 和
|
- Agent 的固定 attach/按需启动命令只读取 Host 管理已提交的 installation registry 和
|
||||||
与其 digest 匹配的有界 manifest 元数据,不在每次项目切换时重新验签或哈希完整 payload。
|
与其 digest 匹配的有界 manifest 元数据,不在每次项目切换时重新验签或哈希完整 payload。
|
||||||
完整验签、payload 扫描和 registry 写入仍只发生在显式 Host 准备/更新流程。
|
完整验签、payload 扫描和 registry 写入仍只发生在显式 Host 准备/更新流程。
|
||||||
|
|||||||
@@ -213,11 +213,8 @@ describe('Remote Runtime bundle verifier', () => {
|
|||||||
Buffer.from('changed')
|
Buffer.from('changed')
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
await expect(capabilities()).resolves.toEqual([])
|
await expect(capabilities()).resolves.toHaveLength(1)
|
||||||
expect(reportError).toHaveBeenCalledWith(
|
expect(reportError).not.toHaveBeenCalled()
|
||||||
'Verified Runtime unavailable: opencode',
|
|
||||||
expect.any(Error)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('adopts signed published metadata without rehashing unrelated payload', async () => {
|
it('adopts signed published metadata without rehashing unrelated payload', async () => {
|
||||||
|
|||||||
@@ -678,6 +678,7 @@ async function defaultDetectBinary(
|
|||||||
return detectRuntimeBinary({
|
return detectRuntimeBinary({
|
||||||
binaryPath: configuredPath,
|
binaryPath: configuredPath,
|
||||||
bundledPath,
|
bundledPath,
|
||||||
|
validation: 'path-only',
|
||||||
binaryNames: [runtime],
|
binaryNames: [runtime],
|
||||||
label: 'OpenCode CLI'
|
label: 'OpenCode CLI'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { EventEmitter } from 'node:events'
|
import { EventEmitter } from 'node:events'
|
||||||
import { realpath } from 'node:fs/promises'
|
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 { fileURLToPath } from 'node:url'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import {
|
import {
|
||||||
@@ -93,6 +93,29 @@ describe('runtime discovery', () => {
|
|||||||
expect(detection.version).toMatch(/^\d+\.\d+\.\d+/u)
|
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 () => {
|
it('rejects relative configured paths without resolving them from cwd', async () => {
|
||||||
process.env.PATH = ''
|
process.env.PATH = ''
|
||||||
process.env.Path = ''
|
process.env.Path = ''
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const VERSION_TERMINATION_WAIT_MS = 500
|
|||||||
export type RuntimeBinaryDiscoveryInput = {
|
export type RuntimeBinaryDiscoveryInput = {
|
||||||
binaryPath: string
|
binaryPath: string
|
||||||
bundledPath?: string
|
bundledPath?: string
|
||||||
|
validation?: 'execute' | 'canonical-file' | 'path-only'
|
||||||
bundledValidation?: 'execute' | 'canonical-file'
|
bundledValidation?: 'execute' | 'canonical-file'
|
||||||
bundledVersion?: string
|
bundledVersion?: string
|
||||||
allowAutomaticDiscovery?: boolean
|
allowAutomaticDiscovery?: boolean
|
||||||
@@ -333,11 +334,24 @@ export async function detectRuntimeBinary(
|
|||||||
if (!bundledPath) {
|
if (!bundledPath) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
if (input.validation === 'path-only') {
|
||||||
|
return isAbsolute(bundledPath)
|
||||||
|
? availableDetection(
|
||||||
|
input.label,
|
||||||
|
normalize(bundledPath),
|
||||||
|
input.bundledVersion,
|
||||||
|
'bundled'
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
const canonicalPath = await canonicalFile(bundledPath)
|
const canonicalPath = await canonicalFile(bundledPath)
|
||||||
if (!canonicalPath) {
|
if (!canonicalPath) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
if (input.bundledValidation === 'canonical-file') {
|
if (
|
||||||
|
(input.bundledValidation ?? input.validation) ===
|
||||||
|
'canonical-file'
|
||||||
|
) {
|
||||||
return availableDetection(
|
return availableDetection(
|
||||||
input.label,
|
input.label,
|
||||||
canonicalPath,
|
canonicalPath,
|
||||||
@@ -359,10 +373,24 @@ export async function detectRuntimeBinary(
|
|||||||
if (configuredPath) {
|
if (configuredPath) {
|
||||||
if (!isAbsolute(configuredPath)) {
|
if (!isAbsolute(configuredPath)) {
|
||||||
configuredPathProblem = 'relative'
|
configuredPathProblem = 'relative'
|
||||||
|
} else if (input.validation === 'path-only') {
|
||||||
|
return availableDetection(
|
||||||
|
input.label,
|
||||||
|
normalize(configuredPath),
|
||||||
|
undefined,
|
||||||
|
'configured'
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
const canonicalPath = await canonicalFile(configuredPath)
|
const canonicalPath = await canonicalFile(configuredPath)
|
||||||
if (!canonicalPath) {
|
if (!canonicalPath) {
|
||||||
configuredPathProblem = 'invalid'
|
configuredPathProblem = 'invalid'
|
||||||
|
} else if (input.validation === 'canonical-file') {
|
||||||
|
return availableDetection(
|
||||||
|
input.label,
|
||||||
|
canonicalPath,
|
||||||
|
undefined,
|
||||||
|
'configured'
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
const validation = await validateRuntimeVersion(canonicalPath)
|
const validation = await validateRuntimeVersion(canonicalPath)
|
||||||
if (validation.valid) {
|
if (validation.valid) {
|
||||||
@@ -391,6 +419,15 @@ export async function detectRuntimeBinary(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
foundAutomaticCandidate = true
|
foundAutomaticCandidate = true
|
||||||
|
if (
|
||||||
|
input.validation === 'canonical-file' ||
|
||||||
|
input.validation === 'path-only'
|
||||||
|
) {
|
||||||
|
return availableDetection(
|
||||||
|
input.label,
|
||||||
|
canonicalPath
|
||||||
|
)
|
||||||
|
}
|
||||||
const validation = await validateRuntimeVersion(canonicalPath)
|
const validation = await validateRuntimeVersion(canonicalPath)
|
||||||
if (validation.valid) {
|
if (validation.valid) {
|
||||||
return availableDetection(
|
return availableDetection(
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ export async function verifyPublishedRuntimeBundle(
|
|||||||
/**
|
/**
|
||||||
* Reconstructs a Runtime identity that was fully verified when it was
|
* Reconstructs a Runtime identity that was fully verified when it was
|
||||||
* registered. Normal capabilities and prompt launches use this bounded
|
* 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(
|
export async function loadRegisteredRuntimeBundle(
|
||||||
bundleDirectoryInput: string,
|
bundleDirectoryInput: string,
|
||||||
@@ -242,12 +242,50 @@ export async function loadRegisteredRuntimeBundle(
|
|||||||
const registered = runtimeRegistryEntrySchema.parse(
|
const registered = runtimeRegistryEntrySchema.parse(
|
||||||
options.registered
|
options.registered
|
||||||
)
|
)
|
||||||
const verified = await verifyPublishedRuntimeBundle(
|
const bundleDirectory = assertAbsoluteManagedPath(
|
||||||
bundleDirectoryInput,
|
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
|
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 (
|
if (
|
||||||
|
basename(bundleDirectory) !==
|
||||||
|
registered.bundleDigest.slice('sha256:'.length) ||
|
||||||
manifest.runtimeId !== registered.runtimeId ||
|
manifest.runtimeId !== registered.runtimeId ||
|
||||||
manifest.runtimeVersion !== registered.runtimeVersion ||
|
manifest.runtimeVersion !== registered.runtimeVersion ||
|
||||||
manifest.architecture !== registered.architecture ||
|
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(
|
export function canonicalRuntimeManifestBytes(
|
||||||
|
|||||||
Reference in New Issue
Block a user