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.
284 lines
8.2 KiB
TypeScript
284 lines
8.2 KiB
TypeScript
import {
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync
|
|
} from 'node:fs'
|
|
import { dirname, join } from 'node:path'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
type RemoteRuntimeBundleManifest
|
|
} from '../shared/remote-runtime-launch-contracts'
|
|
import {
|
|
verifyPublishedRuntimeBundle,
|
|
verifyRuntimeBundle
|
|
} from './runtime-bundle-verifier'
|
|
import {
|
|
RuntimeBundleRegistry,
|
|
createVerifiedRuntimeCapabilitySource
|
|
} from './runtime-bundle-registry'
|
|
import {
|
|
createRuntimeBundleTestFixture,
|
|
TEST_REMOTE_RUNTIME_LOCK
|
|
} from './runtime-bundle-test-fixture'
|
|
|
|
const temporaryPaths: string[] = []
|
|
const lock = TEST_REMOTE_RUNTIME_LOCK
|
|
|
|
afterEach(() => {
|
|
for (const path of temporaryPaths.splice(0)) {
|
|
rmSync(path, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
describe('Remote Runtime bundle verifier', () => {
|
|
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,
|
|
runtimes: {
|
|
...lock.runtimes,
|
|
opencode: { ...lock.runtimes.opencode, version: runtimeVersion }
|
|
}
|
|
},
|
|
verificationEnvironment: 'test',
|
|
filesystemPlatform: 'win32'
|
|
})
|
|
).resolves.toMatchObject({
|
|
bundleDirectory: fixture.bundleDirectory,
|
|
executablePath: join(
|
|
fixture.bundleDirectory,
|
|
'bin',
|
|
'opencode'
|
|
),
|
|
manifest: fixture.manifest,
|
|
manifestDigest: expect.stringMatching(/^sha256:[a-f0-9]{64}$/u)
|
|
})
|
|
})
|
|
|
|
it('fails closed on test signing keys in production verification', async () => {
|
|
const fixture = await createBundle()
|
|
|
|
await expect(
|
|
verifyRuntimeBundle(fixture.bundleDirectory, {
|
|
architecture: 'x64',
|
|
releaseKeyRegistry: fixture.registry,
|
|
runtimeLock: lock,
|
|
filesystemPlatform: 'win32'
|
|
})
|
|
).rejects.toThrow(/production key registry is empty/iu)
|
|
})
|
|
|
|
it('rejects a forged signature and payload mutation', async () => {
|
|
const forged = await createBundle()
|
|
const signaturePath = join(
|
|
forged.bundleDirectory,
|
|
'manifest.sig'
|
|
)
|
|
const signature = Buffer.from(
|
|
readFileSync(signaturePath, 'utf8').trim(),
|
|
'base64'
|
|
)
|
|
signature[0] ^= 0xff
|
|
writeFileSync(
|
|
signaturePath,
|
|
`${signature.toString('base64')}\n`
|
|
)
|
|
await expect(
|
|
verifyRuntimeBundle(forged.bundleDirectory, {
|
|
architecture: 'x64',
|
|
releaseKeyRegistry: forged.registry,
|
|
runtimeLock: lock,
|
|
verificationEnvironment: 'test',
|
|
filesystemPlatform: 'win32'
|
|
})
|
|
).rejects.toThrow(/signature verification failed/iu)
|
|
|
|
const mutated = await createBundle()
|
|
writeFileSync(
|
|
join(mutated.bundleDirectory, 'licenses', 'opencode.txt'),
|
|
'mutated'
|
|
)
|
|
await expect(
|
|
verifyRuntimeBundle(mutated.bundleDirectory, {
|
|
architecture: 'x64',
|
|
releaseKeyRegistry: mutated.registry,
|
|
runtimeLock: lock,
|
|
verificationEnvironment: 'test',
|
|
filesystemPlatform: 'win32'
|
|
})
|
|
).rejects.toThrow(/payload (?:size|hash) mismatch/iu)
|
|
})
|
|
|
|
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, {
|
|
architecture: 'x64',
|
|
releaseKeyRegistry: fixture.registry,
|
|
runtimeLock: lock,
|
|
verificationEnvironment: 'test',
|
|
filesystemPlatform: 'win32'
|
|
})
|
|
).rejects.toThrow(/locked OpenCode profile/iu)
|
|
})
|
|
|
|
it('rejects undeclared files and a mismatched digest directory', async () => {
|
|
const undeclared = await createBundle()
|
|
writeFileSync(
|
|
join(undeclared.bundleDirectory, 'unexpected.txt'),
|
|
'unexpected'
|
|
)
|
|
await expect(
|
|
verifyRuntimeBundle(undeclared.bundleDirectory, {
|
|
architecture: 'x64',
|
|
releaseKeyRegistry: undeclared.registry,
|
|
runtimeLock: lock,
|
|
verificationEnvironment: 'test',
|
|
filesystemPlatform: 'win32'
|
|
})
|
|
).rejects.toThrow(/undeclared or missing/iu)
|
|
|
|
const mismatched = await createBundle({
|
|
directoryDigest: `sha256:${'f'.repeat(64)}`
|
|
})
|
|
await expect(
|
|
verifyRuntimeBundle(mismatched.bundleDirectory, {
|
|
architecture: 'x64',
|
|
releaseKeyRegistry: mismatched.registry,
|
|
runtimeLock: lock,
|
|
verificationEnvironment: 'test',
|
|
filesystemPlatform: 'win32'
|
|
})
|
|
).rejects.toThrow(/managed directory/iu)
|
|
})
|
|
|
|
it('uses registered metadata without rereading Runtime payload files', async () => {
|
|
const fixture = await createBundle()
|
|
const runtimeRoot = dirname(dirname(fixture.bundleDirectory))
|
|
const registry = new RuntimeBundleRegistry({
|
|
runtimeRoot
|
|
})
|
|
const verified = await verifyRuntimeBundle(
|
|
fixture.bundleDirectory,
|
|
{
|
|
architecture: 'x64',
|
|
releaseKeyRegistry: fixture.registry,
|
|
runtimeLock: lock,
|
|
verificationEnvironment: 'test',
|
|
filesystemPlatform: 'win32'
|
|
}
|
|
)
|
|
new RuntimeBundleRegistry({ runtimeRoot }).register(verified)
|
|
const reportError = vi.fn()
|
|
const capabilities = createVerifiedRuntimeCapabilitySource({
|
|
registry,
|
|
architecture: 'x64',
|
|
releaseKeyRegistry: fixture.registry,
|
|
runtimeLock: lock,
|
|
verificationEnvironment: 'test',
|
|
filesystemPlatform: 'win32',
|
|
reportError
|
|
})
|
|
await expect(capabilities()).resolves.toEqual([
|
|
{
|
|
runtimeId: 'opencode',
|
|
version: '1.18.29',
|
|
bundleDigest: fixture.manifest.bundleDigest,
|
|
acpCapabilitiesDigest:
|
|
fixture.manifest.acpCapabilitiesDigest,
|
|
sessionLoad: true,
|
|
sessionResume: true
|
|
}
|
|
])
|
|
|
|
writeFileSync(
|
|
join(fixture.bundleDirectory, 'licenses', 'opencode.txt'),
|
|
'mutated'
|
|
)
|
|
await expect(capabilities()).resolves.toHaveLength(1)
|
|
expect(reportError).not.toHaveBeenCalled()
|
|
|
|
writeFileSync(
|
|
join(fixture.bundleDirectory, 'bin', 'opencode'),
|
|
Buffer.concat([
|
|
readFileSync(
|
|
join(fixture.bundleDirectory, 'bin', 'opencode')
|
|
),
|
|
Buffer.from('changed')
|
|
])
|
|
)
|
|
await expect(capabilities()).resolves.toHaveLength(1)
|
|
expect(reportError).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('adopts signed published metadata without rehashing unrelated payload', async () => {
|
|
const fixture = await createBundle()
|
|
const options = {
|
|
architecture: 'x64' as const,
|
|
releaseKeyRegistry: fixture.registry,
|
|
runtimeLock: lock,
|
|
verificationEnvironment: 'test' as const,
|
|
filesystemPlatform: 'win32' as const
|
|
}
|
|
writeFileSync(
|
|
join(fixture.bundleDirectory, 'licenses', 'opencode.txt'),
|
|
'changed after extraction'
|
|
)
|
|
await expect(
|
|
verifyPublishedRuntimeBundle(
|
|
fixture.bundleDirectory,
|
|
options
|
|
)
|
|
).resolves.toMatchObject({
|
|
manifest: { runtimeVersion: '1.18.29' }
|
|
})
|
|
|
|
writeFileSync(
|
|
join(fixture.bundleDirectory, 'bin', 'opencode'),
|
|
Buffer.concat([
|
|
readFileSync(
|
|
join(fixture.bundleDirectory, 'bin', 'opencode')
|
|
),
|
|
Buffer.from('changed')
|
|
])
|
|
)
|
|
await expect(
|
|
verifyPublishedRuntimeBundle(
|
|
fixture.bundleDirectory,
|
|
options
|
|
)
|
|
).rejects.toThrow('entrypoint metadata changed')
|
|
})
|
|
})
|
|
|
|
async function createBundle(
|
|
overrides: {
|
|
runtimeVersion?: RemoteRuntimeBundleManifest['runtimeVersion']
|
|
sourcePackage?: RemoteRuntimeBundleManifest['sourcePackage']
|
|
directoryDigest?: string
|
|
} = {}
|
|
): Promise<Awaited<
|
|
ReturnType<typeof createRuntimeBundleTestFixture>
|
|
>> {
|
|
const fixture = await createRuntimeBundleTestFixture(overrides)
|
|
temporaryPaths.push(fixture.root)
|
|
return fixture
|
|
}
|