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:
mesalogo
2026-09-16 19:04:54 +08:00
parent 441f14b5f7
commit 1698d9fd3e
9 changed files with 100 additions and 24 deletions
@@ -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(