Files
goodbuddy/src/main/remote-agent/remote-runtime-installation-manager.ts
T
mesalogo de64e9768a
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
fix: make Agent updates reliable and visible
Valid Agent packages could be rejected when packaged trusted-key JSON used CRLF, and Settings could not show whether a newer compatible Agent existed. Registry formatting is now parsed independently of cryptographic trust, and Settings reads only the signed catalog to report local and latest versions.

New installations use a credential-free local Ollama-compatible profile. The v20 migration changes only untouched historical defaults, while explicit profiles and legacy environment aliases remain compatible. Magic Notes count badges no longer shift navigation rows.

Release note: 修复 Agent 下载兼容性并在设置中显示更新状态;新安装默认连接本地模型,保留显式用户配置;魔法笔记待办数字不再改变侧栏菜单高度。
2026-08-26 15:43:41 +08:00

1626 lines
43 KiB
TypeScript

import { createHash, randomUUID } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import {
agentReleaseKeyRegistrySchema,
type AgentArchitecture,
type AgentReleaseKeyRegistry
} from '../../shared/agent-installation-contracts'
import type { AgentBootstrapIncompatibleReason } from '../../shared/ssh-host-contracts'
import {
assertRuntimeManifestMatchesLock,
canonicalRuntimeManifestBytes,
verifyRuntimeManifestSignature
} from '../../shared/node/runtime-bundle-verifier'
import {
parseRuntimeRegistryState
} from '../../shared/remote-environment-registry-contracts'
import {
digestRemoteRuntimeBundleIdentity,
digestRemoteRuntimeBundleManifest,
remoteRuntimeBundleManifestSchema,
remoteRuntimeLockSchema,
type RemoteRuntimeBundleManifest,
type RemoteRuntimeLock
} from '../../shared/remote-runtime-launch-contracts'
import { sha256DigestSchema } from '../../shared/agent-protocol/contracts'
import type {
BoundedSftpLimits,
SftpEntryMetadata,
StagedSftp
} from '../ssh/bounded-sftp'
import type {
SshConnectionLease,
SshConnectionPool,
SshConnectionPoolTarget
} from '../ssh/ssh-connection-pool'
import { settleBoundedly } from './bounded-settlement'
import {
parseAgentReleaseKeyRegistryBytes
} from './agent-bundle-verifier'
import { isMissingPathError } from './path-errors'
const MAXIMUM_INSTALLATION_FILES = 200
const MAXIMUM_INSTALLATION_BYTES = 512 * 1024 * 1024
const MAXIMUM_RUNTIME_FILE_BYTES = 256 * 1024 * 1024
const MAXIMUM_METADATA_BYTES = 1024 * 1024
const MAXIMUM_CLEANUP_ENTRIES = 512
const METADATA_MODE = 0o644
const PRIVATE_FILE_MODE = 0o600
const PRIVATE_DIRECTORY_MODE = 0o700
const DISPOSE_WAIT_TIMEOUT_MS = 5_000
const RUNTIME_ROOT = '.goodbuddy/runtimes'
const RUNTIME_STAGING_ROOT = `${RUNTIME_ROOT}/staging`
const OPENCODE_ROOT = `${RUNTIME_ROOT}/opencode`
const RELEASE_KEYS_PATH = `${RUNTIME_ROOT}/release-keys.json`
const RUNTIME_LOCK_PATH = `${RUNTIME_ROOT}/remote-runtime-lock.json`
const RUNTIME_REGISTRY_PATH = `${RUNTIME_ROOT}/registry.json`
export class RemoteRuntimeBundleResourcesUnavailableError
extends Error {
constructor(cause?: unknown) {
super(
'OpenCode Runtime installation resources are not included in this package',
{ cause }
)
this.name = 'RemoteRuntimeBundleResourcesUnavailableError'
}
}
export type RemoteRuntimeInstallationPhase =
| 'inspecting-host'
| 'verifying-bundle'
| 'preparing-installation'
| 'uploading-bundle'
| 'publishing-bundle'
| 'activating-runtime'
| 'complete'
export type RemoteRuntimeInstallationIdentity = {
runtimeId: string
runtimeVersion: string
bundleDigest: string
manifestDigest: string
runtimeAdapterDigest: string
acpCapabilitiesDigest: string
platform: 'linux'
architecture: AgentArchitecture
}
export interface RemoteRuntimeInstallationTargetResolver {
resolve(hostId: string): Promise<SshConnectionPoolTarget>
}
export type RemoteRuntimeInstallationRequestOptions = {
signal?: AbortSignal
onProgress?: (phase: RemoteRuntimeInstallationPhase) => void
force?: boolean
}
export class RemoteRuntimeInstallationError extends Error {
constructor(
message: string,
readonly reason:
| 'incompatible'
| 'corrupt'
| 'host-identity-changed'
| 'capacity'
| 'activation',
readonly detail?: AgentBootstrapIncompatibleReason | unknown
) {
super(message)
this.name = 'RemoteRuntimeInstallationError'
}
}
type RuntimeBundleBase = {
bundleDirectory: string
manifest: RemoteRuntimeBundleManifest
manifestDigest: string
}
export type VerifiedRemoteRuntimeInstallationBundle =
RuntimeBundleBase & {
canonicalReleaseKeyRegistryBytes: Uint8Array
canonicalRemoteRuntimeLockBytes: Uint8Array
release?: () => void
}
export type RemoteRuntimeInstallationBundleLoader = (
architecture: AgentArchitecture
) => Promise<VerifiedRemoteRuntimeInstallationBundle>
export type RemoteRuntimeInstallationVerificationMetadata = {
releaseKeyRegistry: AgentReleaseKeyRegistry
runtimeLock: RemoteRuntimeLock
canonicalReleaseKeyRegistryBytes: Uint8Array
canonicalRemoteRuntimeLockBytes: Uint8Array
}
export type RemoteRuntimeInstallationVerificationMetadataLoader =
(
architecture: AgentArchitecture
) => Promise<RemoteRuntimeInstallationVerificationMetadata>
export type RemoteRuntimeActivator = (
lease: SshConnectionLease,
runtimeId: string,
bundleDigest: string,
architecture: AgentArchitecture,
signal: AbortSignal
) => Promise<void>
type ActiveInstallation = {
promise: Promise<RemoteRuntimeInstallationIdentity>
controller: AbortController
progressCallbacks: Set<
(phase: RemoteRuntimeInstallationPhase) => void
>
waiters: number
settled: boolean
}
type CleanupEntry = {
path: string
type: 'file' | 'directory'
}
type LoadedBundle = RuntimeBundleBase & {
releaseKeyRegistryBytes: Buffer
remoteRuntimeLockBytes: Buffer
release?: () => void
}
export class RemoteRuntimeInstallationManager {
readonly #resolver: RemoteRuntimeInstallationTargetResolver
readonly #sshPool: Pick<SshConnectionPool, 'acquire'>
readonly #loadVerifiedBundle: RemoteRuntimeInstallationBundleLoader
readonly #loadVerificationMetadata?:
RemoteRuntimeInstallationVerificationMetadataLoader
readonly #activate: RemoteRuntimeActivator
readonly #sftpLimits?: BoundedSftpLimits
readonly #maximumConcurrentHosts: number
readonly #active = new Map<string, ActiveInstallation>()
readonly #installed = new Map<
string,
RemoteRuntimeInstallationIdentity
>()
#closed = false
#disposePromise: Promise<void> | undefined
constructor(options: {
resolver: RemoteRuntimeInstallationTargetResolver
sshPool: Pick<SshConnectionPool, 'acquire'>
loadVerifiedBundle: RemoteRuntimeInstallationBundleLoader
loadVerificationMetadata?:
RemoteRuntimeInstallationVerificationMetadataLoader
activate: RemoteRuntimeActivator
sftpLimits?: BoundedSftpLimits
maximumConcurrentHosts?: number
}) {
this.#resolver = options.resolver
this.#sshPool = options.sshPool
this.#loadVerifiedBundle = options.loadVerifiedBundle
this.#loadVerificationMetadata =
options.loadVerificationMetadata
this.#activate = options.activate
this.#sftpLimits = options.sftpLimits
this.#maximumConcurrentHosts =
options.maximumConcurrentHosts ?? 8
if (
!Number.isSafeInteger(this.#maximumConcurrentHosts) ||
this.#maximumConcurrentHosts <= 0 ||
this.#maximumConcurrentHosts > 32
) {
throw new Error(
'Remote Runtime installation concurrency limit is invalid'
)
}
}
async ensureInstalled(
hostId: string,
options: RemoteRuntimeInstallationRequestOptions = {}
): Promise<RemoteRuntimeInstallationIdentity> {
this.#throwIfClosed()
options.signal?.throwIfAborted()
emitOne(options.onProgress, 'inspecting-host')
const target = await this.#resolver.resolve(hostId)
this.#throwIfClosed()
options.signal?.throwIfAborted()
if (target.host.id !== hostId) {
throw new RemoteRuntimeInstallationError(
'Resolved SSH host does not match the Runtime installation request',
'host-identity-changed'
)
}
const operationKey = targetIdentityKey(target)
const existing = this.#active.get(operationKey)
if (existing) {
return this.#waitForActive(existing, options)
}
const installed = this.#installed.get(operationKey)
if (!options.force && installed !== undefined) {
emitOne(options.onProgress, 'complete')
return installed
}
if (this.#active.size >= this.#maximumConcurrentHosts) {
throw new RemoteRuntimeInstallationError(
'Too many remote Runtime installations are already running',
'capacity'
)
}
const progressCallbacks = new Set<
(phase: RemoteRuntimeInstallationPhase) => void
>()
const controller = new AbortController()
const progress = (
phase: RemoteRuntimeInstallationPhase
): void => {
for (const callback of progressCallbacks) {
emitOne(callback, phase)
}
}
const promise = this.#run(target, controller.signal, progress)
.then((identity) => {
this.#installed.set(operationKey, identity)
return identity
})
const active: ActiveInstallation = {
promise,
controller,
progressCallbacks,
waiters: 0,
settled: false
}
this.#active.set(operationKey, active)
void active.promise.finally(() => {
active.settled = true
if (this.#active.get(operationKey) === active) {
this.#active.delete(operationKey)
}
}).catch(() => undefined)
return this.#waitForActive(active, options)
}
dispose(): Promise<void> {
if (this.#disposePromise) {
return this.#disposePromise
}
this.#closed = true
const operations = [...this.#active.values()]
const reason = new DOMException(
'Remote Runtime installation manager was disposed',
'AbortError'
)
for (const operation of operations) {
operation.controller.abort(reason)
}
this.#disposePromise = settleBoundedly(
operations.map((operation) => operation.promise),
DISPOSE_WAIT_TIMEOUT_MS
)
this.#installed.clear()
return this.#disposePromise
}
#throwIfClosed(): void {
if (this.#closed) {
throw new Error(
'Remote Runtime installation manager is disposed'
)
}
}
async #waitForActive(
active: ActiveInstallation,
options: RemoteRuntimeInstallationRequestOptions
): Promise<RemoteRuntimeInstallationIdentity> {
active.waiters += 1
if (options.onProgress) {
active.progressCallbacks.add(options.onProgress)
}
try {
return await waitForOperation(active.promise, options.signal)
} finally {
if (options.onProgress) {
active.progressCallbacks.delete(options.onProgress)
}
active.waiters -= 1
if (
active.waiters === 0 &&
!active.settled &&
!active.controller.signal.aborted
) {
active.controller.abort(
new DOMException(
'Remote Runtime installation has no remaining waiters',
'AbortError'
)
)
}
}
}
async #run(
target: SshConnectionPoolTarget,
signal: AbortSignal,
progress: (phase: RemoteRuntimeInstallationPhase) => void
): Promise<RemoteRuntimeInstallationIdentity> {
let lease: SshConnectionLease | undefined
let sftp: StagedSftp | undefined
let cleanupHomeDirectory: string | undefined
const cleanup: CleanupEntry[] = []
let stagingPublished = false
let releaseBundle: (() => void) | undefined
try {
lease = await this.#sshPool.acquire(target, signal)
assertLeaseMatchesTarget(lease, target)
const probe = await lease.runAgentBootstrapProbe(signal)
if (!probe.ready) {
throw new RemoteRuntimeInstallationError(
`Remote host cannot install the OpenCode Runtime: ${probe.reason}`,
'incompatible',
probe.reason
)
}
if (
probe.platform !== 'linux' ||
(probe.architecture !== 'x64' &&
probe.architecture !== 'arm64')
) {
throw new RemoteRuntimeInstallationError(
'Remote host platform or architecture is unsupported',
'incompatible'
)
}
progress('verifying-bundle')
let bundle: LoadedBundle
try {
bundle = await this.#loadAndValidateBundle(
probe.architecture
)
releaseBundle = bundle.release
} catch (error) {
if (
error instanceof RemoteRuntimeBundleResourcesUnavailableError
) {
if (this.#loadVerificationMetadata === undefined) {
throw new RemoteRuntimeInstallationError(
'This package does not include Runtime installation resources',
'incompatible',
safeErrorDetail(error)
)
}
return await this.#reuseInstalledBundle(
target,
lease,
probe,
signal,
progress
)
}
throw error
}
assertBundleCapacity(bundle)
const identity = publicIdentity(bundle)
const destination =
`${OPENCODE_ROOT}/${digestDirectoryName(bundle.manifest.bundleDigest)}`
progress('preparing-installation')
sftp = await lease.openStagedSftp(
probe.canonicalHomeDirectory,
installationSftpLimits(bundle, this.#sftpLimits),
signal
)
cleanupHomeDirectory = probe.canonicalHomeDirectory
await ensureManagedHierarchy(sftp, probe.uid, signal)
await installPrivateMetadata(
sftp,
RELEASE_KEYS_PATH,
'.release-keys',
bundle.releaseKeyRegistryBytes,
probe.uid,
cleanup,
signal
)
await installPrivateMetadata(
sftp,
RUNTIME_LOCK_PATH,
'.remote-runtime-lock',
bundle.remoteRuntimeLockBytes,
probe.uid,
cleanup,
signal
)
const existingDestination = await pathMetadata(
sftp,
destination,
signal
)
if (existingDestination !== undefined) {
await verifyRemoteBundle(
sftp,
destination,
bundle,
probe.uid,
signal
).catch((error: unknown) => {
rethrowAbort(error)
throw new RemoteRuntimeInstallationError(
'Existing OpenCode Runtime installation is corrupt',
'corrupt',
safeErrorDetail(error)
)
})
await assertCurrentTarget(
this.#resolver,
lease,
target,
signal
)
progress('activating-runtime')
await activateRuntime(
this.#activate,
lease,
bundle,
signal
)
progress('complete')
return identity
}
progress('uploading-bundle')
const staging = `${RUNTIME_STAGING_ROOT}/op-${randomUUID()}`
await sftp.mkdir(staging, signal)
trackCleanup(cleanup, { path: staging, type: 'directory' })
await assertRemoteMetadata(
sftp,
staging,
'directory',
probe.uid,
PRIVATE_DIRECTORY_MODE,
undefined,
signal
)
await uploadAndVerifyBundle(
sftp,
staging,
bundle,
probe.uid,
cleanup,
signal
).catch((error: unknown) => {
rethrowAbort(error)
throw new RemoteRuntimeInstallationError(
'OpenCode Runtime bundle upload verification failed',
'corrupt',
safeErrorDetail(error)
)
})
await assertCurrentTarget(
this.#resolver,
lease,
target,
signal
)
progress('publishing-bundle')
await sftp.rename(staging, destination, signal)
stagingPublished = true
await verifyRemoteBundle(
sftp,
destination,
bundle,
probe.uid,
signal
).catch((error: unknown) => {
rethrowAbort(error)
throw new RemoteRuntimeInstallationError(
'Published OpenCode Runtime bundle verification failed',
'corrupt',
safeErrorDetail(error)
)
})
progress('activating-runtime')
await activateRuntime(
this.#activate,
lease,
bundle,
signal
)
progress('complete')
return identity
} catch (error) {
if (
error instanceof RemoteRuntimeInstallationError ||
isAbortError(error)
) {
throw error
}
throw new RemoteRuntimeInstallationError(
'Remote Runtime installation data is corrupt or unsafe',
'corrupt',
safeErrorDetail(error)
)
} finally {
let cleanupIncomplete = false
if (sftp && !stagingPublished) {
cleanupIncomplete = !(await cleanupOwnedStaging(sftp, cleanup))
}
sftp?.close()
if (
cleanupIncomplete &&
lease?.isUsable() &&
cleanupHomeDirectory
) {
let cleanupSftp: StagedSftp | undefined
try {
cleanupSftp = await lease.openStagedSftp(
cleanupHomeDirectory,
this.#sftpLimits
)
await cleanupOwnedStaging(cleanupSftp, cleanup)
} catch {
// Cancellation can make the lease unavailable. Cleanup remains
// bounded to this operation's temporary files and staging tree.
} finally {
cleanupSftp?.close()
}
}
lease?.release()
releaseBundle?.()
}
}
async #loadAndValidateBundle(
architecture: AgentArchitecture
): Promise<LoadedBundle> {
let loaded: VerifiedRemoteRuntimeInstallationBundle | undefined
try {
loaded = await this.#loadVerifiedBundle(architecture)
const manifest = remoteRuntimeBundleManifestSchema.parse(
loaded.manifest
)
if (
manifest.runtimeId !== 'opencode' ||
manifest.provider !== 'opencode' ||
manifest.platform !== 'linux' ||
manifest.architecture !== architecture
) {
throw new Error(
'Verified Runtime bundle does not match the OpenCode host target'
)
}
if (
loaded.manifestDigest !==
await digestRemoteRuntimeBundleManifest(manifest)
) {
throw new Error('Verified Runtime manifest digest is invalid')
}
if (
manifest.bundleDigest !==
await digestRemoteRuntimeBundleIdentity(manifest)
) {
throw new Error('Verified Runtime bundle digest is invalid')
}
const releaseKeyRegistryBytes = canonicalJsonBytes(
loaded.canonicalReleaseKeyRegistryBytes,
agentReleaseKeyRegistrySchema,
'Runtime release-key registry'
)
const remoteRuntimeLockBytes = canonicalJsonBytes(
loaded.canonicalRemoteRuntimeLockBytes,
remoteRuntimeLockSchema,
'Remote Runtime lock'
)
return {
bundleDirectory: loaded.bundleDirectory,
manifest,
manifestDigest: loaded.manifestDigest,
releaseKeyRegistryBytes,
remoteRuntimeLockBytes,
...(loaded.release ? { release: loaded.release } : {})
}
} catch (error) {
loaded?.release?.()
rethrowAbort(error)
if (
error instanceof RemoteRuntimeBundleResourcesUnavailableError
) {
throw error
}
throw new RemoteRuntimeInstallationError(
'Verified OpenCode Runtime bundle is corrupt',
'corrupt',
safeErrorDetail(error)
)
}
}
async #reuseInstalledBundle(
target: SshConnectionPoolTarget,
lease: SshConnectionLease,
probe: {
architecture: AgentArchitecture
canonicalHomeDirectory: string
uid: number
},
signal: AbortSignal,
progress: (phase: RemoteRuntimeInstallationPhase) => void
): Promise<RemoteRuntimeInstallationIdentity> {
const loadMetadata = this.#loadVerificationMetadata
if (loadMetadata === undefined) {
throw new RemoteRuntimeInstallationError(
'This package cannot verify an existing Host Runtime',
'incompatible'
)
}
let metadataSftp: StagedSftp | undefined
try {
const metadata = await loadMetadata(probe.architecture)
const releaseKeyRegistryBytes = canonicalJsonBytes(
metadata.canonicalReleaseKeyRegistryBytes,
agentReleaseKeyRegistrySchema,
'Runtime release-key registry'
)
const remoteRuntimeLockBytes = canonicalJsonBytes(
metadata.canonicalRemoteRuntimeLockBytes,
remoteRuntimeLockSchema,
'Remote Runtime lock'
)
const releaseKeyRegistry =
agentReleaseKeyRegistrySchema.parse(
metadata.releaseKeyRegistry
)
const runtimeLock = remoteRuntimeLockSchema.parse(
metadata.runtimeLock
)
if (
!canonicalJsonValueBytes(releaseKeyRegistry).equals(
releaseKeyRegistryBytes
) ||
!canonicalJsonValueBytes(runtimeLock).equals(
remoteRuntimeLockBytes
)
) {
throw new Error(
'Runtime verification metadata values do not match their canonical bytes'
)
}
metadataSftp = await lease.openStagedSftp(
probe.canonicalHomeDirectory,
{
maximumFileBytes: MAXIMUM_METADATA_BYTES,
maximumTotalBytes: MAXIMUM_METADATA_BYTES * 6,
maximumOperations: 48,
operationTimeoutMs: 15_000
},
signal
)
const [
remoteRegistryBytes,
remoteReleaseKeyBytes,
remoteRuntimeLockFileBytes
] = await Promise.all([
metadataSftp.readFile(RUNTIME_REGISTRY_PATH, signal),
metadataSftp.readFile(RELEASE_KEYS_PATH, signal),
metadataSftp.readFile(RUNTIME_LOCK_PATH, signal)
])
const remoteReleaseKeyRegistryBytes =
Buffer.from(remoteReleaseKeyBytes)
const remoteReleaseKeyRegistry =
parseAgentReleaseKeyRegistryBytes(
remoteReleaseKeyRegistryBytes,
'Installed Runtime release-key registry'
)
if (!remoteRuntimeLockFileBytes.equals(remoteRuntimeLockBytes)) {
throw new Error(
'Installed Runtime lock does not match this GoodBuddy build'
)
}
const remoteRegistry = parseRuntimeRegistryState(
parseJsonBytes(
remoteRegistryBytes,
'Installed Runtime registry'
)
)
const current = remoteRegistry.current.find(
(entry) =>
entry.runtimeId === 'opencode' &&
entry.architecture === probe.architecture
)
if (
current === undefined ||
current.runtimeVersion !==
runtimeLock.runtimes.opencode.version
) {
throw new RemoteRuntimeInstallationError(
'This package does not include Runtime installation resources and the Host has no matching current OpenCode Runtime',
'incompatible'
)
}
const destination =
`${OPENCODE_ROOT}/${digestDirectoryName(current.bundleDigest)}`
const manifestBytes = await metadataSftp.readFile(
`${destination}/manifest.json`,
signal
)
const signatureFile = await metadataSftp.readFile(
`${destination}/manifest.sig`,
signal
)
const signature = decodeDetachedSignature(
signatureFile,
'Runtime detached signature'
)
const manifest = await verifyRuntimeManifestSignature(
manifestBytes,
signature,
releaseKeyRegistry
)
await verifyRuntimeManifestSignature(
manifestBytes,
signature,
remoteReleaseKeyRegistry
)
assertRuntimeManifestMatchesLock(
manifest,
runtimeLock,
probe.architecture
)
const manifestDigest =
await digestRemoteRuntimeBundleManifest(manifest)
if (
manifest.bundleDigest !== current.bundleDigest ||
manifestDigest !== current.manifestDigest ||
manifest.acpCapabilitiesDigest !==
current.acpCapabilitiesDigest
) {
throw new Error(
'Installed Runtime manifest identity does not match its registry'
)
}
const bundle: LoadedBundle = {
bundleDirectory: destination,
manifest,
manifestDigest,
releaseKeyRegistryBytes,
remoteRuntimeLockBytes
}
assertBundleCapacity(bundle)
await verifyRemoteFile(
metadataSftp,
RUNTIME_REGISTRY_PATH,
remoteRegistryBytes.byteLength,
sha256(remoteRegistryBytes),
probe.uid,
PRIVATE_FILE_MODE,
signal
)
await verifyRemoteFile(
metadataSftp,
RELEASE_KEYS_PATH,
remoteReleaseKeyRegistryBytes.byteLength,
sha256(remoteReleaseKeyRegistryBytes),
probe.uid,
PRIVATE_FILE_MODE,
signal
)
await verifyRemoteFile(
metadataSftp,
RUNTIME_LOCK_PATH,
remoteRuntimeLockBytes.byteLength,
sha256(remoteRuntimeLockBytes),
probe.uid,
PRIVATE_FILE_MODE,
signal
)
progress('preparing-installation')
await verifyRemoteBundleFromMetadata(
metadataSftp,
destination,
bundle,
manifestBytes,
signatureFile,
probe.uid,
'metadata',
signal
)
metadataSftp.close()
metadataSftp = undefined
await assertCurrentTarget(
this.#resolver,
lease,
target,
signal
)
progress('activating-runtime')
await activateRuntime(
this.#activate,
lease,
bundle,
signal
)
progress('complete')
return publicIdentity(bundle)
} catch (error) {
rethrowAbort(error)
if (error instanceof RemoteRuntimeInstallationError) {
throw error
}
if (isMissingPathError(error)) {
throw new RemoteRuntimeInstallationError(
'This package does not include Runtime installation resources and the Host has no matching current OpenCode Runtime',
'incompatible',
safeErrorDetail(error)
)
}
throw new RemoteRuntimeInstallationError(
'The existing Host OpenCode Runtime could not be verified',
'corrupt',
safeErrorDetail(error)
)
} finally {
metadataSftp?.close()
}
}
}
function targetIdentityKey(target: SshConnectionPoolTarget): string {
const hostKey = target.host.hostKey
return createHash('sha256')
.update(JSON.stringify([
'goodbuddy-remote-runtime-installation-target-v1',
target.host.id,
target.host.name,
target.host.hostname,
target.host.port,
target.host.username,
target.host.authentication,
target.hostRevision,
target.hostKeyGeneration,
hostKey?.algorithm ?? null,
hostKey?.publicKeyBase64 ?? null,
hostKey?.fingerprintSha256 ?? null,
hostKey?.generation ?? null
]))
.digest('hex')
}
function assertLeaseMatchesTarget(
lease: SshConnectionLease,
target: SshConnectionPoolTarget
): void {
if (
lease.identity.hostId !== target.host.id ||
lease.identity.hostRevision !== target.hostRevision ||
lease.identity.hostKeyGeneration !==
target.hostKeyGeneration ||
!/^[a-f0-9]{64}$/u.test(
lease.identity.authenticationIdentity
)
) {
throw new RemoteRuntimeInstallationError(
'SSH lease identity changed during Runtime installation',
'host-identity-changed'
)
}
}
async function assertCurrentTarget(
resolver: RemoteRuntimeInstallationTargetResolver,
lease: SshConnectionLease,
originalTarget: SshConnectionPoolTarget,
signal: AbortSignal
): Promise<void> {
signal.throwIfAborted()
const currentTarget = await resolver.resolve(
originalTarget.host.id
)
signal.throwIfAborted()
if (
targetIdentityKey(currentTarget) !==
targetIdentityKey(originalTarget)
) {
throw new RemoteRuntimeInstallationError(
'SSH host identity changed before Runtime publication or activation',
'host-identity-changed'
)
}
assertLeaseMatchesTarget(lease, currentTarget)
}
function publicIdentity(
bundle: LoadedBundle
): RemoteRuntimeInstallationIdentity {
return {
runtimeId: bundle.manifest.runtimeId,
runtimeVersion: bundle.manifest.runtimeVersion,
bundleDigest: bundle.manifest.bundleDigest,
manifestDigest: bundle.manifestDigest,
runtimeAdapterDigest: bundle.manifest.adapterDigest,
acpCapabilitiesDigest:
bundle.manifest.acpCapabilitiesDigest,
platform: bundle.manifest.platform,
architecture: bundle.manifest.architecture
}
}
function digestDirectoryName(bundleDigest: string): string {
if (!sha256DigestSchema.safeParse(bundleDigest).success) {
throw new Error('Runtime bundle digest is invalid')
}
return bundleDigest.slice('sha256:'.length)
}
function assertBundleCapacity(bundle: LoadedBundle): void {
const metadataBytes =
bundle.releaseKeyRegistryBytes.byteLength +
bundle.remoteRuntimeLockBytes.byteLength
const payloadBytes = bundle.manifest.files.reduce(
(sum, file) => sum + file.size,
0
)
const total = payloadBytes + metadataBytes
if (
bundle.manifest.files.length > MAXIMUM_INSTALLATION_FILES ||
bundle.manifest.files.some(
(file) => file.size > MAXIMUM_RUNTIME_FILE_BYTES
) ||
bundle.releaseKeyRegistryBytes.byteLength >
MAXIMUM_METADATA_BYTES ||
bundle.remoteRuntimeLockBytes.byteLength >
MAXIMUM_METADATA_BYTES ||
!Number.isSafeInteger(total) ||
total > MAXIMUM_INSTALLATION_BYTES
) {
throw new RemoteRuntimeInstallationError(
'OpenCode Runtime bundle exceeds installation safety limits',
'capacity'
)
}
}
function installationSftpLimits(
bundle: LoadedBundle,
configured?: BoundedSftpLimits
): BoundedSftpLimits {
const largest = Math.max(
bundle.releaseKeyRegistryBytes.byteLength,
bundle.remoteRuntimeLockBytes.byteLength,
...bundle.manifest.files.map((file) => file.size),
MAXIMUM_METADATA_BYTES
)
const payload =
bundle.manifest.files.reduce(
(sum, file) => sum + file.size,
0
) +
bundle.releaseKeyRegistryBytes.byteLength +
bundle.remoteRuntimeLockBytes.byteLength +
2 * MAXIMUM_METADATA_BYTES
return {
maximumFileBytes: Math.max(
largest,
configured?.maximumFileBytes ?? 0
),
maximumTotalBytes: Math.max(
payload * 4,
configured?.maximumTotalBytes ?? 0
),
maximumOperations:
configured?.maximumOperations ?? 2_048,
operationTimeoutMs: configured?.operationTimeoutMs
}
}
async function ensureManagedHierarchy(
sftp: StagedSftp,
uid: number,
signal: AbortSignal
): Promise<void> {
for (const path of [
'.goodbuddy',
RUNTIME_ROOT,
RUNTIME_STAGING_ROOT,
OPENCODE_ROOT
]) {
const existing = await pathMetadata(sftp, path, signal)
if (existing === undefined) {
await sftp.mkdir(path, signal)
} else {
assertMetadata(
existing,
path,
'directory',
uid,
undefined,
undefined
)
if (existing.mode !== PRIVATE_DIRECTORY_MODE) {
await sftp.chmod(path, PRIVATE_DIRECTORY_MODE, signal)
}
}
await assertRemoteMetadata(
sftp,
path,
'directory',
uid,
PRIVATE_DIRECTORY_MODE,
undefined,
signal
)
}
}
async function installPrivateMetadata(
sftp: StagedSftp,
destination: string,
temporaryPrefix: string,
contents: Buffer,
uid: number,
cleanup: CleanupEntry[],
signal: AbortSignal
): Promise<void> {
const existing = await pathMetadata(sftp, destination, signal)
if (existing !== undefined) {
assertMetadata(
existing,
destination,
'file',
uid,
PRIVATE_FILE_MODE,
undefined
)
if (
existing.size === contents.byteLength &&
(await sftp.readFile(destination, signal)).equals(contents)
) {
return
}
}
const temporary =
`${RUNTIME_ROOT}/${temporaryPrefix}-${randomUUID()}.tmp`
trackCleanup(cleanup, { path: temporary, type: 'file' })
await sftp.writeFile(temporary, contents, signal)
await sftp.chmod(temporary, PRIVATE_FILE_MODE, signal)
await verifyRemoteFile(
sftp,
temporary,
contents.byteLength,
sha256(contents),
uid,
PRIVATE_FILE_MODE,
signal
)
await sftp.replaceFile(temporary, destination, signal)
removeCleanup(cleanup, temporary)
await verifyRemoteFile(
sftp,
destination,
contents.byteLength,
sha256(contents),
uid,
PRIVATE_FILE_MODE,
signal
)
}
async function uploadAndVerifyBundle(
sftp: StagedSftp,
staging: string,
bundle: LoadedBundle,
uid: number,
cleanup: CleanupEntry[],
signal: AbortSignal
): Promise<void> {
const createdDirectories = new Set<string>()
for (const file of bundle.manifest.files) {
const parts = file.path.split('/')
let relativeDirectory = ''
for (const part of parts.slice(0, -1)) {
relativeDirectory = relativeDirectory
? `${relativeDirectory}/${part}`
: part
if (!createdDirectories.has(relativeDirectory)) {
const directory = `${staging}/${relativeDirectory}`
await sftp.mkdir(directory, signal)
trackCleanup(cleanup, {
path: directory,
type: 'directory'
})
createdDirectories.add(relativeDirectory)
await assertRemoteMetadata(
sftp,
directory,
'directory',
uid,
PRIVATE_DIRECTORY_MODE,
undefined,
signal
)
}
}
const contents = await readFile(
join(bundle.bundleDirectory, ...file.path.split('/'))
)
if (
contents.byteLength !== file.size ||
sha256(contents) !== file.sha256
) {
throw new Error(
`Runtime bundle changed after local verification: ${file.path}`
)
}
const destination = `${staging}/${file.path}`
trackCleanup(cleanup, { path: destination, type: 'file' })
await sftp.writeFile(destination, contents, signal)
const mode = file.mode === '0755' ? 0o755 : 0o644
await sftp.chmod(destination, mode, signal)
await verifyRemoteFile(
sftp,
destination,
file.size,
file.sha256,
uid,
mode,
signal
)
}
const expectedManifestBytes =
canonicalRuntimeManifestBytes(bundle.manifest)
for (const metadataName of ['manifest.json', 'manifest.sig']) {
const contents = await readFile(
join(bundle.bundleDirectory, metadataName)
)
if (
contents.byteLength > MAXIMUM_METADATA_BYTES ||
(
metadataName === 'manifest.json' &&
!contents.equals(expectedManifestBytes)
)
) {
throw new Error(
`Runtime bundle metadata changed after verification: ${metadataName}`
)
}
const destination = `${staging}/${metadataName}`
trackCleanup(cleanup, { path: destination, type: 'file' })
await sftp.writeFile(destination, contents, signal)
await sftp.chmod(destination, METADATA_MODE, signal)
await verifyRemoteFile(
sftp,
destination,
contents.byteLength,
sha256(contents),
uid,
METADATA_MODE,
signal
)
}
}
async function verifyRemoteBundle(
sftp: StagedSftp,
destination: string,
bundle: LoadedBundle,
uid: number,
signal: AbortSignal
): Promise<void> {
const [manifestBytes, signatureBytes] = await Promise.all([
readFile(join(bundle.bundleDirectory, 'manifest.json')),
readFile(join(bundle.bundleDirectory, 'manifest.sig'))
])
await verifyRemoteBundleFromMetadata(
sftp,
destination,
bundle,
manifestBytes,
signatureBytes,
uid,
'contents',
signal
)
}
async function verifyRemoteBundleFromMetadata(
sftp: StagedSftp,
destination: string,
bundle: LoadedBundle,
manifestBytes: Buffer,
signatureBytes: Buffer,
uid: number,
verification: 'metadata' | 'contents',
signal: AbortSignal
): Promise<void> {
await assertRemoteMetadata(
sftp,
destination,
'directory',
uid,
PRIVATE_DIRECTORY_MODE,
undefined,
signal
)
const checkedDirectories = new Set<string>()
for (const file of bundle.manifest.files) {
const parts = file.path.split('/')
let directory = destination
for (const part of parts.slice(0, -1)) {
directory = `${directory}/${part}`
if (!checkedDirectories.has(directory)) {
checkedDirectories.add(directory)
await assertRemoteMetadata(
sftp,
directory,
'directory',
uid,
PRIVATE_DIRECTORY_MODE,
undefined,
signal
)
}
}
const path = `${destination}/${file.path}`
const mode = file.mode === '0755' ? 0o755 : 0o644
if (verification === 'contents') {
await verifyRemoteFile(
sftp,
path,
file.size,
file.sha256,
uid,
mode,
signal
)
} else {
await assertRemoteMetadata(
sftp,
path,
'file',
uid,
mode,
file.size,
signal
)
}
}
for (const [metadataName, contents] of [
['manifest.json', manifestBytes],
['manifest.sig', signatureBytes]
] as const) {
if (contents.byteLength > MAXIMUM_METADATA_BYTES) {
throw new Error('Runtime metadata exceeds its safety limit')
}
const path = `${destination}/${metadataName}`
if (verification === 'contents') {
await verifyRemoteFile(
sftp,
path,
contents.byteLength,
sha256(contents),
uid,
METADATA_MODE,
signal
)
} else {
await assertRemoteMetadata(
sftp,
path,
'file',
uid,
METADATA_MODE,
contents.byteLength,
signal
)
}
}
}
async function verifyRemoteFile(
sftp: StagedSftp,
path: string,
size: number,
digest: string,
uid: number,
mode: number,
signal: AbortSignal
): Promise<void> {
await assertRemoteMetadata(
sftp,
path,
'file',
uid,
mode,
size,
signal
)
const contents = await sftp.readFile(path, signal)
if (contents.byteLength !== size || sha256(contents) !== digest) {
throw new Error(`Remote Runtime file readback mismatch: ${path}`)
}
await assertRemoteMetadata(
sftp,
path,
'file',
uid,
mode,
size,
signal
)
}
async function assertRemoteMetadata(
sftp: StagedSftp,
path: string,
type: 'file' | 'directory',
uid: number,
mode: number,
size: number | undefined,
signal: AbortSignal
): Promise<void> {
assertMetadata(
await sftp.stat(path, signal),
path,
type,
uid,
mode,
size
)
}
function assertMetadata(
metadata: SftpEntryMetadata,
path: string,
type: 'file' | 'directory',
uid: number,
mode: number | undefined,
size: number | undefined
): void {
if (
metadata.type !== type ||
metadata.uid !== uid ||
(mode !== undefined && metadata.mode !== mode) ||
(size !== undefined && metadata.size !== size)
) {
throw new Error(`Remote Runtime metadata mismatch: ${path}`)
}
}
async function pathMetadata(
sftp: StagedSftp,
path: string,
signal?: AbortSignal
): Promise<SftpEntryMetadata | undefined> {
try {
return await sftp.lstat(path, signal)
} catch (error) {
if (isMissingPathError(error)) {
return undefined
}
throw error
}
}
async function activateRuntime(
activate: RemoteRuntimeActivator,
lease: SshConnectionLease,
bundle: LoadedBundle,
signal: AbortSignal
): Promise<void> {
signal.throwIfAborted()
try {
await activate(
lease,
bundle.manifest.runtimeId,
bundle.manifest.bundleDigest,
bundle.manifest.architecture,
signal
)
} catch (error) {
rethrowAbort(error)
throw new RemoteRuntimeInstallationError(
'OpenCode Runtime activation failed',
'activation',
safeErrorDetail(error)
)
}
}
function canonicalJsonBytes<T>(
input: Uint8Array | undefined,
schema: { parse(value: unknown): T },
label: string
): Buffer {
if (input === undefined) {
throw new Error(`${label} bytes are missing`)
}
const bytes = Buffer.from(input)
if (
bytes.byteLength === 0 ||
bytes.byteLength > MAXIMUM_METADATA_BYTES
) {
throw new Error(`${label} exceeds its safety limit`)
}
let parsedJson: unknown
try {
parsedJson = JSON.parse(bytes.toString('utf8'))
} catch (error) {
throw new Error(`${label} is invalid JSON`, { cause: error })
}
const parsed = schema.parse(parsedJson)
const canonical = Buffer.from(
`${JSON.stringify(parsed, null, 2)}\n`,
'utf8'
)
if (!canonical.equals(bytes)) {
throw new Error(`${label} bytes are not canonical`)
}
return canonical
}
function canonicalJsonValueBytes(value: unknown): Buffer {
return Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8')
}
function parseJsonBytes(
value: Buffer,
description: string
): unknown {
try {
return JSON.parse(value.toString('utf8')) as unknown
} catch (error) {
throw new Error(`${description} is not valid JSON`, {
cause: error
})
}
}
function decodeDetachedSignature(
value: Buffer,
description: string
): Buffer {
const text = value.toString('ascii')
if (!/^[A-Za-z0-9+/]{86}==\n$/u.test(text)) {
throw new Error(`${description} is not canonical Base64`)
}
const signature = Buffer.from(text.slice(0, -1), 'base64')
if (signature.byteLength !== 64) {
throw new Error(`${description} is not an Ed25519 signature`)
}
return signature
}
function trackCleanup(
cleanup: CleanupEntry[],
entry: CleanupEntry
): void {
if (cleanup.length >= MAXIMUM_CLEANUP_ENTRIES) {
throw new RemoteRuntimeInstallationError(
'Runtime cleanup inventory exceeds its safety limit',
'capacity'
)
}
cleanup.push(entry)
}
function removeCleanup(
cleanup: CleanupEntry[],
path: string
): void {
const index = cleanup.findIndex((entry) => entry.path === path)
if (index >= 0) {
cleanup.splice(index, 1)
}
}
async function cleanupOwnedStaging(
sftp: StagedSftp,
entries: readonly CleanupEntry[]
): Promise<boolean> {
let complete = true
for (const entry of [...entries]
.slice(0, MAXIMUM_CLEANUP_ENTRIES)
.reverse()) {
try {
if (entry.type === 'file') {
await sftp.unlink(entry.path)
} else {
await sftp.rmdir(entry.path)
}
} catch (error) {
if (!isMissingPathError(error)) {
complete = false
// Cleanup never widens beyond operation-owned temporary paths.
}
}
}
return complete
}
function waitForOperation<T>(
promise: Promise<T>,
signal?: AbortSignal
): Promise<T> {
if (!signal) {
return promise
}
if (signal.aborted) {
return Promise.reject(
signal.reason ??
new DOMException('The operation was aborted', 'AbortError')
)
}
return new Promise((resolve, reject) => {
const abort = (): void => {
reject(
signal.reason ??
new DOMException('The operation was aborted', 'AbortError')
)
}
signal.addEventListener('abort', abort, { once: true })
promise.then(
(value) => {
signal.removeEventListener('abort', abort)
resolve(value)
},
(error: unknown) => {
signal.removeEventListener('abort', abort)
reject(error)
}
)
})
}
function emitOne(
callback:
| ((phase: RemoteRuntimeInstallationPhase) => void)
| undefined,
phase: RemoteRuntimeInstallationPhase
): void {
try {
callback?.(phase)
} catch {
// Progress observers cannot alter installation outcomes.
}
}
function isAbortError(error: unknown): boolean {
return (
typeof error === 'object' &&
error !== null &&
'name' in error &&
(error as { name?: unknown }).name === 'AbortError'
)
}
function rethrowAbort(error: unknown): void {
if (isAbortError(error)) {
throw error
}
}
function safeErrorDetail(error: unknown): unknown {
if (error instanceof Error) {
return {
name: error.name,
message: error.message
}
}
return undefined
}
function sha256(contents: Uint8Array): string {
return createHash('sha256').update(contents).digest('hex')
}