fix: keep valid older agent packages usable after desktop updates
Accept package-declared OpenCode, Node and koffi versions instead of desktop release pins. Preserve signature, hash, protocol and package-internal version consistency checks. Validate both cached 0.11.18 packages and Linux x64 installation, bootstrap, health and stop. Related regression suite: 326 passed, 1 skipped; unrelated full-workspace check failures are recorded in the remote Host design. Release note: Desktop updates no longer reject intact older Agent packages solely because their bundled runtime versions differ.
This commit is contained in:
@@ -66,6 +66,7 @@ export const TEST_REMOTE_RUNTIME_LOCK: RemoteRuntimeLock = {
|
||||
|
||||
export async function createRuntimeBundleTestFixture(
|
||||
overrides: {
|
||||
runtimeVersion?: RemoteRuntimeBundleManifest['runtimeVersion']
|
||||
sourcePackage?: RemoteRuntimeBundleManifest['sourcePackage']
|
||||
directoryDigest?: string
|
||||
} = {}
|
||||
@@ -87,7 +88,7 @@ export async function createRuntimeBundleTestFixture(
|
||||
formatVersion: 2,
|
||||
product: 'GoodBuddy',
|
||||
runtimeId: 'opencode',
|
||||
runtimeVersion: '1.18.29',
|
||||
runtimeVersion: overrides.runtimeVersion ?? '1.18.29',
|
||||
provider: 'opencode',
|
||||
platform: 'linux',
|
||||
architecture: 'x64',
|
||||
@@ -127,7 +128,7 @@ export async function createRuntimeBundleTestFixture(
|
||||
licenses: [
|
||||
{
|
||||
package: 'opencode-ai',
|
||||
version: '1.18.29',
|
||||
version: overrides.runtimeVersion ?? '1.18.29',
|
||||
spdx: 'MIT',
|
||||
path: 'licenses/opencode.txt'
|
||||
}
|
||||
|
||||
@@ -31,14 +31,20 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('Remote Runtime bundle verifier', () => {
|
||||
it('verifies an authentic digest-addressed OpenCode bundle', async () => {
|
||||
const fixture = await createBundle()
|
||||
it.each(['1.18.29', '1.18.9'])('verifies an authentic digest-addressed OpenCode %s bundle with a matching lock', async (runtimeVersion) => {
|
||||
const fixture = await createBundle({ runtimeVersion })
|
||||
|
||||
await expect(
|
||||
verifyRuntimeBundle(fixture.bundleDirectory, {
|
||||
architecture: 'x64',
|
||||
releaseKeyRegistry: fixture.registry,
|
||||
runtimeLock: lock,
|
||||
runtimeLock: {
|
||||
...lock,
|
||||
runtimes: {
|
||||
...lock.runtimes,
|
||||
opencode: { ...lock.runtimes.opencode, version: runtimeVersion }
|
||||
}
|
||||
},
|
||||
verificationEnvironment: 'test',
|
||||
filesystemPlatform: 'win32'
|
||||
})
|
||||
@@ -108,14 +114,19 @@ describe('Remote Runtime bundle verifier', () => {
|
||||
).rejects.toThrow(/payload (?:size|hash) mismatch/iu)
|
||||
})
|
||||
|
||||
it('rejects a validly signed bundle outside the locked profile', async () => {
|
||||
const fixture = await createBundle({
|
||||
it.each([
|
||||
{
|
||||
runtimeVersion: '1.18.9'
|
||||
},
|
||||
{
|
||||
sourcePackage: {
|
||||
name: 'opencode-linux-x64',
|
||||
integrity:
|
||||
'sha512-VrvzV5Agrj0T2ZPvr5gzmh8xc4zqQ5pW8UeNgTzt5cJ/9Cbdxw6oFywgv7nfJqlpSfQqTWYJYH+LIHt3QdCS5g=='
|
||||
}
|
||||
})
|
||||
}
|
||||
])('rejects a validly signed bundle outside the locked profile: %j', async (overrides) => {
|
||||
const fixture = await createBundle(overrides)
|
||||
|
||||
await expect(
|
||||
verifyRuntimeBundle(fixture.bundleDirectory, {
|
||||
@@ -259,6 +270,7 @@ describe('Remote Runtime bundle verifier', () => {
|
||||
|
||||
async function createBundle(
|
||||
overrides: {
|
||||
runtimeVersion?: RemoteRuntimeBundleManifest['runtimeVersion']
|
||||
sourcePackage?: RemoteRuntimeBundleManifest['sourcePackage']
|
||||
directoryDigest?: string
|
||||
} = {}
|
||||
|
||||
@@ -108,7 +108,7 @@ function writeKoffiPayload(
|
||||
join(packageRoot, 'package.json'),
|
||||
JSON.stringify({
|
||||
name: 'koffi',
|
||||
version: '3.1.4',
|
||||
version: runtimeLock.koffi.version,
|
||||
type: 'module',
|
||||
exports: { '.': { import: './index.js' } }
|
||||
})
|
||||
@@ -129,7 +129,7 @@ function writeKoffiPayload(
|
||||
join(nativeRoot, 'package.json'),
|
||||
JSON.stringify({
|
||||
name: nativePackage,
|
||||
version: '3.1.4',
|
||||
version: runtimeLock.koffi.version,
|
||||
main: './index.js',
|
||||
os: ['linux'],
|
||||
cpu: [architecture]
|
||||
@@ -202,9 +202,9 @@ function writeBundle(
|
||||
|
||||
const manifest = buildAgentBundle.createManifest(directory, {
|
||||
agentVersion: '0.11.0',
|
||||
nodeVersion: '24.19.0',
|
||||
nodeVersion: runtimeLock.node.version,
|
||||
zodVersion: '4.4.3',
|
||||
koffiVersion: '3.1.4',
|
||||
koffiVersion: runtimeLock.koffi.version,
|
||||
koffiNativePackage: '@koromix/koffi-linux-x64',
|
||||
arch: 'x64',
|
||||
protocol: { major: 1, minor: 0 },
|
||||
@@ -282,6 +282,27 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('runtime and build-time Agent verifier parity', () => {
|
||||
it('accepts older dependencies when the signed manifest matches the package lock', async () => {
|
||||
runtimeLock.node.version = '24.18.0'
|
||||
runtimeLock.koffi.version = '3.1.3'
|
||||
writeBundle()
|
||||
await expect(verifyBundle()).resolves.toMatchObject({
|
||||
manifest: { agentVersion: runtimeLock.agentVersion }
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['node', 'koffi'] as const)('rejects a signed bundle with a mismatched %s lock version', async (component) => {
|
||||
writeBundle()
|
||||
runtimeLock[component].version = component === 'node' ? '24.18.0' : '3.1.3'
|
||||
await expect(verifyBundle()).rejects.toThrow(/does not match the runtime lock|dependencies do not match the runtime lock/u)
|
||||
})
|
||||
|
||||
it.each(['node', 'koffi'] as const)('rejects an invalid %s lock version', async (component) => {
|
||||
writeBundle()
|
||||
runtimeLock[component].version = 'latest'
|
||||
await expect(verifyBundle()).rejects.toThrow('Agent runtime lock contract is invalid')
|
||||
})
|
||||
|
||||
it('accepts equivalent registry whitespace and canonicalizes validated keys', () => {
|
||||
const canonical = canonicalAgentReleaseKeyRegistryBytes(registry)
|
||||
const crlf = Buffer.from(
|
||||
|
||||
@@ -342,6 +342,12 @@ export function assertAgentManifestMatchesRuntimeLock(
|
||||
'Agent manifest architecture does not match the selected bundle'
|
||||
)
|
||||
}
|
||||
if (
|
||||
manifest.licenses.find((license) => license.package === 'Node.js')
|
||||
?.version !== lock.node.version
|
||||
) {
|
||||
throw new Error('Agent Node version does not match the runtime lock')
|
||||
}
|
||||
if (
|
||||
manifest.protocol.major !== lock.protocol.major ||
|
||||
manifest.protocol.minor !== lock.protocol.minor
|
||||
|
||||
@@ -74,19 +74,33 @@ async function createFixture(): Promise<{
|
||||
}
|
||||
|
||||
describe('remote Runtime verification metadata loader', () => {
|
||||
it('loads canonical verification metadata without Runtime payloads', async () => {
|
||||
it.each(['1.18.9', '1.18.29'])('loads canonical verification metadata for Runtime %s without payloads', async (version) => {
|
||||
const fixture = await createFixture()
|
||||
const packageLock = structuredClone(runtimeLock)
|
||||
packageLock.runtimes.opencode.version = version
|
||||
await writeFile(fixture.paths.runtimeLockPath, canonical(packageLock))
|
||||
|
||||
await expect(
|
||||
loadRemoteRuntimeVerificationMetadata(fixture.paths)
|
||||
).resolves.toEqual({
|
||||
releaseKeyRegistry: registry,
|
||||
runtimeLock,
|
||||
runtimeLock: packageLock,
|
||||
canonicalReleaseKeyRegistryBytes: canonical(registry),
|
||||
canonicalRemoteRuntimeLockBytes: canonical(runtimeLock)
|
||||
canonicalRemoteRuntimeLockBytes: canonical(packageLock)
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['', 'latest', '^1.18.9', '1.18'])('rejects invalid Runtime version %j', async (version) => {
|
||||
const fixture = await createFixture()
|
||||
const packageLock = structuredClone(runtimeLock)
|
||||
packageLock.runtimes.opencode.version = version
|
||||
await writeFile(fixture.paths.runtimeLockPath, canonical(packageLock))
|
||||
|
||||
await expect(
|
||||
loadRemoteRuntimeVerificationMetadata(fixture.paths)
|
||||
).rejects.toThrow('Remote Runtime lock contract is invalid')
|
||||
})
|
||||
|
||||
it('canonicalizes equivalent release-key registry formatting', async () => {
|
||||
const fixture = await createFixture()
|
||||
await writeFile(
|
||||
|
||||
@@ -147,7 +147,10 @@ describe('Agent installation contracts', () => {
|
||||
).toThrow('canonical Base64')
|
||||
})
|
||||
|
||||
it('requires both locked Linux targets and the fixed Node patch', () => {
|
||||
it.each([
|
||||
['24.19.0', '3.1.4'],
|
||||
['24.18.0', '3.1.3']
|
||||
])('accepts package Node %s and koffi %s versions while requiring valid targets and source', (nodeVersion, koffiVersion) => {
|
||||
const target = {
|
||||
archive: 'node.tar.gz',
|
||||
sha256: digest,
|
||||
@@ -160,16 +163,16 @@ describe('Agent installation contracts', () => {
|
||||
agentVersion: '0.11.0',
|
||||
protocol: { major: 1, minor: 0 },
|
||||
node: {
|
||||
version: '24.19.0',
|
||||
source: 'https://nodejs.org/dist/v24.19.0/',
|
||||
version: nodeVersion,
|
||||
source: `https://nodejs.org/dist/v${nodeVersion}/`,
|
||||
targets: {
|
||||
'linux-x64': target,
|
||||
'linux-arm64': target
|
||||
}
|
||||
},
|
||||
koffi: { version: '3.1.4' }
|
||||
koffi: { version: koffiVersion }
|
||||
}).koffi.version
|
||||
).toBe('3.1.4')
|
||||
).toBe(koffiVersion)
|
||||
expect(() =>
|
||||
agentRuntimeLockSchema.parse({
|
||||
formatVersion: 1,
|
||||
|
||||
@@ -239,7 +239,7 @@ export const agentRuntimeLockSchema = z
|
||||
protocol: agentProtocolVersionSchema,
|
||||
node: z
|
||||
.object({
|
||||
version: z.literal('24.19.0'),
|
||||
version: componentVersionSchema,
|
||||
source: z
|
||||
.url()
|
||||
.refine(
|
||||
@@ -257,7 +257,7 @@ export const agentRuntimeLockSchema = z
|
||||
.strict(),
|
||||
koffi: z
|
||||
.object({
|
||||
version: z.literal('3.1.4')
|
||||
version: componentVersionSchema
|
||||
})
|
||||
.strict()
|
||||
})
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
agentManifestLicenseSchema,
|
||||
agentManifestPathSchema,
|
||||
agentProtocolVersionSchema,
|
||||
agentReleaseKeySchema
|
||||
agentReleaseKeySchema,
|
||||
componentVersionSchema
|
||||
} from './agent-installation-contracts'
|
||||
import {
|
||||
agentIdentifierSchema,
|
||||
@@ -227,7 +228,8 @@ export const remoteRuntimeLockSchema = z
|
||||
.object({
|
||||
opencode: z
|
||||
.object({
|
||||
version: z.literal('1.18.29'),
|
||||
// Installed packages carry their own signed Runtime version.
|
||||
version: componentVersionSchema,
|
||||
provider: z.literal('opencode'),
|
||||
entrypoint: z.literal('bin/opencode'),
|
||||
entrypointIdentity: z.literal('opencode-acp'),
|
||||
|
||||
Reference in New Issue
Block a user