feat: show runtime checklists and support remote Continue
Display native OpenCode and Continue checklists alongside scheduled tasks, preserving request ownership, explicit clears and saved progress. Bundle both remote runtimes and support managed Continue installation, model routing, cancellation and restart continuation. Validation: real local and Linux remote model checks, full Electron remote Continue flows, typecheck and lint passed. Full suite: 4707 passed, 67 skipped, one OpenCode compaction failure passed targeted rerun. Release note: Display native execution checklists above conversations and support managed remote Continue projects with saved progress and cancellation recovery.
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
writeFile
|
||||
} from 'node:fs/promises'
|
||||
import { basename, join, resolve } from 'node:path'
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
import { agentPlatformSchema, type AgentPlatform } from '../../shared/agent-target'
|
||||
import {
|
||||
agentPackageArchiveName,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
agentPackageInventorySchema,
|
||||
type AgentPackageCatalog,
|
||||
type AgentPackageCatalogEntry,
|
||||
type AgentPackageDescriptor,
|
||||
type AgentPackageDownloadProgress,
|
||||
type AgentPackageInventory
|
||||
} from '../../shared/agent-package-contracts'
|
||||
@@ -105,6 +107,7 @@ export type AgentPackageManagerOptions = {
|
||||
}
|
||||
|
||||
export type VerifiedRemoteAgentInstallCandidate = {
|
||||
additionalRuntimes?: AgentPackageDescriptor['additionalRuntimes']
|
||||
source: UpdateSource
|
||||
platform: AgentPlatform
|
||||
architecture: AgentArchitecture
|
||||
@@ -115,8 +118,8 @@ export type VerifiedRemoteAgentInstallCandidate = {
|
||||
minor: number
|
||||
}
|
||||
remoteRuntime: {
|
||||
runtimeId: 'opencode'
|
||||
provider: 'opencode'
|
||||
runtimeId: 'opencode' | 'continue'
|
||||
provider: 'opencode' | 'continue'
|
||||
version: string
|
||||
bundleDigest: string
|
||||
protocol: {
|
||||
@@ -138,8 +141,8 @@ export type VerifiedRemoteAgentEnvironmentCatalog = {
|
||||
expected: {
|
||||
agent: { version: string }
|
||||
runtimes: Array<{
|
||||
runtimeId: 'opencode'
|
||||
provider: 'opencode'
|
||||
runtimeId: 'opencode' | 'continue'
|
||||
provider: 'opencode' | 'continue'
|
||||
version: string
|
||||
}>
|
||||
}
|
||||
@@ -170,15 +173,17 @@ export type AcquireInstallArchiveOptions = {
|
||||
|
||||
function expectedCatalog(
|
||||
agentVersion: string,
|
||||
runtimeVersion: string
|
||||
runtimeVersion: string,
|
||||
runtimeId: 'opencode' | 'continue' = 'opencode',
|
||||
additionalRuntimes: NonNullable<AgentPackageDescriptor['additionalRuntimes']> = []
|
||||
): VerifiedRemoteAgentEnvironmentCatalog['expected'] {
|
||||
return {
|
||||
agent: { version: agentVersion },
|
||||
runtimes: [{
|
||||
runtimeId: 'opencode',
|
||||
provider: 'opencode',
|
||||
runtimeId,
|
||||
provider: runtimeId,
|
||||
version: runtimeVersion
|
||||
}]
|
||||
}, ...additionalRuntimes.map(({ runtimeId, provider, version }) => ({ runtimeId, provider, version }))]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +198,7 @@ function candidateFromCatalogEntry(
|
||||
version: entry.version,
|
||||
minimumDesktopVersion: entry.minimumDesktopVersion,
|
||||
agentProtocol: { ...entry.agentProtocol },
|
||||
...(entry.additionalRuntimes ? { additionalRuntimes: structuredClone(entry.additionalRuntimes) } : {}),
|
||||
remoteRuntime: {
|
||||
...entry.remoteRuntime,
|
||||
protocol: { ...entry.remoteRuntime.protocol }
|
||||
@@ -217,6 +223,7 @@ function candidateFromVerifiedRecord(
|
||||
version: descriptor.version,
|
||||
minimumDesktopVersion: descriptor.minimumDesktopVersion,
|
||||
agentProtocol: { ...descriptor.agentProtocol },
|
||||
...(descriptor.additionalRuntimes ? { additionalRuntimes: structuredClone(descriptor.additionalRuntimes) } : {}),
|
||||
remoteRuntime: {
|
||||
...descriptor.remoteRuntime,
|
||||
protocol: { ...descriptor.remoteRuntime.protocol }
|
||||
@@ -399,7 +406,9 @@ export class AgentPackageManager {
|
||||
return {
|
||||
expected: expectedCatalog(
|
||||
installed.verified.descriptor.version,
|
||||
installed.verified.descriptor.remoteRuntime.version
|
||||
installed.verified.descriptor.remoteRuntime.version,
|
||||
installed.verified.descriptor.remoteRuntime.runtimeId,
|
||||
installed.verified.descriptor.additionalRuntimes
|
||||
),
|
||||
candidate: null,
|
||||
candidateFailure: {
|
||||
@@ -414,7 +423,9 @@ export class AgentPackageManager {
|
||||
}
|
||||
const expected = expectedCatalog(
|
||||
entry.version,
|
||||
entry.remoteRuntime.version
|
||||
entry.remoteRuntime.version,
|
||||
entry.remoteRuntime.runtimeId,
|
||||
entry.additionalRuntimes
|
||||
)
|
||||
try {
|
||||
return {
|
||||
@@ -710,6 +721,7 @@ export class AgentPackageManager {
|
||||
candidate.agentProtocol.major &&
|
||||
descriptor.agentProtocol.minor ===
|
||||
candidate.agentProtocol.minor &&
|
||||
isDeepStrictEqual(descriptor.additionalRuntimes ?? [], candidate.additionalRuntimes ?? []) &&
|
||||
descriptor.remoteRuntime.runtimeId ===
|
||||
candidate.remoteRuntime.runtimeId &&
|
||||
descriptor.remoteRuntime.provider ===
|
||||
@@ -835,6 +847,7 @@ export class AgentPackageManager {
|
||||
entry.agentProtocol.major ||
|
||||
descriptor.agentProtocol.minor !==
|
||||
entry.agentProtocol.minor ||
|
||||
!isDeepStrictEqual(descriptor.additionalRuntimes ?? [], entry.additionalRuntimes ?? []) ||
|
||||
descriptor.remoteRuntime.runtimeId !==
|
||||
entry.remoteRuntime.runtimeId ||
|
||||
descriptor.remoteRuntime.provider !==
|
||||
@@ -1981,6 +1994,7 @@ function assertCandidateMatchesCatalogEntry(
|
||||
entry.agentProtocol.major ||
|
||||
candidate.agentProtocol.minor !==
|
||||
entry.agentProtocol.minor ||
|
||||
!isDeepStrictEqual(candidate.additionalRuntimes ?? [], entry.additionalRuntimes ?? []) ||
|
||||
candidate.remoteRuntime.runtimeId !==
|
||||
entry.remoteRuntime.runtimeId ||
|
||||
candidate.remoteRuntime.provider !==
|
||||
@@ -2218,4 +2232,4 @@ async function writeAll(
|
||||
}
|
||||
offset += bytesWritten
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,30 +223,35 @@ export async function verifyExtractedAgentPackage(options: {
|
||||
)
|
||||
}
|
||||
}
|
||||
for (const runtime of [descriptor.remoteRuntime, ...(descriptor.additionalRuntimes ?? [])]) {
|
||||
const runtimeBundle =
|
||||
await loadVerifiedRemoteRuntimeResourceBundle(
|
||||
runtimePaths,
|
||||
descriptor.architecture,
|
||||
{
|
||||
runtimeId: runtime.runtimeId,
|
||||
verificationEnvironment:
|
||||
options.verificationEnvironment ?? 'production'
|
||||
}
|
||||
)
|
||||
if (
|
||||
runtimeBundle.manifest.runtimeId !== runtime.runtimeId ||
|
||||
runtimeBundle.manifest.provider !== runtime.provider ||
|
||||
runtimeBundle.manifest.platform !== descriptor.platform ||
|
||||
runtimeBundle.manifest.runtimeVersion !==
|
||||
descriptor.remoteRuntime.version ||
|
||||
runtime.version ||
|
||||
runtimeBundle.manifest.bundleDigest !==
|
||||
descriptor.remoteRuntime.bundleDigest ||
|
||||
runtime.bundleDigest ||
|
||||
runtimeBundle.manifest.protocol.major !==
|
||||
descriptor.remoteRuntime.protocol.major ||
|
||||
runtime.protocol.major ||
|
||||
runtimeBundle.manifest.protocol.minor !==
|
||||
descriptor.remoteRuntime.protocol.minor
|
||||
runtime.protocol.minor
|
||||
) {
|
||||
throw new Error(
|
||||
'Agent package descriptor does not match its Runtime bundle'
|
||||
)
|
||||
}
|
||||
}
|
||||
return {
|
||||
rootDirectory,
|
||||
descriptor,
|
||||
@@ -765,4 +770,4 @@ export function compareSemanticVersions(
|
||||
return leftIdentifier > rightIdentifier ? 1 : -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { join } from 'node:path'
|
||||
import { dirname, join } from 'node:path'
|
||||
import type { AgentArchitecture } from '../../shared/agent-installation-contracts'
|
||||
|
||||
export type BundledRemoteRuntimeResourcePaths = {
|
||||
@@ -72,7 +72,8 @@ export function resolveBundledRemoteRuntimeResourcePaths(
|
||||
|
||||
export function getBundledRemoteRuntimeRoot(
|
||||
paths: BundledRemoteRuntimeResourcePaths,
|
||||
architecture: AgentArchitecture
|
||||
architecture: AgentArchitecture,
|
||||
runtimeId: 'opencode' | 'continue' = 'opencode'
|
||||
): string {
|
||||
return paths.runtimeRoots[architecture]
|
||||
return runtimeId === 'opencode' ? paths.runtimeRoots[architecture] : join(dirname(paths.runtimeRoots[architecture]), runtimeId)
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export type PackageInstallerAgentIdentity = {
|
||||
}
|
||||
|
||||
export type PackageInstallerRuntimeIdentity = {
|
||||
runtimeId: 'opencode'
|
||||
runtimeId: 'opencode' | 'continue'
|
||||
runtimeVersion: string
|
||||
bundleDigest: string
|
||||
manifestDigest: string
|
||||
@@ -121,6 +121,7 @@ export type PackageInstallerResult = {
|
||||
archiveSha256: string
|
||||
agent: PackageInstallerAgentIdentity
|
||||
runtime: PackageInstallerRuntimeIdentity
|
||||
additionalRuntimes?: PackageInstallerRuntimeIdentity[]
|
||||
}
|
||||
|
||||
export type PackageInstallerEvent =
|
||||
@@ -166,6 +167,7 @@ type VerifiedPackage = {
|
||||
runtimeManifestDigest: string
|
||||
agent: PackageInstallerAgentIdentity
|
||||
runtime: PackageInstallerRuntimeIdentity
|
||||
additionalRuntimes?: PackageInstallerRuntimeIdentity[]
|
||||
}
|
||||
|
||||
type PreparedState = {
|
||||
@@ -178,6 +180,7 @@ type PreparedState = {
|
||||
packageSignatureSha256: string
|
||||
agent: PackageInstallerAgentIdentity
|
||||
runtime: PackageInstallerRuntimeIdentity
|
||||
additionalRuntimes?: PackageInstallerRuntimeIdentity[]
|
||||
}
|
||||
|
||||
function canonicalJson(value: unknown): string {
|
||||
@@ -811,6 +814,7 @@ function assertOuterDescriptor(
|
||||
'signingKeyId',
|
||||
'agentProtocol',
|
||||
'remoteRuntime',
|
||||
...(descriptor.additionalRuntimes === undefined ? [] : ['additionalRuntimes']),
|
||||
'contentDigest',
|
||||
'files'
|
||||
],
|
||||
@@ -839,8 +843,8 @@ function assertOuterDescriptor(
|
||||
'Agent package Runtime identity'
|
||||
)
|
||||
if (
|
||||
remoteRuntime.runtimeId !== 'opencode' ||
|
||||
remoteRuntime.provider !== 'opencode' ||
|
||||
!['opencode', 'continue'].includes(String(remoteRuntime.runtimeId)) ||
|
||||
remoteRuntime.provider !== remoteRuntime.runtimeId ||
|
||||
!versionPattern.test(String(remoteRuntime.version)) ||
|
||||
!digestPattern.test(String(remoteRuntime.bundleDigest))
|
||||
) {
|
||||
@@ -848,6 +852,18 @@ function assertOuterDescriptor(
|
||||
}
|
||||
protocol(descriptor.agentProtocol, 'Agent package protocol')
|
||||
protocol(remoteRuntime.protocol, 'Agent package Runtime protocol')
|
||||
if (descriptor.additionalRuntimes !== undefined) {
|
||||
if (!Array.isArray(descriptor.additionalRuntimes) || descriptor.additionalRuntimes.length > 1) throw new Error('Invalid additional Runtimes')
|
||||
for (const extra of descriptor.additionalRuntimes) {
|
||||
if (!isRecord(extra)) throw new Error('Invalid additional Runtime')
|
||||
exactKeys(extra, ['runtimeId', 'provider', 'version', 'bundleDigest', 'protocol'], 'Additional Runtime')
|
||||
if (!['opencode', 'continue'].includes(String(extra.runtimeId)) || extra.runtimeId === remoteRuntime.runtimeId ||
|
||||
extra.provider !== extra.runtimeId || !versionPattern.test(String(extra.version)) || !digestPattern.test(String(extra.bundleDigest))) {
|
||||
throw new Error('Invalid additional Runtime identity')
|
||||
}
|
||||
protocol(extra.protocol, 'Additional Runtime protocol')
|
||||
}
|
||||
}
|
||||
const { contentDigest: _contentDigest, ...content } = descriptor
|
||||
void _contentDigest
|
||||
if (
|
||||
@@ -864,6 +880,11 @@ function assertOuterDescriptor(
|
||||
'Agent package'
|
||||
)
|
||||
const files = parseFiles(descriptor.files, 'Agent package')
|
||||
const runtimePrefixes = [remoteRuntime, ...((descriptor.additionalRuntimes as JsonRecord[] | undefined) ?? [])]
|
||||
.map(runtime => `runtime/${String(runtime.runtimeId)}/${String(runtime.bundleDigest).slice(7)}/`)
|
||||
if (files.some(file => file.path.startsWith('runtime/') && !runtimePrefixes.some(prefix => file.path.startsWith(prefix)))) {
|
||||
throw new Error('Agent package contains an undeclared Runtime')
|
||||
}
|
||||
const expected = new Set([
|
||||
...files.map((file) => file.path),
|
||||
PACKAGE_DESCRIPTOR,
|
||||
@@ -1093,8 +1114,9 @@ function assertRuntimeManifest(
|
||||
],
|
||||
'Runtime manifest'
|
||||
)
|
||||
const runtime = isRecord(lock.runtimes) && isRecord(lock.runtimes.opencode)
|
||||
? lock.runtimes.opencode
|
||||
const remote = descriptor.remoteRuntime as JsonRecord
|
||||
const runtime = isRecord(lock.runtimes) && isRecord(lock.runtimes[String(remote.runtimeId)])
|
||||
? lock.runtimes[String(remote.runtimeId)] as JsonRecord
|
||||
: undefined
|
||||
const targetKey = descriptor.platform === 'darwin' ? 'darwin-arm64' : String(descriptor.architecture)
|
||||
const target =
|
||||
@@ -1103,12 +1125,11 @@ function assertRuntimeManifest(
|
||||
isRecord(runtime.targets[targetKey])
|
||||
? runtime.targets[targetKey]
|
||||
: undefined
|
||||
const remote = descriptor.remoteRuntime as JsonRecord
|
||||
if (
|
||||
manifest.formatVersion !== 2 ||
|
||||
manifest.product !== 'GoodBuddy' ||
|
||||
manifest.runtimeId !== 'opencode' ||
|
||||
manifest.provider !== 'opencode' ||
|
||||
manifest.runtimeId !== remote.runtimeId ||
|
||||
manifest.provider !== remote.provider ||
|
||||
manifest.platform !== descriptor.platform ||
|
||||
manifest.architecture !== descriptor.architecture ||
|
||||
!versionPattern.test(String(manifest.runtimeVersion)) ||
|
||||
@@ -1195,14 +1216,14 @@ function assertRuntimeManifest(
|
||||
)
|
||||
const files = parseFiles(manifest.files, 'Runtime manifest')
|
||||
const digestDirectory = String(manifest.bundleDigest).slice('sha256:'.length)
|
||||
const prefix = `runtime/opencode/${digestDirectory}/`
|
||||
const prefix = `runtime/${String(remote.runtimeId)}/${digestDirectory}/`
|
||||
const expectedRuntimePaths = new Set([
|
||||
...files.map((file) => `${prefix}${file.path}`),
|
||||
`${prefix}manifest.json`,
|
||||
`${prefix}manifest.sig`
|
||||
])
|
||||
const actualRuntimePaths = [...packageFiles.keys()].filter((path) =>
|
||||
path.startsWith('runtime/')
|
||||
path.startsWith(`runtime/${String(remote.runtimeId)}/`)
|
||||
)
|
||||
if (
|
||||
actualRuntimePaths.length !== expectedRuntimePaths.size ||
|
||||
@@ -1214,7 +1235,7 @@ function assertRuntimeManifest(
|
||||
if (!safeManifestPath(file.path)) {
|
||||
throw new Error(`Runtime manifest path is invalid: ${file.path}`)
|
||||
}
|
||||
const expectedMode = file.path === 'bin/opencode' ? '0755' : '0644'
|
||||
const expectedMode = file.path === entrypoint.path ? '0755' : '0644'
|
||||
const outer = packageFiles.get(`${prefix}${file.path}`)
|
||||
if (
|
||||
file.mode !== expectedMode ||
|
||||
@@ -1232,20 +1253,20 @@ function assertRuntimeManifest(
|
||||
'Runtime manifest'
|
||||
)
|
||||
const openCodeLicense = runtimeLicenses.find(
|
||||
(license) => license.package === 'opencode-ai'
|
||||
(license) => license.package === (remote.runtimeId === 'continue' ? '@continuedev/cli' : 'opencode-ai')
|
||||
)
|
||||
if (
|
||||
openCodeLicense?.version !== manifest.runtimeVersion ||
|
||||
openCodeLicense?.spdx !== 'MIT' ||
|
||||
openCodeLicense?.path !== 'licenses/opencode-MIT.txt'
|
||||
openCodeLicense?.spdx !== (remote.runtimeId === 'continue' ? 'Apache-2.0' : 'MIT') ||
|
||||
openCodeLicense?.path !== (remote.runtimeId === 'continue' ? 'licenses/continue-Apache-2.0.txt' : 'licenses/opencode-MIT.txt')
|
||||
) {
|
||||
throw new Error('Runtime license declaration is invalid')
|
||||
}
|
||||
if (
|
||||
entrypoint.path !== 'bin/opencode' ||
|
||||
entrypoint.path !== (remote.runtimeId === 'continue' ? 'lib/continue/dist/cn.js' : 'bin/opencode') ||
|
||||
!files.some(
|
||||
(file) =>
|
||||
file.path === 'bin/opencode' &&
|
||||
file.path === entrypoint.path &&
|
||||
file.sha256 === entrypoint.sha256
|
||||
)
|
||||
) {
|
||||
@@ -1299,12 +1320,13 @@ function elfArchitecture(bytes: Buffer, platform: Platform): Architecture | unde
|
||||
function assertExtractedArchitectures(
|
||||
preparedRoot: string,
|
||||
architecture: Architecture,
|
||||
platform: Platform
|
||||
platform: Platform,
|
||||
runtimeId: 'opencode' | 'continue'
|
||||
): void {
|
||||
for (const path of [
|
||||
join(preparedRoot, 'agent', 'node'),
|
||||
...nativeKoffiPaths(platform, architecture).map(path => join(preparedRoot, 'agent', path)),
|
||||
join(preparedRoot, 'runtime', 'bin', 'opencode')
|
||||
...(runtimeId === 'opencode' ? [join(preparedRoot, 'runtime', 'bin', 'opencode')] : [])
|
||||
]) {
|
||||
if (
|
||||
elfArchitecture(readFileHeader(path, 64), platform) !== architecture
|
||||
@@ -1460,13 +1482,14 @@ function verifyArchive(
|
||||
const runtimeDigest = String(
|
||||
(descriptor.remoteRuntime as JsonRecord).bundleDigest
|
||||
).slice('sha256:'.length)
|
||||
const runtimeId = (descriptor.remoteRuntime as JsonRecord).runtimeId as 'opencode' | 'continue'
|
||||
const runtimeManifestBytes = readEntry(
|
||||
handle,
|
||||
entries,
|
||||
`runtime/opencode/${runtimeDigest}/manifest.json`
|
||||
`runtime/${runtimeId}/${runtimeDigest}/manifest.json`
|
||||
)
|
||||
assertDeclaredMetadata(
|
||||
`runtime/opencode/${runtimeDigest}/manifest.json`,
|
||||
`runtime/${runtimeId}/${runtimeDigest}/manifest.json`,
|
||||
runtimeManifestBytes,
|
||||
declaredMap
|
||||
)
|
||||
@@ -1481,7 +1504,7 @@ function verifyArchive(
|
||||
declaredMap
|
||||
)
|
||||
const runtimeSignaturePath =
|
||||
`runtime/opencode/${runtimeDigest}/manifest.sig`
|
||||
`runtime/${runtimeId}/${runtimeDigest}/manifest.sig`
|
||||
const runtimeSignatureBytes = readEntry(
|
||||
handle,
|
||||
entries,
|
||||
@@ -1503,6 +1526,16 @@ function verifyArchive(
|
||||
descriptor,
|
||||
declaredMap
|
||||
)
|
||||
const additionalRuntimes = (descriptor.additionalRuntimes as JsonRecord[] | undefined)?.map(remote => {
|
||||
const prefix = `runtime/${String(remote.runtimeId)}/${String(remote.bundleDigest).slice(7)}/`
|
||||
const bytes = readEntry(handle, entries, `${prefix}manifest.json`)
|
||||
const signature = readEntry(handle, entries, `${prefix}manifest.sig`)
|
||||
assertDeclaredMetadata(`${prefix}manifest.json`, bytes, declaredMap)
|
||||
assertDeclaredMetadata(`${prefix}manifest.sig`, signature, declaredMap)
|
||||
const verified = assertRuntimeManifest(bytes, signature, registry,
|
||||
parseCanonicalJson(remoteRuntimeLockBytes, 'Remote Runtime lock'), { ...descriptor, remoteRuntime: remote }, declaredMap)
|
||||
return runtimeIdentity(verified, descriptor)
|
||||
})
|
||||
if (!sameSnapshot(snapshot, fstatSync(handle))) {
|
||||
throw new Error('Agent package archive changed during verification')
|
||||
}
|
||||
@@ -1522,6 +1555,7 @@ function verifyArchive(
|
||||
runtimeManifest: runtime.manifest,
|
||||
agentManifestSha256,
|
||||
runtimeManifestDigest: runtime.manifestDigest,
|
||||
...(additionalRuntimes ? { additionalRuntimes } : {}),
|
||||
agent: {
|
||||
installationId: `agent-${agentManifestSha256}`,
|
||||
agentVersion: String(agentManifest.agentVersion),
|
||||
@@ -1533,7 +1567,7 @@ function verifyArchive(
|
||||
supervisor: 'detached-on-demand'
|
||||
},
|
||||
runtime: {
|
||||
runtimeId: 'opencode',
|
||||
runtimeId,
|
||||
runtimeVersion: String(runtime.manifest.runtimeVersion),
|
||||
bundleDigest: String(runtime.manifest.bundleDigest),
|
||||
manifestDigest: runtime.manifestDigest,
|
||||
@@ -1646,7 +1680,8 @@ function persistPreparedState(
|
||||
packageSignatureSha256:
|
||||
sha256(verified.packageSignatureBytes),
|
||||
agent: verified.agent,
|
||||
runtime: verified.runtime
|
||||
runtime: verified.runtime,
|
||||
...(verified.additionalRuntimes ? { additionalRuntimes: verified.additionalRuntimes } : {})
|
||||
}
|
||||
const bytes = prettyBytes(state)
|
||||
if (bytes.length > MAXIMUM_METADATA_BYTES) {
|
||||
@@ -1728,7 +1763,10 @@ function extractPayload(
|
||||
mkdirSync(join(temporary, 'agent'), { recursive: true, mode: 0o700 })
|
||||
mkdirSync(join(temporary, 'runtime'), { recursive: true, mode: 0o700 })
|
||||
const runtimePrefix =
|
||||
`runtime/opencode/${verified.runtime.bundleDigest.slice('sha256:'.length)}/`
|
||||
`runtime/${verified.runtime.runtimeId}/${verified.runtime.bundleDigest.slice('sha256:'.length)}/`
|
||||
const extraPrefixes = (verified.additionalRuntimes ?? []).map(runtime => ({
|
||||
prefix: `runtime/${runtime.runtimeId}/${runtime.bundleDigest.slice(7)}/`, directory: `runtime-${runtime.runtimeId}`
|
||||
}))
|
||||
const declared = new Map(
|
||||
parseFiles(verified.descriptor.files, 'Agent package')
|
||||
.map((file) => [file.path, file])
|
||||
@@ -1757,6 +1795,9 @@ function extractPayload(
|
||||
...name.slice(runtimePrefix.length).split('/')
|
||||
)
|
||||
}
|
||||
for (const extra of extraPrefixes) {
|
||||
if (name.startsWith(extra.prefix)) target = join(temporary, extra.directory, ...name.slice(extra.prefix.length).split('/'))
|
||||
}
|
||||
if (target !== undefined) {
|
||||
const actual = streamEntry(handle, entry, target)
|
||||
const expected = declared.get(name)
|
||||
@@ -1777,7 +1818,11 @@ function extractPayload(
|
||||
} finally {
|
||||
closeSync(handle)
|
||||
}
|
||||
assertExtractedArchitectures(temporary, verified.agent.architecture, verified.agent.platform)
|
||||
assertExtractedArchitectures(temporary, verified.agent.architecture, verified.agent.platform, verified.runtime.runtimeId)
|
||||
if (verified.additionalRuntimes?.some(runtime => runtime.runtimeId === 'opencode') &&
|
||||
elfArchitecture(readFileHeader(join(temporary, 'runtime-opencode/bin/opencode'), 64), verified.agent.platform) !== verified.agent.architecture) {
|
||||
throw new Error('Additional OpenCode executable architecture mismatch')
|
||||
}
|
||||
chmodSync(temporary, 0o700)
|
||||
renameSync(temporary, destinationRoot)
|
||||
} catch (error) {
|
||||
@@ -1812,7 +1857,8 @@ export function preparePackage(options: InstallerOptions): PackageInstallerResul
|
||||
status: 'prepared',
|
||||
archiveSha256: state.archiveSha256,
|
||||
agent: state.agent,
|
||||
runtime: state.runtime
|
||||
runtime: state.runtime,
|
||||
...(state.additionalRuntimes ? { additionalRuntimes: state.additionalRuntimes } : {})
|
||||
}
|
||||
options.emit?.(result)
|
||||
return result
|
||||
@@ -1843,7 +1889,8 @@ export function preparePackage(options: InstallerOptions): PackageInstallerResul
|
||||
status: 'prepared',
|
||||
archiveSha256: verified.archiveSha256,
|
||||
agent: verified.agent,
|
||||
runtime: verified.runtime
|
||||
runtime: verified.runtime,
|
||||
...(verified.additionalRuntimes ? { additionalRuntimes: verified.additionalRuntimes } : {})
|
||||
}
|
||||
options.emit?.(result)
|
||||
return result
|
||||
@@ -2070,6 +2117,7 @@ function readPreparedState(path: string): PreparedState {
|
||||
'packageSignatureSha256',
|
||||
'agent',
|
||||
'runtime'
|
||||
,...(value.additionalRuntimes === undefined ? [] : ['additionalRuntimes'])
|
||||
],
|
||||
'Prepared package state'
|
||||
)
|
||||
@@ -2097,6 +2145,11 @@ function readPreparedState(path: string): PreparedState {
|
||||
throw new Error('Prepared package state is invalid')
|
||||
}
|
||||
const agent = value.agent
|
||||
if (value.additionalRuntimes !== undefined && (!Array.isArray(value.additionalRuntimes) || value.additionalRuntimes.length > 1 ||
|
||||
value.additionalRuntimes.some(extra => !isRecord(extra) || !['opencode', 'continue'].includes(String(extra.runtimeId)) ||
|
||||
extra.runtimeId === (value.runtime as JsonRecord).runtimeId || !digestPattern.test(String(extra.bundleDigest))))) {
|
||||
throw new Error('Invalid prepared additional Runtimes')
|
||||
}
|
||||
const runtime = value.runtime
|
||||
exactKeys(
|
||||
agent,
|
||||
@@ -2141,7 +2194,7 @@ function readPreparedState(path: string): PreparedState {
|
||||
!isSupportedTarget(agent.platform, agent.architecture) ||
|
||||
!['x64', 'arm64'].includes(String(agent.architecture)) ||
|
||||
agent.supervisor !== 'detached-on-demand' ||
|
||||
runtime.runtimeId !== 'opencode' ||
|
||||
!['opencode', 'continue'].includes(String(runtime.runtimeId)) ||
|
||||
!versionPattern.test(String(runtime.runtimeVersion)) ||
|
||||
!digestPattern.test(String(runtime.bundleDigest)) ||
|
||||
!digestPattern.test(String(runtime.manifestDigest)) ||
|
||||
@@ -2155,6 +2208,7 @@ function readPreparedState(path: string): PreparedState {
|
||||
return {
|
||||
formatVersion: 1,
|
||||
archiveSha256: String(value.archiveSha256),
|
||||
...(value.additionalRuntimes ? { additionalRuntimes: value.additionalRuntimes as PackageInstallerRuntimeIdentity[] } : {}),
|
||||
releaseKeyRegistrySha256:
|
||||
String(value.releaseKeyRegistrySha256),
|
||||
agentRuntimeLockSha256:
|
||||
@@ -2176,7 +2230,7 @@ function readPreparedState(path: string): PreparedState {
|
||||
supervisor: 'detached-on-demand'
|
||||
},
|
||||
runtime: {
|
||||
runtimeId: 'opencode',
|
||||
runtimeId: runtime.runtimeId as 'opencode' | 'continue',
|
||||
runtimeVersion: String(runtime.runtimeVersion),
|
||||
bundleDigest: String(runtime.bundleDigest),
|
||||
manifestDigest: String(runtime.manifestDigest),
|
||||
@@ -2230,6 +2284,16 @@ function readBoundedRegularFile(
|
||||
}
|
||||
}
|
||||
|
||||
function runtimeIdentity(runtime: { manifest: JsonRecord; manifestDigest: string }, descriptor: JsonRecord): PackageInstallerRuntimeIdentity {
|
||||
return {
|
||||
runtimeId: runtime.manifest.runtimeId as 'opencode' | 'continue',
|
||||
runtimeVersion: String(runtime.manifest.runtimeVersion), bundleDigest: String(runtime.manifest.bundleDigest),
|
||||
manifestDigest: runtime.manifestDigest, runtimeAdapterDigest: String(runtime.manifest.adapterDigest),
|
||||
acpCapabilitiesDigest: String(runtime.manifest.acpCapabilitiesDigest), platform: descriptor.platform as Platform,
|
||||
architecture: descriptor.architecture as Architecture, protocol: protocol(runtime.manifest.protocol, 'Runtime protocol')
|
||||
}
|
||||
}
|
||||
|
||||
function assertPreparedIdentity(
|
||||
agentRoot: string,
|
||||
runtimeRoot: string,
|
||||
@@ -2238,12 +2302,17 @@ function assertPreparedIdentity(
|
||||
agentRuntimeLockBytes: Buffer,
|
||||
remoteRuntimeLockBytes: Buffer,
|
||||
descriptorBytes: Buffer,
|
||||
packageSignatureBytes: Buffer
|
||||
packageSignatureBytes: Buffer,
|
||||
selectedRuntime = state.runtime
|
||||
): void {
|
||||
const descriptor = parseCanonicalJson(
|
||||
descriptorBytes,
|
||||
'Prepared Agent package descriptor'
|
||||
)
|
||||
const packagedRuntimes = [descriptor.remoteRuntime, ...((descriptor.additionalRuntimes as JsonRecord[] | undefined) ?? [])] as JsonRecord[]
|
||||
if (packagedRuntimes.length !== 1 + (state.additionalRuntimes?.length ?? 0)) throw new Error('Prepared Runtime inventory differs from package')
|
||||
const remoteRuntime = packagedRuntimes.find(runtime => runtime.runtimeId === selectedRuntime.runtimeId)
|
||||
if (!remoteRuntime) throw new Error('Prepared Runtime is not declared in package')
|
||||
const declared = parseFiles(
|
||||
descriptor.files,
|
||||
'Prepared Agent package'
|
||||
@@ -2323,7 +2392,7 @@ function assertPreparedIdentity(
|
||||
remoteRuntimeLockBytes,
|
||||
'Prepared Remote Runtime lock'
|
||||
),
|
||||
descriptor,
|
||||
{ ...descriptor, remoteRuntime },
|
||||
packageFiles
|
||||
)
|
||||
const agentManifestSha256 = sha256(agentManifestBytes)
|
||||
@@ -2339,7 +2408,7 @@ function assertPreparedIdentity(
|
||||
supervisor: 'detached-on-demand'
|
||||
}
|
||||
const expectedRuntime: PackageInstallerRuntimeIdentity = {
|
||||
runtimeId: 'opencode',
|
||||
runtimeId: runtime.manifest.runtimeId as 'opencode' | 'continue',
|
||||
runtimeVersion: String(runtime.manifest.runtimeVersion),
|
||||
bundleDigest: String(runtime.manifest.bundleDigest),
|
||||
manifestDigest: runtime.manifestDigest,
|
||||
@@ -2356,7 +2425,7 @@ function assertPreparedIdentity(
|
||||
}
|
||||
if (
|
||||
canonicalJson(state.agent) !== canonicalJson(expectedAgent) ||
|
||||
canonicalJson(state.runtime) !== canonicalJson(expectedRuntime)
|
||||
canonicalJson(selectedRuntime) !== canonicalJson(expectedRuntime)
|
||||
) {
|
||||
throw new Error(
|
||||
'Prepared package identity does not match the authenticated archive'
|
||||
@@ -2473,6 +2542,15 @@ export function commitPackage(options: InstallerOptions): PackageInstallerResult
|
||||
join(managedRoot, 'runtimes', 'release-keys.json'),
|
||||
join(managedRoot, 'runtimes', 'remote-runtime-lock.json')
|
||||
] as const
|
||||
const extraPublications = (state.additionalRuntimes ?? []).map(runtime => {
|
||||
ensureOwnedManagedHierarchy(home, runtime.runtimeId)
|
||||
const source = join(payloadRoot, `runtime-${runtime.runtimeId}`)
|
||||
const destination = join(managedRoot, 'runtimes', runtime.runtimeId, runtime.bundleDigest.slice(7))
|
||||
assertPreparedIdentity(agentSourceExists ? agentSource : agentDestination,
|
||||
existsSync(source) ? source : destination, state, releaseKeyRegistryBytes, agentRuntimeLockBytes,
|
||||
remoteRuntimeLockBytes, descriptorBytes, packageSignatureBytes, runtime)
|
||||
return { source, destination }
|
||||
})
|
||||
if (!agentSourceExists || !runtimeSourceExists) {
|
||||
assertPreparedIdentity(
|
||||
agentSourceExists ? agentSource : agentDestination,
|
||||
@@ -2507,6 +2585,9 @@ export function commitPackage(options: InstallerOptions): PackageInstallerResult
|
||||
)
|
||||
)
|
||||
}
|
||||
for (const extra of extraPublications) {
|
||||
if (existsSync(extra.source)) published.push(publishDirectory(extra.source, extra.destination, inputs.operationRoot))
|
||||
}
|
||||
progress('publishing-metadata')
|
||||
writePrivateFileAtomic(
|
||||
metadataPaths[0],
|
||||
@@ -2529,7 +2610,8 @@ export function commitPackage(options: InstallerOptions): PackageInstallerResult
|
||||
status: 'committed',
|
||||
archiveSha256: state.archiveSha256,
|
||||
agent: state.agent,
|
||||
runtime: state.runtime
|
||||
runtime: state.runtime,
|
||||
...(state.additionalRuntimes ? { additionalRuntimes: state.additionalRuntimes } : {})
|
||||
}
|
||||
options.emit?.(result)
|
||||
return result
|
||||
|
||||
@@ -277,19 +277,25 @@ describe('createManagedRemoteAcpRuntime', () => {
|
||||
await runtime.dispose()
|
||||
})
|
||||
|
||||
it('rejects non-OpenCode providers before acquiring remote resources', async () => {
|
||||
it('rejects unsupported providers before acquiring remote resources', async () => {
|
||||
const fixture = harness({
|
||||
selection: { provider: 'continue' }
|
||||
selection: { provider: 'model', profileId: '00000000-0000-4000-8000-000000000001' }
|
||||
})
|
||||
|
||||
await expect(
|
||||
createManagedRemoteAcpRuntime(fixture.options)
|
||||
).rejects.toThrow(/OpenCode Runtime/iu)
|
||||
).rejects.toThrow(/OpenCode or Continue/iu)
|
||||
expect(
|
||||
fixture.options.agentServices.connectionManager.acquire
|
||||
).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects an installed Runtime that differs from the selected Continue provider', async () => {
|
||||
const fixture = harness({ selection: { provider: 'continue' } })
|
||||
await expect(createManagedRemoteAcpRuntime(fixture.options)).rejects.toThrow(/selected provider/iu)
|
||||
expect(fixture.releaseConnection).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('binds the current Agent and Runtime and owns its connection lease', async () => {
|
||||
const fixture = harness()
|
||||
const runtime = await createManagedRemoteAcpRuntime(
|
||||
@@ -307,6 +313,7 @@ describe('createManagedRemoteAcpRuntime', () => {
|
||||
expect(
|
||||
fixture.options.runtimeInstallationManager.activateInstalled
|
||||
).toHaveBeenCalledWith('host-1', {
|
||||
runtimeId: 'opencode',
|
||||
agentInstallationId: 'agent-1'
|
||||
})
|
||||
expect(
|
||||
|
||||
@@ -43,7 +43,7 @@ type ManagedRemoteTransportIdentity = {
|
||||
}
|
||||
|
||||
type ManagedRemoteRuntimeIdentity = {
|
||||
runtimeId: 'opencode'
|
||||
runtimeId: 'opencode' | 'continue'
|
||||
runtimeVersion: string
|
||||
bundleDigest: string
|
||||
runtimeAdapterDigest: string
|
||||
@@ -80,9 +80,9 @@ export async function createManagedRemoteAcpRuntime(
|
||||
options: ManagedRemoteAcpRuntimeOptions
|
||||
): Promise<AgentRuntime> {
|
||||
const descriptor = options.executionSpace
|
||||
if (options.selection.provider !== 'opencode') {
|
||||
if (options.selection.provider !== 'opencode' && options.selection.provider !== 'continue') {
|
||||
throw new Error(
|
||||
'Managed remote execution requires the OpenCode Runtime'
|
||||
'Managed remote execution requires OpenCode or Continue'
|
||||
)
|
||||
}
|
||||
const workspaceBinding = workspaceProjectBinding(descriptor)
|
||||
@@ -114,8 +114,11 @@ export async function createManagedRemoteAcpRuntime(
|
||||
const runtimeInstallation =
|
||||
await options.runtimeInstallationManager.activateInstalled(
|
||||
descriptor.hostId,
|
||||
{ agentInstallationId: agent.installationId }
|
||||
{ agentInstallationId: agent.installationId, runtimeId: options.selection.provider }
|
||||
)
|
||||
if (runtimeInstallation.runtimeId !== options.selection.provider) {
|
||||
throw new Error('Installed Runtime does not match the selected provider')
|
||||
}
|
||||
const capabilities = await activeConnection.refreshCapabilities()
|
||||
assertConnectionMatches(workspaceBinding, agent, activeConnection)
|
||||
const runtime = runtimeIdentityFromCapabilities(
|
||||
@@ -137,8 +140,8 @@ export async function createManagedRemoteAcpRuntime(
|
||||
transport: ManagedRemoteTransportIdentity
|
||||
): AcpRemoteRuntime => {
|
||||
const remoteOptions: AcpRemoteRuntimeOptions = {
|
||||
runtimeId: 'opencode',
|
||||
label: 'OpenCode(远程托管)',
|
||||
runtimeId: runtime.runtimeId,
|
||||
label: `${runtime.runtimeId === 'continue' ? 'Continue' : 'OpenCode'}(远程托管)`,
|
||||
workspacePath: descriptor.remoteRootPath,
|
||||
identity: {
|
||||
controllerId,
|
||||
@@ -180,7 +183,7 @@ export async function createManagedRemoteAcpRuntime(
|
||||
...(options.usage
|
||||
? {
|
||||
usage: {
|
||||
runtime: 'opencode',
|
||||
runtime: runtime.runtimeId,
|
||||
provider: options.usage.provider,
|
||||
model: options.usage.model
|
||||
}
|
||||
@@ -296,7 +299,7 @@ class ManagedRemoteRuntimeResources {
|
||||
connection: this.connection,
|
||||
openIdentity: {
|
||||
bindingId,
|
||||
runtimeId: 'opencode',
|
||||
runtimeId: this.runtime.runtimeId,
|
||||
runtimeBundleDigest: this.runtime.bundleDigest,
|
||||
workspaceIdentity: this.workspaceIdentity
|
||||
}
|
||||
@@ -429,7 +432,7 @@ class ManagedRemoteAcpRuntime implements AgentRuntime {
|
||||
readonly requiresToolApproval = false
|
||||
readonly supportsScopedDataTools = false
|
||||
readonly capability = 'chat' as const
|
||||
readonly runtimeId = 'opencode' as const
|
||||
get runtimeId(): 'opencode' | 'continue' { return this.remote.runtimeId as 'opencode' | 'continue' }
|
||||
#disposePromise?: Promise<void>
|
||||
#transitionTail: Promise<void> = Promise.resolve()
|
||||
#activeRuns = 0
|
||||
@@ -660,7 +663,7 @@ function runtimeIdentityFromCapabilities(
|
||||
>['capabilities']
|
||||
): ManagedRemoteRuntimeIdentity {
|
||||
if (
|
||||
installation.runtimeId !== 'opencode' ||
|
||||
(installation.runtimeId !== 'opencode' && installation.runtimeId !== 'continue') ||
|
||||
(installation.platform !== 'linux' && !(installation.platform === 'darwin' && installation.architecture === 'arm64'))
|
||||
) {
|
||||
throw new Error(
|
||||
@@ -668,7 +671,7 @@ function runtimeIdentityFromCapabilities(
|
||||
)
|
||||
}
|
||||
const runtime: ManagedRemoteRuntimeIdentity = {
|
||||
runtimeId: 'opencode',
|
||||
runtimeId: installation.runtimeId,
|
||||
runtimeVersion: installation.runtimeVersion,
|
||||
bundleDigest: installation.bundleDigest,
|
||||
runtimeAdapterDigest: installation.runtimeAdapterDigest,
|
||||
@@ -718,7 +721,7 @@ function assertRuntimeAdvertised(
|
||||
(entry) => entry.name === RUNTIME_ACP_CAPABILITY.name
|
||||
)
|
||||
const advertised = capabilities.runtimes.filter(
|
||||
(entry) => entry.runtimeId === 'opencode'
|
||||
(entry) => entry.runtimeId === runtime.runtimeId
|
||||
)
|
||||
if (
|
||||
capability === undefined ||
|
||||
|
||||
@@ -93,7 +93,7 @@ export class ManagedRemoteExecutionServices {
|
||||
)
|
||||
).installationId
|
||||
signal.throwIfAborted()
|
||||
if (runtimeId !== 'opencode') {
|
||||
if (runtimeId !== 'opencode' && runtimeId !== 'continue') {
|
||||
throw new Error(
|
||||
'Remote Runtime identity is unsupported'
|
||||
)
|
||||
|
||||
@@ -186,6 +186,7 @@ describe('ManagedRemoteProjectRuntimeValidator', () => {
|
||||
|
||||
expect(test.calls).toEqual(['model', 'activate', 'refresh'])
|
||||
expect(test.activateInstalled).toHaveBeenCalledWith('host-1', {
|
||||
runtimeId: 'opencode',
|
||||
agentInstallationId,
|
||||
signal: test.input.signal
|
||||
})
|
||||
@@ -209,7 +210,7 @@ describe('ManagedRemoteProjectRuntimeValidator', () => {
|
||||
expect(() => lease.assertCurrent()).toThrow(/not ready/iu)
|
||||
})
|
||||
|
||||
it('rejects non-OpenCode selections before installation', async () => {
|
||||
it('rejects unsupported selections before installation', async () => {
|
||||
const test = harness({
|
||||
selection: {
|
||||
provider: 'model',
|
||||
@@ -218,12 +219,28 @@ describe('ManagedRemoteProjectRuntimeValidator', () => {
|
||||
})
|
||||
|
||||
await expect(test.validator.validate(test.input)).rejects.toThrow(
|
||||
/OpenCode Runtime/iu
|
||||
/OpenCode or Continue/iu
|
||||
)
|
||||
expect(test.activateInstalled).not.toHaveBeenCalled()
|
||||
expect(test.refreshCapabilities).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('validates Continue against its own installed bundle and capability', async () => {
|
||||
const refreshed = advertisedCapabilities()
|
||||
refreshed.runtimes[0]!.runtimeId = 'continue'
|
||||
const test = harness({
|
||||
selection: { provider: 'continue' },
|
||||
installed: { ...installation, runtimeId: 'continue' },
|
||||
refreshed
|
||||
})
|
||||
const lease = await test.validator.validate(test.input)
|
||||
expect(test.activateInstalled).toHaveBeenCalledWith('host-1', {
|
||||
runtimeId: 'continue', agentInstallationId, signal: test.input.signal
|
||||
})
|
||||
expect(() => lease.assertCurrent()).not.toThrow()
|
||||
lease.release()
|
||||
})
|
||||
|
||||
it('requires the installed Runtime architecture to exactly match the Agent', async () => {
|
||||
const test = harness({ architecture: 'arm64' })
|
||||
|
||||
|
||||
@@ -46,10 +46,10 @@ export class ManagedRemoteProjectRuntimeValidator
|
||||
): Promise<RemoteProjectRuntimeValidationLease> {
|
||||
input.signal.throwIfAborted()
|
||||
if (
|
||||
input.selection.provider !== 'opencode'
|
||||
input.selection.provider !== 'opencode' && input.selection.provider !== 'continue'
|
||||
) {
|
||||
throw new Error(
|
||||
'Managed remote projects require the OpenCode Runtime'
|
||||
'Managed remote projects require OpenCode or Continue'
|
||||
)
|
||||
}
|
||||
assertInputIdentity(input)
|
||||
@@ -72,7 +72,8 @@ export class ManagedRemoteProjectRuntimeValidator
|
||||
input.host.hostId,
|
||||
{
|
||||
signal: input.signal,
|
||||
agentInstallationId: input.agent.installationId
|
||||
agentInstallationId: input.agent.installationId,
|
||||
runtimeId: input.selection.provider
|
||||
}
|
||||
)
|
||||
input.signal.throwIfAborted()
|
||||
@@ -164,7 +165,7 @@ function assertInstallationIdentity(
|
||||
installation: RemoteRuntimeInstallationIdentity
|
||||
): void {
|
||||
if (
|
||||
installation.runtimeId !== 'opencode' ||
|
||||
installation.runtimeId !== input.selection.provider ||
|
||||
installation.platform !== input.agent.platform ||
|
||||
installation.architecture !== input.agent.architecture
|
||||
) {
|
||||
@@ -186,7 +187,7 @@ function assertRuntimeCapability(
|
||||
capability.name === RUNTIME_MODEL_BRIDGE_CAPABILITY_NAME
|
||||
)
|
||||
const runtimes = capabilities.runtimes.filter(
|
||||
(runtime) => runtime.runtimeId === 'opencode'
|
||||
(runtime) => runtime.runtimeId === installation.runtimeId
|
||||
)
|
||||
if (
|
||||
acp === undefined ||
|
||||
|
||||
@@ -594,6 +594,24 @@ describe('ProtocolRemoteRuntimeChannel', () => {
|
||||
await fixture.channel.close()
|
||||
})
|
||||
|
||||
it('allows cold native initialization beyond the ordinary control timeout without resending', async () => {
|
||||
const fixture = await openChannel()
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
fixture.client.responder = async (method, params) => {
|
||||
if (method === 'runtime/startPrompt') await new Promise(resolve => setTimeout(resolve, 20_000))
|
||||
return defaultResponse(method, params)
|
||||
}
|
||||
const started = fixture.channel.startOwnedPrompt({
|
||||
bindingId: 'binding-1', operationId: 'operation-1', requestId: 'operation-1',
|
||||
prompt: [{ type: 'text', text: 'hello' }]
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(20_000)
|
||||
await expect(started).resolves.toMatchObject({ sessionId: 'session-owned-1' })
|
||||
expect(fixture.client.requests.filter(request => request.method === 'runtime/startPrompt')).toHaveLength(1)
|
||||
} finally { vi.useRealTimers(); await fixture.channel.close() }
|
||||
})
|
||||
|
||||
it('attaches the exact operation when a prompt start response is lost', async () => {
|
||||
const fixture = await openChannel()
|
||||
const deadlineAt = new Date(Date.now() + 10_000).toISOString()
|
||||
|
||||
@@ -1116,7 +1116,10 @@ export class ProtocolRemoteRuntimeChannel
|
||||
this.#state.client,
|
||||
method,
|
||||
params,
|
||||
this.#state.controlTimeoutMs,
|
||||
// Cold OpenCode ACP initialization on the shared Host exceeds 15 seconds.
|
||||
method === 'runtime/startPrompt'
|
||||
? Math.max(this.#state.controlTimeoutMs, 30_000)
|
||||
: this.#state.controlTimeoutMs,
|
||||
this.#lifetime.signal
|
||||
)
|
||||
this.#assertCurrent()
|
||||
|
||||
@@ -389,21 +389,23 @@ export class RemoteEnvironmentPreparer implements Preparer {
|
||||
signal.throwIfAborted()
|
||||
|
||||
emit(method, 'installing-runtime')
|
||||
for (const runtime of [identity.runtime, ...(identity.additionalRuntimes ?? [])]) {
|
||||
assertAgentCommandSucceeded(
|
||||
await lease.runAgentRuntimeAction(
|
||||
installationId,
|
||||
{
|
||||
kind: 'runtime-activate',
|
||||
runtimeId: identity.runtime.runtimeId,
|
||||
bundleDigest: identity.runtime.bundleDigest,
|
||||
architecture: identity.runtime.architecture,
|
||||
runtimeId: runtime.runtimeId,
|
||||
bundleDigest: runtime.bundleDigest,
|
||||
architecture: runtime.architecture,
|
||||
forceVerification: true
|
||||
},
|
||||
signal
|
||||
),
|
||||
'OpenCode Runtime 激活'
|
||||
`${runtime.runtimeId} Runtime 激活`
|
||||
)
|
||||
signal.throwIfAborted()
|
||||
}
|
||||
}
|
||||
|
||||
async #prepareUploadedArchive(options: {
|
||||
@@ -693,6 +695,14 @@ function assertPackageIdentity(
|
||||
identity: SshRemotePackageIdentity,
|
||||
candidate: VerifiedRemoteAgentInstallCandidate
|
||||
): void {
|
||||
const extras = identity.additionalRuntimes ?? []
|
||||
const expected = candidate.additionalRuntimes ?? []
|
||||
if (extras.length !== expected.length || extras.some((runtime, index) => {
|
||||
const item = expected[index]!
|
||||
return runtime.runtimeId !== item.runtimeId || runtime.runtimeVersion !== item.version ||
|
||||
runtime.bundleDigest !== item.bundleDigest || runtime.platform !== candidate.platform ||
|
||||
runtime.architecture !== candidate.architecture || !isDeepStrictEqual(runtime.protocol, item.protocol)
|
||||
})) throw new Error('Additional Runtime identity does not match the signed package')
|
||||
if (
|
||||
identity.archiveSha256 !== candidate.sha256 ||
|
||||
identity.agent.agentVersion !== candidate.version ||
|
||||
|
||||
@@ -21,7 +21,6 @@ import { remoteHostTargetIdentityKey } from './remote-host-target-identity'
|
||||
const MAXIMUM_METADATA_BYTES = 1024 * 1024
|
||||
const RUNTIME_REGISTRY_PATH =
|
||||
'.goodbuddy/runtimes/registry.json'
|
||||
const OPENCODE_ROOT = '.goodbuddy/runtimes/opencode'
|
||||
|
||||
export type RemoteRuntimeInstallationPhase =
|
||||
| 'inspecting-host'
|
||||
@@ -45,6 +44,7 @@ export interface RemoteRuntimeInstallationTargetResolver {
|
||||
}
|
||||
|
||||
export type RemoteRuntimeActivationRequestOptions = {
|
||||
runtimeId?: 'opencode' | 'continue'
|
||||
signal?: AbortSignal
|
||||
onProgress?: (phase: RemoteRuntimeInstallationPhase) => void
|
||||
agentInstallationId?: VerifiedAgentInstallationId
|
||||
@@ -117,7 +117,8 @@ export class RemoteRuntimeInstallationManager {
|
||||
this.#coordinator.assertAvailable()
|
||||
options.signal?.throwIfAborted()
|
||||
emitOne(options.onProgress, 'inspecting-host')
|
||||
const agentKey = options.agentInstallationId ?? ''
|
||||
const runtimeId = options.runtimeId ?? 'opencode'
|
||||
const agentKey = `${options.agentInstallationId ?? ''}\0${runtimeId}`
|
||||
const earlyCached =
|
||||
this.#coordinator.cachedForHost(hostId, agentKey)
|
||||
if (earlyCached !== undefined) {
|
||||
@@ -154,6 +155,7 @@ export class RemoteRuntimeInstallationManager {
|
||||
this.#activateRegistered(
|
||||
target,
|
||||
options.agentInstallationId,
|
||||
runtimeId,
|
||||
signal,
|
||||
progress
|
||||
)
|
||||
@@ -171,6 +173,7 @@ export class RemoteRuntimeInstallationManager {
|
||||
async #activateRegistered(
|
||||
target: SshConnectionPoolTarget,
|
||||
agentInstallationId: VerifiedAgentInstallationId | undefined,
|
||||
runtimeId: 'opencode' | 'continue',
|
||||
signal: AbortSignal,
|
||||
progress: (phase: RemoteRuntimeInstallationPhase) => void
|
||||
): Promise<RemoteRuntimeInstallationIdentity> {
|
||||
@@ -182,7 +185,7 @@ export class RemoteRuntimeInstallationManager {
|
||||
const probe = await lease.runAgentBootstrapProbe(signal)
|
||||
if (!probe.ready) {
|
||||
throw new RemoteRuntimeInstallationError(
|
||||
`Remote host cannot activate OpenCode Runtime: ${probe.reason}`,
|
||||
`Remote host cannot activate ${runtimeId} Runtime: ${probe.reason}`,
|
||||
'incompatible',
|
||||
probe.reason
|
||||
)
|
||||
@@ -222,12 +225,12 @@ export class RemoteRuntimeInstallationManager {
|
||||
)
|
||||
const current = registry.current.find(
|
||||
(entry) =>
|
||||
entry.runtimeId === 'opencode' &&
|
||||
entry.runtimeId === runtimeId &&
|
||||
entry.architecture === probe.architecture
|
||||
)
|
||||
if (current === undefined) {
|
||||
throw new RemoteRuntimeInstallationError(
|
||||
'The Host has no current OpenCode Runtime for this architecture',
|
||||
`The Host has no current ${runtimeId} Runtime for this architecture`,
|
||||
'incompatible'
|
||||
)
|
||||
}
|
||||
@@ -236,7 +239,7 @@ export class RemoteRuntimeInstallationManager {
|
||||
current.runtimeAdapterDigest
|
||||
if (runtimeAdapterDigest === undefined) {
|
||||
const manifestPath =
|
||||
`${OPENCODE_ROOT}/${digestDirectoryName(
|
||||
`.goodbuddy/runtimes/${runtimeId}/${digestDirectoryName(
|
||||
current.bundleDigest
|
||||
)}/manifest.json`
|
||||
const manifest =
|
||||
@@ -288,7 +291,7 @@ export class RemoteRuntimeInstallationManager {
|
||||
}
|
||||
if (isMissingPathError(error)) {
|
||||
throw new RemoteRuntimeInstallationError(
|
||||
'The Host has no current OpenCode Runtime',
|
||||
`The Host has no current ${runtimeId} Runtime`,
|
||||
'incompatible',
|
||||
error
|
||||
)
|
||||
|
||||
@@ -39,6 +39,7 @@ type RuntimeBundleVerifier = (
|
||||
) => Promise<VerifiedRuntimeBundle>
|
||||
|
||||
export type RemoteRuntimeResourceLoaderOptions = {
|
||||
runtimeId?: 'opencode' | 'continue'
|
||||
verificationEnvironment?: 'production' | 'test'
|
||||
verifyRuntimeBundle?: RuntimeBundleVerifier
|
||||
}
|
||||
@@ -74,7 +75,7 @@ export async function loadVerifiedRemoteRuntimeResourceBundle(
|
||||
const metadata =
|
||||
await loadRemoteRuntimeVerificationMetadata(paths)
|
||||
const bundleDirectory = await resolveSingleDigestDirectory(
|
||||
getBundledRemoteRuntimeRoot(paths, architecture)
|
||||
getBundledRemoteRuntimeRoot(paths, architecture, options.runtimeId)
|
||||
)
|
||||
const verifier = options.verifyRuntimeBundle ?? verifyRuntimeBundle
|
||||
const verified = await verifier(bundleDirectory, {
|
||||
@@ -89,7 +90,8 @@ export async function loadVerifiedRemoteRuntimeResourceBundle(
|
||||
bundleDirectory,
|
||||
architecture,
|
||||
metadata.canonicalReleaseKeyRegistryBytes,
|
||||
metadata.canonicalRemoteRuntimeLockBytes
|
||||
metadata.canonicalRemoteRuntimeLockBytes,
|
||||
options.runtimeId ?? 'opencode'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -258,15 +260,16 @@ async function validateVerifiedBundle(
|
||||
bundleDirectory: string,
|
||||
architecture: AgentArchitecture,
|
||||
keyRegistryBytes: Buffer,
|
||||
runtimeLockBytes: Buffer
|
||||
runtimeLockBytes: Buffer,
|
||||
runtimeId: 'opencode' | 'continue'
|
||||
): Promise<VerifiedRemoteRuntimeResourceBundle> {
|
||||
const manifest = remoteRuntimeBundleManifestSchema.parse(
|
||||
verified.manifest
|
||||
)
|
||||
if (
|
||||
verified.bundleDirectory !== bundleDirectory ||
|
||||
manifest.runtimeId !== 'opencode' ||
|
||||
manifest.provider !== 'opencode' ||
|
||||
manifest.runtimeId !== runtimeId ||
|
||||
manifest.provider !== runtimeId ||
|
||||
(manifest.platform !== 'linux' && !(manifest.platform === 'darwin' && manifest.architecture === 'arm64')) ||
|
||||
manifest.architecture !== architecture
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user