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.
2509 lines
74 KiB
TypeScript
2509 lines
74 KiB
TypeScript
import {
|
|
createHash,
|
|
createPrivateKey,
|
|
sign,
|
|
type KeyObject
|
|
} from 'node:crypto'
|
|
import { execFileSync } from 'node:child_process'
|
|
import {
|
|
chmodSync,
|
|
lstatSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
renameSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
writeFileSync
|
|
} from 'node:fs'
|
|
import { createRequire } from 'node:module'
|
|
import { tmpdir } from 'node:os'
|
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
import { c as createTar } from 'tar'
|
|
import { unzipSync, zipSync } from 'fflate'
|
|
import {
|
|
afterAll,
|
|
beforeAll,
|
|
describe,
|
|
expect,
|
|
it,
|
|
vi
|
|
} from 'vitest'
|
|
import {
|
|
agentPackageCatalogSchema,
|
|
agentPackageDescriptorSchema,
|
|
agentPackageArchiveName,
|
|
type AgentPackageCatalog,
|
|
type AgentPackageCatalogEntry,
|
|
type AgentPackageDescriptor
|
|
} from '../src/shared/agent-package-contracts'
|
|
import type {
|
|
AgentReleaseKeyRegistry,
|
|
AgentRuntimeLock
|
|
} from '../src/shared/agent-installation-contracts'
|
|
import type {
|
|
RemoteRuntimeLock
|
|
} from '../src/shared/remote-runtime-launch-contracts'
|
|
import {
|
|
compareSemanticVersions,
|
|
extractAndVerifyAgentPackage
|
|
} from '../src/main/remote-agent/agent-package-verifier'
|
|
import {
|
|
AgentPackageManager,
|
|
selectLatestCompatibleEntry
|
|
} from '../src/main/remote-agent/agent-package-manager'
|
|
import { createOpenCodeConfigFixture } from './support/opencode-config-fixture'
|
|
|
|
type AgentBundleModule = {
|
|
canonicalManifestBytes(manifest: unknown): Buffer
|
|
createManifest(
|
|
directory: string,
|
|
metadata: Record<string, unknown>
|
|
): AgentPackageDescriptor
|
|
publicKeySpkiBase64(key: KeyObject): string
|
|
signManifestForTest(bytes: Buffer, key: KeyObject): Buffer
|
|
}
|
|
|
|
type RuntimeBundleModule = {
|
|
buildRuntimeBundle(options: {
|
|
runtimeId?: 'opencode' | 'continue'
|
|
projectRoot: string
|
|
architecture: 'x64' | 'arm64'
|
|
runtimeArchive: string
|
|
outputRoot: string
|
|
lock: RemoteRuntimeLock
|
|
registry: AgentReleaseKeyRegistry
|
|
testSigningIdentity: {
|
|
keyId: string
|
|
privateKey: KeyObject
|
|
}
|
|
enforceFilesystemMode: false
|
|
}): {
|
|
bundleDirectory: string
|
|
}
|
|
}
|
|
|
|
type AgentPackageModule = {
|
|
assembleAgentPackage(options: {
|
|
projectRoot: string
|
|
architecture: 'x64' | 'arm64'
|
|
minimumDesktopVersion: string
|
|
output: string
|
|
agentBundle: string
|
|
runtimeBundle: string
|
|
additionalRuntimeBundles?: string[]
|
|
agentLock: AgentRuntimeLock
|
|
runtimeLock: RemoteRuntimeLock
|
|
registry: AgentReleaseKeyRegistry
|
|
testSigningIdentity: {
|
|
keyId: string
|
|
privateKey: KeyObject
|
|
}
|
|
}): {
|
|
descriptor: AgentPackageDescriptor
|
|
archive: string
|
|
size: number
|
|
sha256: string
|
|
}
|
|
descriptorBytes(descriptor: AgentPackageDescriptor): Buffer
|
|
descriptorContentDigest(
|
|
descriptor: AgentPackageDescriptor
|
|
): string
|
|
signatureDomain: Buffer
|
|
}
|
|
|
|
type AgentCatalogModule = {
|
|
readPackageMetadata(
|
|
archivePath: string,
|
|
registry: AgentReleaseKeyRegistry
|
|
): {
|
|
descriptor: AgentPackageDescriptor
|
|
size: number
|
|
sha256: string
|
|
}
|
|
createCatalog(options: {
|
|
projectRoot: string
|
|
x64Package: string
|
|
arm64Package: string
|
|
previousCatalog?: string
|
|
previousSignature?: string
|
|
outputCatalog: string
|
|
outputSignature: string
|
|
generatedAt: string
|
|
registry: AgentReleaseKeyRegistry
|
|
signingIdentity: {
|
|
keyId: string
|
|
privateKey: KeyObject
|
|
}
|
|
}): {
|
|
version: string
|
|
catalog: string
|
|
signature: string
|
|
entries: number
|
|
}
|
|
readVerifiedCatalog(
|
|
catalogPath: string,
|
|
signaturePath: string,
|
|
registry: AgentReleaseKeyRegistry
|
|
): AgentPackageCatalog
|
|
}
|
|
|
|
type PackageInstallerModule = {
|
|
preparePackage(options: {
|
|
operationRoot: string
|
|
archive: string
|
|
expectedSha256: string
|
|
homeDirectory: string
|
|
emit?: (event: unknown) => void
|
|
}): {
|
|
type: 'result'
|
|
command: 'prepare'
|
|
status: 'prepared'
|
|
archiveSha256: string
|
|
agent: {
|
|
installationId: string
|
|
agentVersion: string
|
|
manifestSha256: string
|
|
binaryDigest: string
|
|
platform: 'linux'
|
|
architecture: 'x64' | 'arm64'
|
|
protocol: { major: number; minor: number }
|
|
supervisor: 'detached-on-demand'
|
|
}
|
|
runtime: {
|
|
runtimeId: 'opencode'
|
|
runtimeVersion: string
|
|
bundleDigest: string
|
|
manifestDigest: string
|
|
runtimeAdapterDigest: string
|
|
acpCapabilitiesDigest: string
|
|
platform: 'linux'
|
|
architecture: 'x64' | 'arm64'
|
|
protocol: { major: number; minor: number }
|
|
}
|
|
}
|
|
commitPackage(options: {
|
|
operationRoot: string
|
|
archive: string
|
|
expectedSha256: string
|
|
homeDirectory: string
|
|
emit?: (event: unknown) => void
|
|
}): {
|
|
command: 'commit'
|
|
status: 'committed'
|
|
agent: PackageInstallerModule[
|
|
'preparePackage'
|
|
] extends (...args: never[]) => infer Result
|
|
? Result extends { agent: infer Agent }
|
|
? Agent
|
|
: never
|
|
: never
|
|
runtime: PackageInstallerModule[
|
|
'preparePackage'
|
|
] extends (...args: never[]) => infer Result
|
|
? Result extends { runtime: infer Runtime }
|
|
? Runtime
|
|
: never
|
|
: never
|
|
}
|
|
}
|
|
|
|
const require = createRequire(import.meta.url)
|
|
const agentBundle = require(
|
|
'../build/agent-bundle.cjs'
|
|
) as AgentBundleModule
|
|
const runtimeBundle = require(
|
|
'../build/remote-runtime-bundle.cjs'
|
|
) as RuntimeBundleModule
|
|
const agentPackage = require(
|
|
'../build/agent-package.cjs'
|
|
) as AgentPackageModule
|
|
const agentCatalog = require(
|
|
'../build/agent-catalog.cjs'
|
|
) as AgentCatalogModule
|
|
const catalogSignatureDomain = Buffer.from(
|
|
'GoodBuddy Agent Package Catalog Signature v1\0',
|
|
'utf8'
|
|
)
|
|
|
|
let temporaryRoot = ''
|
|
let privateKey: KeyObject
|
|
let registry: AgentReleaseKeyRegistry
|
|
let agentLock: AgentRuntimeLock
|
|
let remoteRuntimeLock: RemoteRuntimeLock
|
|
let packagePath = ''
|
|
let arm64PackagePath = ''
|
|
let packageResult: ReturnType<
|
|
AgentPackageModule['assembleAgentPackage']
|
|
>
|
|
let arm64PackageResult: ReturnType<
|
|
AgentPackageModule['assembleAgentPackage']
|
|
>
|
|
let packageInstaller: PackageInstallerModule
|
|
let installerBundle = ''
|
|
|
|
beforeAll(() => {
|
|
temporaryRoot = resolve(
|
|
mkdtempSync(join(tmpdir(), 'goodbuddy-agent-package-test-'))
|
|
)
|
|
installerBundle = join(
|
|
temporaryRoot,
|
|
'package-installer-fixture.cjs'
|
|
)
|
|
execFileSync(process.execPath, [
|
|
'-e',
|
|
[
|
|
"require('esbuild').buildSync({",
|
|
`entryPoints:[${JSON.stringify(
|
|
join(
|
|
process.cwd(),
|
|
'src',
|
|
'main',
|
|
'remote-agent',
|
|
'control-plane-package-installer.ts'
|
|
)
|
|
)}],`,
|
|
`outfile:${JSON.stringify(installerBundle)},`,
|
|
"bundle:true,platform:'node',target:['node24.19'],",
|
|
"format:'cjs',logLevel:'silent'})"
|
|
].join('')
|
|
])
|
|
packageInstaller = require(installerBundle) as PackageInstallerModule
|
|
privateKey = createPrivateKey({
|
|
key: Buffer.from(
|
|
'302e020100300506032b657004220420404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f',
|
|
'hex'
|
|
),
|
|
format: 'der',
|
|
type: 'pkcs8'
|
|
})
|
|
registry = {
|
|
formatVersion: 1,
|
|
keys: [{
|
|
keyId: 'agent-package-fixture',
|
|
publicKeySpkiBase64:
|
|
agentBundle.publicKeySpkiBase64(privateKey),
|
|
environment: 'production'
|
|
}],
|
|
revocations: []
|
|
}
|
|
agentLock = JSON.parse(
|
|
readFileSync(
|
|
join(process.cwd(), 'agent-runtime-lock.json'),
|
|
'utf8'
|
|
)
|
|
) as AgentRuntimeLock
|
|
const agentDirectory = join(temporaryRoot, 'agent')
|
|
const arm64AgentDirectory = join(temporaryRoot, 'agent-arm64')
|
|
writeAgentBundle(agentDirectory, 'x64')
|
|
writeAgentBundle(arm64AgentDirectory, 'arm64')
|
|
const runtimeInput = createRuntimeInput('x64')
|
|
const arm64RuntimeInput = createRuntimeInput('arm64')
|
|
remoteRuntimeLock = createRemoteRuntimeLock(
|
|
runtimeInput,
|
|
arm64RuntimeInput
|
|
)
|
|
const buildRuntime = (
|
|
architecture: 'x64' | 'arm64',
|
|
archive: string
|
|
) => runtimeBundle.buildRuntimeBundle({
|
|
projectRoot: temporaryRoot,
|
|
architecture,
|
|
runtimeArchive: archive,
|
|
outputRoot: join(temporaryRoot, 'runtime'),
|
|
lock: remoteRuntimeLock,
|
|
registry,
|
|
testSigningIdentity: {
|
|
keyId: 'agent-package-fixture',
|
|
privateKey
|
|
},
|
|
enforceFilesystemMode: false
|
|
})
|
|
createOpenCodeConfigFixture(temporaryRoot)
|
|
const builtRuntime = buildRuntime('x64', runtimeInput)
|
|
const builtArm64Runtime = buildRuntime(
|
|
'arm64',
|
|
arm64RuntimeInput
|
|
)
|
|
packagePath = join(
|
|
temporaryRoot,
|
|
agentPackageArchiveName(agentLock.agentVersion, 'x64')
|
|
)
|
|
packageResult = agentPackage.assembleAgentPackage({
|
|
projectRoot: process.cwd(),
|
|
architecture: 'x64',
|
|
minimumDesktopVersion: '0.11.0',
|
|
output: packagePath,
|
|
agentBundle: agentDirectory,
|
|
runtimeBundle: builtRuntime.bundleDirectory,
|
|
agentLock,
|
|
runtimeLock: remoteRuntimeLock,
|
|
registry,
|
|
testSigningIdentity: {
|
|
keyId: 'agent-package-fixture',
|
|
privateKey
|
|
}
|
|
})
|
|
arm64PackagePath = join(
|
|
temporaryRoot,
|
|
agentPackageArchiveName(agentLock.agentVersion, 'arm64')
|
|
)
|
|
arm64PackageResult = agentPackage.assembleAgentPackage({
|
|
projectRoot: process.cwd(),
|
|
architecture: 'arm64',
|
|
minimumDesktopVersion: '0.11.0',
|
|
output: arm64PackagePath,
|
|
agentBundle: arm64AgentDirectory,
|
|
runtimeBundle: builtArm64Runtime.bundleDirectory,
|
|
agentLock,
|
|
runtimeLock: remoteRuntimeLock,
|
|
registry,
|
|
testSigningIdentity: {
|
|
keyId: 'agent-package-fixture',
|
|
privateKey
|
|
}
|
|
})
|
|
})
|
|
|
|
afterAll(() => {
|
|
rmSync(temporaryRoot, { recursive: true, force: true })
|
|
})
|
|
|
|
describe('compound Agent packages', () => {
|
|
it.each([false, true])('verifies and installs a Continue compound package through the production installer (joint=%s)', async (joint) => {
|
|
const root = join(temporaryRoot, `continue-package-${joint}`)
|
|
const source = join(root, 'source')
|
|
mkdirSync(join(source, 'package', 'dist'), { recursive: true })
|
|
writeFileSync(join(source, 'package', 'package.json'), JSON.stringify({ name: '@continuedev/cli', version: '1.5.47' }))
|
|
writeFileSync(join(source, 'package', 'dist', 'cn.js'), 'console.log("1.5.47")\n')
|
|
const archive = join(root, 'continuedev-cli-1.5.47.tgz')
|
|
createTar({ cwd: source, file: archive, sync: true, gzip: true }, ['package'])
|
|
const lock = JSON.parse(readFileSync(join(process.cwd(), 'remote-runtime-lock.json'), 'utf8')) as RemoteRuntimeLock
|
|
const integrity = `sha512-${createHash('sha512').update(readFileSync(archive)).digest('base64')}`
|
|
lock.runtimes.continue!.targets.x64.integrity = integrity
|
|
lock.runtimes.opencode = remoteRuntimeLock.runtimes.opencode
|
|
const built = runtimeBundle.buildRuntimeBundle({
|
|
projectRoot: process.cwd(), runtimeId: 'continue', architecture: 'x64', runtimeArchive: archive,
|
|
outputRoot: join(root, 'runtime'), lock, registry,
|
|
testSigningIdentity: { keyId: 'agent-package-fixture', privateKey }, enforceFilesystemMode: false
|
|
})
|
|
const result = agentPackage.assembleAgentPackage({
|
|
projectRoot: process.cwd(), architecture: 'x64', minimumDesktopVersion: '0.13.6',
|
|
output: join(root, agentPackageArchiveName(agentLock.agentVersion, 'x64')),
|
|
agentBundle: join(temporaryRoot, 'agent'), runtimeBundle: joint ? runtimeBundle.buildRuntimeBundle({
|
|
projectRoot: temporaryRoot, architecture: 'x64', runtimeArchive: createRuntimeInput('x64'),
|
|
outputRoot: join(root, 'opencode'), lock, registry,
|
|
testSigningIdentity: { keyId: 'agent-package-fixture', privateKey }, enforceFilesystemMode: false
|
|
}).bundleDirectory : built.bundleDirectory,
|
|
...(joint ? { additionalRuntimeBundles: [built.bundleDirectory] } : {}),
|
|
agentLock, runtimeLock: lock, registry, testSigningIdentity: { keyId: 'agent-package-fixture', privateKey }
|
|
})
|
|
await expect(extractAndVerifyAgentPackage({
|
|
archivePath: join(root, result.archive), destinationDirectory: join(root, 'verified'), architecture: 'x64',
|
|
desktopVersion: '0.13.6', trustedRegistry: registry
|
|
})).resolves.toMatchObject({ descriptor: joint
|
|
? { remoteRuntime: { runtimeId: 'opencode' }, additionalRuntimes: [{ runtimeId: 'continue', version: '1.5.47' }] }
|
|
: { remoteRuntime: { runtimeId: 'continue', version: '1.5.47' } } })
|
|
const options = { operationRoot: join(root, 'operation'), archive: join(root, result.archive),
|
|
expectedSha256: result.sha256, homeDirectory: join(root, 'home') }
|
|
mkdirSync(options.homeDirectory, { mode: 0o700 })
|
|
const expected = joint ? { runtime: { runtimeId: 'opencode' }, additionalRuntimes: [{ runtimeId: 'continue' }] } : { runtime: { runtimeId: 'continue' } }
|
|
expect(packageInstaller.preparePackage(options)).toMatchObject({ status: 'prepared', ...expected })
|
|
expect(packageInstaller.commitPackage(options)).toMatchObject({ status: 'committed', ...expected })
|
|
const contents = unzipSync(new Uint8Array(readFileSync(join(root, result.archive))))
|
|
const cnEntry = Object.keys(contents).find(path => path.endsWith('/lib/continue/dist/cn.js'))!
|
|
contents[cnEntry] = new TextEncoder().encode('tampered')
|
|
const corrupted = join(root, 'corrupted.gbagent')
|
|
writeFileSync(corrupted, zipSync(contents, { level: 0 }))
|
|
await expect(extractAndVerifyAgentPackage({ archivePath: corrupted,
|
|
destinationDirectory: join(root, 'corrupted'), architecture: 'x64', desktopVersion: '0.13.6', trustedRegistry: registry
|
|
})).rejects.toThrow()
|
|
})
|
|
|
|
it('verifies and installs offline dependency inventories larger than one MiB', async () => {
|
|
const projectRoot = join(temporaryRoot, 'large-offline-config')
|
|
const config = createOpenCodeConfigFixture(projectRoot)
|
|
const inventory = join(config, 'inventory')
|
|
mkdirSync(inventory)
|
|
for (let index = 0; index < 3000; index++) {
|
|
const path = join(inventory, `${String(index).padStart(4, '0')}-${'x'.repeat(100)}.js`)
|
|
writeFileSync(path, '// fixture\n')
|
|
}
|
|
const runtime = runtimeBundle.buildRuntimeBundle({
|
|
projectRoot, architecture: 'x64',
|
|
runtimeArchive: createRuntimeInput('x64'),
|
|
outputRoot: join(projectRoot, 'runtime'),
|
|
lock: remoteRuntimeLock, registry,
|
|
testSigningIdentity: { keyId: 'agent-package-fixture', privateKey },
|
|
enforceFilesystemMode: false
|
|
})
|
|
const archive = join(projectRoot, agentPackageArchiveName(agentLock.agentVersion, 'x64'))
|
|
const result = agentPackage.assembleAgentPackage({
|
|
projectRoot, architecture: 'x64', minimumDesktopVersion: '0.11.0',
|
|
output: archive, agentBundle: join(temporaryRoot, 'agent'),
|
|
runtimeBundle: runtime.bundleDirectory, agentLock, runtimeLock: remoteRuntimeLock,
|
|
registry, testSigningIdentity: { keyId: 'agent-package-fixture', privateKey }
|
|
})
|
|
const descriptorBytes = agentPackage.descriptorBytes(result.descriptor)
|
|
expect(descriptorBytes.length).toBeGreaterThan(1024 * 1024)
|
|
expect(descriptorBytes.length).toBeLessThan(4 * 1024 * 1024)
|
|
expect(agentCatalog.readPackageMetadata(archive, registry).sha256).toBe(result.sha256)
|
|
await expect(extractAndVerifyAgentPackage({
|
|
archivePath: archive, destinationDirectory: join(projectRoot, 'verified'),
|
|
architecture: 'x64', desktopVersion: '0.11.0', trustedRegistry: registry
|
|
})).resolves.toMatchObject({ descriptor: { contentDigest: result.descriptor.contentDigest } })
|
|
const options = {
|
|
operationRoot: join(projectRoot, 'operation'), archive,
|
|
expectedSha256: result.sha256, homeDirectory: join(projectRoot, 'home')
|
|
}
|
|
mkdirSync(options.homeDirectory, { mode: 0o700 })
|
|
expect(packageInstaller.preparePackage(options).status).toBe('prepared')
|
|
expect(packageInstaller.commitPackage(options).status).toBe('committed')
|
|
}, 60_000)
|
|
|
|
it('still rejects descriptors larger than four MiB before parsing', async () => {
|
|
const archive = join(temporaryRoot, 'oversized-descriptor.zip')
|
|
writeFileSync(archive, zipSync({
|
|
'agent-package.json': new Uint8Array(4 * 1024 * 1024 + 1)
|
|
}, { level: 0 }))
|
|
await expect(extractAndVerifyAgentPackage({
|
|
archivePath: archive, destinationDirectory: join(temporaryRoot, 'oversized-descriptor'),
|
|
desktopVersion: '0.11.0', trustedRegistry: registry
|
|
})).rejects.toThrow(/limit|large/iu)
|
|
})
|
|
|
|
it('assembles deterministic signed archives with private extraction', async () => {
|
|
const secondRoot = join(temporaryRoot, 'second')
|
|
mkdirSync(secondRoot)
|
|
const secondPath = join(
|
|
secondRoot,
|
|
agentPackageArchiveName(agentLock.agentVersion, 'x64')
|
|
)
|
|
const archive = unzipSync(
|
|
new Uint8Array(readFileSync(packagePath))
|
|
)
|
|
expect(Object.keys(archive).sort()).toEqual(
|
|
expect.arrayContaining([
|
|
'agent-package.json',
|
|
'agent-package.sig',
|
|
'agent-release-keys.json',
|
|
'agent-runtime-lock.json',
|
|
'remote-runtime-lock.json'
|
|
])
|
|
)
|
|
expect(packageResult.descriptor.files).toContainEqual(
|
|
expect.objectContaining({
|
|
path: 'agent/helpers/fixture-helper',
|
|
mode: '0755'
|
|
})
|
|
)
|
|
for (const [result, archivePath] of [
|
|
[packageResult, packagePath],
|
|
[arm64PackageResult, arm64PackagePath]
|
|
] as const) {
|
|
expect(result.descriptor.files).toContainEqual(
|
|
expect.objectContaining({
|
|
path: 'agent/node',
|
|
mode: '0755'
|
|
})
|
|
)
|
|
expect(result.descriptor.files).not.toContainEqual(
|
|
expect.objectContaining({
|
|
path: 'agent/lib/package-installer.cjs'
|
|
})
|
|
)
|
|
expect(
|
|
Object.keys(
|
|
unzipSync(new Uint8Array(readFileSync(archivePath)))
|
|
)
|
|
).not.toContain('agent/lib/package-installer.cjs')
|
|
}
|
|
|
|
const contentRoot = join(temporaryRoot, 'verified')
|
|
const verified = await extractAndVerifyAgentPackage({
|
|
archivePath: packagePath,
|
|
destinationDirectory: contentRoot,
|
|
architecture: 'x64',
|
|
desktopVersion: '0.11.0',
|
|
trustedRegistry: registry
|
|
})
|
|
expect(verified).toMatchObject({
|
|
descriptor: {
|
|
version: agentLock.agentVersion,
|
|
architecture: 'x64',
|
|
remoteRuntime: {
|
|
version:
|
|
remoteRuntimeLock.runtimes.opencode.version
|
|
}
|
|
}
|
|
})
|
|
if (process.platform !== 'win32') {
|
|
for (const directory of [verified.rootDirectory]) {
|
|
expect(lstatSync(directory).mode & 0o777).toBe(0o700)
|
|
}
|
|
}
|
|
|
|
const second = agentPackage.assembleAgentPackage({
|
|
projectRoot: process.cwd(),
|
|
architecture: 'x64',
|
|
minimumDesktopVersion: '0.11.0',
|
|
output: secondPath,
|
|
agentBundle: join(temporaryRoot, 'agent'),
|
|
runtimeBundle: findRuntimeBundleDirectory(),
|
|
agentLock,
|
|
runtimeLock: remoteRuntimeLock,
|
|
registry,
|
|
testSigningIdentity: {
|
|
keyId: 'agent-package-fixture',
|
|
privateKey
|
|
}
|
|
})
|
|
expect(second.archive).toBe(
|
|
agentPackageArchiveName(agentLock.agentVersion, 'x64')
|
|
)
|
|
expect(readFileSync(secondPath).equals(
|
|
readFileSync(packagePath)
|
|
)).toBe(true)
|
|
})
|
|
|
|
it('prepares and commits a format-v1 package without an embedded installer', () => {
|
|
const operationRoot = join(temporaryRoot, 'remote-operation')
|
|
const homeDirectory = join(temporaryRoot, 'remote-home')
|
|
const globalMetadata = seedGlobalManagedMetadata(
|
|
homeDirectory,
|
|
'a'
|
|
)
|
|
const events: unknown[] = []
|
|
const prepared = packageInstaller.preparePackage({
|
|
operationRoot,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory,
|
|
emit: (event) => events.push(event)
|
|
})
|
|
expect(prepared).toMatchObject({
|
|
type: 'result',
|
|
command: 'prepare',
|
|
status: 'prepared',
|
|
archiveSha256: packageResult.sha256,
|
|
agent: {
|
|
installationId: expect.stringMatching(/^agent-[a-f0-9]{64}$/u),
|
|
agentVersion: agentLock.agentVersion,
|
|
architecture: 'x64',
|
|
protocol: agentLock.protocol
|
|
},
|
|
runtime: {
|
|
runtimeId: 'opencode',
|
|
runtimeVersion:
|
|
remoteRuntimeLock.runtimes.opencode.version,
|
|
bundleDigest:
|
|
packageResult.descriptor.remoteRuntime.bundleDigest,
|
|
architecture: 'x64'
|
|
}
|
|
})
|
|
expect(events.at(-1)).toEqual(prepared)
|
|
expect(readdirSync(join(operationRoot, 'prepared')))
|
|
.toEqual([
|
|
'agent-package.json',
|
|
'agent-package.sig',
|
|
'agent-release-keys.json',
|
|
'agent-runtime-lock.json',
|
|
'payload',
|
|
'remote-runtime-lock.json',
|
|
'result.json'
|
|
])
|
|
expectGlobalMetadataUnchanged(globalMetadata)
|
|
|
|
const cliOperationRoot = join(temporaryRoot, 'remote-cli-operation')
|
|
const cliLines = execFileSync(
|
|
process.execPath,
|
|
[
|
|
installerBundle,
|
|
'prepare',
|
|
'--operation-root',
|
|
cliOperationRoot,
|
|
'--archive',
|
|
packagePath,
|
|
'--expected-sha256',
|
|
packageResult.sha256
|
|
],
|
|
{ encoding: 'utf8' }
|
|
)
|
|
.trim()
|
|
.split('\n')
|
|
.map((line) => JSON.parse(line) as Record<string, unknown>)
|
|
expect(cliLines[0]).toMatchObject({
|
|
type: 'progress',
|
|
command: 'prepare',
|
|
phase: 'validating'
|
|
})
|
|
expect(cliLines.at(-1)).toMatchObject({
|
|
type: 'result',
|
|
command: 'prepare',
|
|
status: 'prepared',
|
|
agent: prepared.agent,
|
|
runtime: prepared.runtime
|
|
})
|
|
|
|
const commitEvents: Array<Record<string, unknown>> = []
|
|
const committed = packageInstaller.commitPackage({
|
|
operationRoot,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory,
|
|
emit: (event) => {
|
|
commitEvents.push(event as Record<string, unknown>)
|
|
}
|
|
})
|
|
expect(committed).toMatchObject({
|
|
command: 'commit',
|
|
status: 'committed',
|
|
agent: prepared.agent,
|
|
runtime: prepared.runtime
|
|
})
|
|
const agentDestination = join(
|
|
homeDirectory,
|
|
'.goodbuddy',
|
|
'agent',
|
|
'installations',
|
|
prepared.agent.installationId
|
|
)
|
|
const runtimeDestination = join(
|
|
homeDirectory,
|
|
'.goodbuddy',
|
|
'runtimes',
|
|
'opencode',
|
|
prepared.runtime.bundleDigest.slice('sha256:'.length)
|
|
)
|
|
expect(lstatSync(join(agentDestination, 'lib', 'agent.cjs')).isFile())
|
|
.toBe(true)
|
|
expect(lstatSync(join(runtimeDestination, 'bin', 'opencode')).isFile())
|
|
.toBe(true)
|
|
expect(
|
|
commitEvents.filter(
|
|
(event) =>
|
|
event.type === 'progress' &&
|
|
event.phase === 'publishing-content'
|
|
)
|
|
).toHaveLength(1)
|
|
expect(readdirSync(join(operationRoot, 'prepared')))
|
|
.toEqual([
|
|
'agent-package.json',
|
|
'agent-package.sig',
|
|
'agent-release-keys.json',
|
|
'agent-runtime-lock.json',
|
|
'payload',
|
|
'remote-runtime-lock.json',
|
|
'result.json'
|
|
])
|
|
expect(readdirSync(operationRoot)).toEqual(['prepared'])
|
|
expectRegistriesUnchangedAndPackageTrustPublished(
|
|
globalMetadata
|
|
)
|
|
|
|
packageInstaller.preparePackage({
|
|
operationRoot,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
expect(() =>
|
|
packageInstaller.commitPackage({
|
|
operationRoot,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
).not.toThrow()
|
|
expectRegistriesUnchangedAndPackageTrustPublished(
|
|
globalMetadata
|
|
)
|
|
})
|
|
|
|
it('resumes commit after an Agent directory was already published', () => {
|
|
const operationRoot = join(
|
|
temporaryRoot,
|
|
'partial-commit-operation'
|
|
)
|
|
const homeDirectory = join(
|
|
temporaryRoot,
|
|
'partial-commit-home'
|
|
)
|
|
const prepared = packageInstaller.preparePackage({
|
|
operationRoot,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
const agentSource = join(
|
|
operationRoot,
|
|
'prepared',
|
|
'payload',
|
|
'agent'
|
|
)
|
|
const agentDestination = join(
|
|
homeDirectory,
|
|
'.goodbuddy',
|
|
'agent',
|
|
'installations',
|
|
prepared.agent.installationId
|
|
)
|
|
mkdirSync(dirname(agentDestination), {
|
|
recursive: true,
|
|
mode: 0o700
|
|
})
|
|
renameSync(agentSource, agentDestination)
|
|
|
|
expect(
|
|
packageInstaller.commitPackage({
|
|
operationRoot,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
).toMatchObject({
|
|
status: 'committed',
|
|
agent: prepared.agent,
|
|
runtime: prepared.runtime
|
|
})
|
|
expect(
|
|
lstatSync(join(agentDestination, 'goodbuddy-agent'))
|
|
.isFile()
|
|
).toBe(true)
|
|
expect(
|
|
lstatSync(
|
|
join(
|
|
homeDirectory,
|
|
'.goodbuddy',
|
|
'runtimes',
|
|
'opencode',
|
|
prepared.runtime.bundleDigest.slice(
|
|
'sha256:'.length
|
|
),
|
|
'bin',
|
|
'opencode'
|
|
)
|
|
).isFile()
|
|
).toBe(true)
|
|
})
|
|
|
|
it('requires untampered prepared state before commit mutation', () => {
|
|
const homeDirectory = join(temporaryRoot, 'prepared-state-home')
|
|
const tamperedOperation = join(
|
|
temporaryRoot,
|
|
'tampered-prepared-state-operation'
|
|
)
|
|
packageInstaller.preparePackage({
|
|
operationRoot: tamperedOperation,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
const statePath = join(tamperedOperation, 'prepared', 'result.json')
|
|
const state = JSON.parse(
|
|
readFileSync(statePath, 'utf8')
|
|
) as Record<string, unknown>
|
|
const stateAgent = state.agent as Record<string, unknown>
|
|
stateAgent.agentVersion = '99.0.0'
|
|
writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`)
|
|
expect(() =>
|
|
packageInstaller.commitPackage({
|
|
operationRoot: tamperedOperation,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
).toThrow('does not match the authenticated archive')
|
|
expect(readdirSync(tamperedOperation)).toEqual(['prepared'])
|
|
|
|
const missingOperation = join(
|
|
temporaryRoot,
|
|
'missing-prepared-state-operation'
|
|
)
|
|
packageInstaller.preparePackage({
|
|
operationRoot: missingOperation,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
rmSync(join(missingOperation, 'prepared', 'result.json'))
|
|
expect(() =>
|
|
packageInstaller.commitPackage({
|
|
operationRoot: missingOperation,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
).toThrow()
|
|
expect(readdirSync(missingOperation)).toEqual(['prepared'])
|
|
})
|
|
|
|
it('preserves current registries when commit fails', () => {
|
|
const operationRoot = join(temporaryRoot, 'failed-commit-operation')
|
|
const homeDirectory = join(temporaryRoot, 'failed-commit-home')
|
|
const globalMetadata = seedGlobalManagedMetadata(
|
|
homeDirectory,
|
|
'e'
|
|
)
|
|
const prepared = packageInstaller.preparePackage({
|
|
operationRoot,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
const conflictingAgentDestination = join(
|
|
homeDirectory,
|
|
'.goodbuddy',
|
|
'agent',
|
|
'installations',
|
|
prepared.agent.installationId
|
|
)
|
|
mkdirSync(dirname(conflictingAgentDestination), {
|
|
recursive: true,
|
|
mode: 0o700
|
|
})
|
|
writeFileSync(
|
|
conflictingAgentDestination,
|
|
'different'
|
|
)
|
|
|
|
expect(() =>
|
|
packageInstaller.commitPackage({
|
|
operationRoot,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
).toThrow('conflicting content')
|
|
expectGlobalMetadataUnchanged(globalMetadata)
|
|
})
|
|
|
|
it.runIf(process.platform !== 'win32')(
|
|
'rejects symbolic-link ancestors before publishing managed payloads',
|
|
() => {
|
|
const cases = [
|
|
{
|
|
name: 'managed-root',
|
|
link(homeDirectory: string, linkTarget: string) {
|
|
symlinkSync(
|
|
linkTarget,
|
|
join(homeDirectory, '.goodbuddy')
|
|
)
|
|
}
|
|
},
|
|
{
|
|
name: 'agent-installations',
|
|
link(homeDirectory: string, linkTarget: string) {
|
|
const agentRoot = join(
|
|
homeDirectory,
|
|
'.goodbuddy',
|
|
'agent'
|
|
)
|
|
mkdirSync(agentRoot, {
|
|
recursive: true,
|
|
mode: 0o700
|
|
})
|
|
symlinkSync(
|
|
linkTarget,
|
|
join(agentRoot, 'installations')
|
|
)
|
|
}
|
|
},
|
|
{
|
|
name: 'runtime-root',
|
|
link(homeDirectory: string, linkTarget: string) {
|
|
const runtimes = join(
|
|
homeDirectory,
|
|
'.goodbuddy',
|
|
'runtimes'
|
|
)
|
|
mkdirSync(runtimes, {
|
|
recursive: true,
|
|
mode: 0o700
|
|
})
|
|
symlinkSync(
|
|
linkTarget,
|
|
join(runtimes, 'opencode')
|
|
)
|
|
}
|
|
}
|
|
]
|
|
|
|
for (const fixture of cases) {
|
|
const operationRoot = join(
|
|
temporaryRoot,
|
|
`symlink-${fixture.name}-operation`
|
|
)
|
|
const homeDirectory = join(
|
|
temporaryRoot,
|
|
`symlink-${fixture.name}-home`
|
|
)
|
|
const linkTarget = join(
|
|
temporaryRoot,
|
|
`symlink-${fixture.name}-target`
|
|
)
|
|
mkdirSync(homeDirectory, { mode: 0o700 })
|
|
mkdirSync(linkTarget, { mode: 0o700 })
|
|
fixture.link(homeDirectory, linkTarget)
|
|
packageInstaller.preparePackage({
|
|
operationRoot,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
|
|
expect(() =>
|
|
packageInstaller.commitPackage({
|
|
operationRoot,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
).toThrow('Managed installation directory is unsafe')
|
|
expect(readdirSync(linkTarget)).toEqual([])
|
|
}
|
|
}
|
|
)
|
|
|
|
it('replaces a conflicting managed digest directory during explicit repair', () => {
|
|
const operationRoot = join(temporaryRoot, 'repair-commit-operation')
|
|
const homeDirectory = join(temporaryRoot, 'repair-commit-home')
|
|
const globalMetadata = seedGlobalManagedMetadata(
|
|
homeDirectory,
|
|
'f'
|
|
)
|
|
const prepared = packageInstaller.preparePackage({
|
|
operationRoot,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
const agentDestination = join(
|
|
homeDirectory,
|
|
'.goodbuddy',
|
|
'agent',
|
|
'installations',
|
|
prepared.agent.installationId
|
|
)
|
|
mkdirSync(agentDestination, { recursive: true, mode: 0o700 })
|
|
writeFileSync(
|
|
join(agentDestination, 'conflicting-content'),
|
|
'different'
|
|
)
|
|
|
|
expect(
|
|
packageInstaller.commitPackage({
|
|
operationRoot,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
).toMatchObject({
|
|
status: 'committed',
|
|
agent: prepared.agent,
|
|
runtime: prepared.runtime
|
|
})
|
|
expect(
|
|
readdirSync(agentDestination)
|
|
).not.toContain('conflicting-content')
|
|
expect(
|
|
readFileSync(join(agentDestination, 'manifest.json'))
|
|
).toBeTruthy()
|
|
expectRegistriesUnchangedAndPackageTrustPublished(
|
|
globalMetadata
|
|
)
|
|
})
|
|
|
|
it('restores a replaced Agent directory when Runtime publication fails', () => {
|
|
const operationRoot = join(temporaryRoot, 'repair-rollback-operation')
|
|
const homeDirectory = join(temporaryRoot, 'repair-rollback-home')
|
|
const prepared = packageInstaller.preparePackage({
|
|
operationRoot,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
const agentDestination = join(
|
|
homeDirectory,
|
|
'.goodbuddy',
|
|
'agent',
|
|
'installations',
|
|
prepared.agent.installationId
|
|
)
|
|
mkdirSync(agentDestination, { recursive: true, mode: 0o700 })
|
|
writeFileSync(join(agentDestination, 'old-marker'), 'old')
|
|
const runtimeDestination = join(
|
|
homeDirectory,
|
|
'.goodbuddy',
|
|
'runtimes',
|
|
'opencode',
|
|
prepared.runtime.bundleDigest.slice('sha256:'.length)
|
|
)
|
|
mkdirSync(dirname(runtimeDestination), {
|
|
recursive: true,
|
|
mode: 0o700
|
|
})
|
|
writeFileSync(runtimeDestination, 'unsafe destination type')
|
|
|
|
expect(() =>
|
|
packageInstaller.commitPackage({
|
|
operationRoot,
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
).toThrow('conflicting content')
|
|
expect(readdirSync(agentDestination)).toEqual(['old-marker'])
|
|
})
|
|
|
|
it('publishes each Agent payload independently of existing installations', () => {
|
|
const variantAgent = join(temporaryRoot, 'reuse-variant-agent')
|
|
writeAgentBundle(variantAgent, 'x64')
|
|
writeFileSync(
|
|
join(variantAgent, 'lib', 'agent.cjs'),
|
|
'module.exports={variant:true}\n'
|
|
)
|
|
signAgentBundleManifest(variantAgent, 'x64')
|
|
const variantPath = join(
|
|
temporaryRoot,
|
|
'reuse-variant',
|
|
agentPackageArchiveName(agentLock.agentVersion, 'x64')
|
|
)
|
|
mkdirSync(dirname(variantPath), { recursive: true })
|
|
const variant = agentPackage.assembleAgentPackage({
|
|
projectRoot: process.cwd(),
|
|
architecture: 'x64',
|
|
minimumDesktopVersion: '0.11.0',
|
|
output: variantPath,
|
|
agentBundle: variantAgent,
|
|
runtimeBundle: findRuntimeBundleDirectory(),
|
|
agentLock,
|
|
runtimeLock: remoteRuntimeLock,
|
|
registry,
|
|
testSigningIdentity: {
|
|
keyId: 'agent-package-fixture',
|
|
privateKey
|
|
}
|
|
})
|
|
const changedNodeAgent = join(temporaryRoot, 'reuse-changed-node-agent')
|
|
writeAgentBundle(changedNodeAgent, 'x64')
|
|
writeFileSync(
|
|
join(changedNodeAgent, 'node'),
|
|
Buffer.concat([elf('x64'), Buffer.from('different Node payload')])
|
|
)
|
|
chmodSync(join(changedNodeAgent, 'node'), 0o755)
|
|
signAgentBundleManifest(changedNodeAgent, 'x64')
|
|
const changedNodePath = join(
|
|
temporaryRoot,
|
|
'reuse-changed-node',
|
|
agentPackageArchiveName(agentLock.agentVersion, 'x64')
|
|
)
|
|
mkdirSync(dirname(changedNodePath), { recursive: true })
|
|
const changedNodeVariant = agentPackage.assembleAgentPackage({
|
|
projectRoot: process.cwd(),
|
|
architecture: 'x64',
|
|
minimumDesktopVersion: '0.11.0',
|
|
output: changedNodePath,
|
|
agentBundle: changedNodeAgent,
|
|
runtimeBundle: findRuntimeBundleDirectory(),
|
|
agentLock,
|
|
runtimeLock: remoteRuntimeLock,
|
|
registry,
|
|
testSigningIdentity: {
|
|
keyId: 'agent-package-fixture',
|
|
privateKey
|
|
}
|
|
})
|
|
const prepareAndCommit = (
|
|
options: Parameters<PackageInstallerModule['preparePackage']>[0]
|
|
) => {
|
|
packageInstaller.preparePackage(options)
|
|
return packageInstaller.commitPackage(options)
|
|
}
|
|
|
|
const installCurrent = (
|
|
name: string
|
|
): {
|
|
homeDirectory: string
|
|
currentNode: string
|
|
globalMetadata: Map<string, Buffer>
|
|
} => {
|
|
const homeDirectory = join(temporaryRoot, `reuse-${name}-home`)
|
|
const globalMetadata = seedGlobalManagedMetadata(homeDirectory, 'b')
|
|
const current = prepareAndCommit({
|
|
operationRoot: join(temporaryRoot, `reuse-${name}-current-operation`),
|
|
archive: packagePath,
|
|
expectedSha256: packageResult.sha256,
|
|
homeDirectory
|
|
})
|
|
const registryPath = join(
|
|
homeDirectory,
|
|
'.goodbuddy',
|
|
'agent',
|
|
'registry.json'
|
|
)
|
|
const registryBytes = Buffer.from(
|
|
`${JSON.stringify({
|
|
formatVersion: 1,
|
|
current: {
|
|
installationId: current.agent.installationId,
|
|
agentVersion: current.agent.agentVersion,
|
|
manifestSha256: current.agent.manifestSha256,
|
|
arch: current.agent.architecture
|
|
}
|
|
}, null, 2)}\n`
|
|
)
|
|
writeFileSync(registryPath, registryBytes)
|
|
globalMetadata.set(registryPath, registryBytes)
|
|
return {
|
|
homeDirectory,
|
|
currentNode: join(
|
|
homeDirectory,
|
|
'.goodbuddy',
|
|
'agent',
|
|
'installations',
|
|
current.agent.installationId,
|
|
'node'
|
|
),
|
|
globalMetadata
|
|
}
|
|
}
|
|
const commitVariant = (name: string, homeDirectory: string) =>
|
|
prepareAndCommit({
|
|
operationRoot: join(temporaryRoot, `reuse-${name}-variant-operation`),
|
|
archive: variantPath,
|
|
expectedSha256: variant.sha256,
|
|
homeDirectory
|
|
})
|
|
const installedVariantNode = (
|
|
homeDirectory: string,
|
|
installationId: string
|
|
) => join(
|
|
homeDirectory,
|
|
'.goodbuddy',
|
|
'agent',
|
|
'installations',
|
|
installationId,
|
|
'node'
|
|
)
|
|
|
|
const identical = installCurrent('identical')
|
|
const identicalResult = commitVariant(
|
|
'identical',
|
|
identical.homeDirectory
|
|
)
|
|
expect(
|
|
lstatSync(identical.currentNode).ino
|
|
).not.toBe(
|
|
lstatSync(
|
|
installedVariantNode(
|
|
identical.homeDirectory,
|
|
identicalResult.agent.installationId
|
|
)
|
|
).ino
|
|
)
|
|
expectRegistriesUnchangedAndPackageTrustPublished(
|
|
identical.globalMetadata
|
|
)
|
|
|
|
const changed = installCurrent('changed')
|
|
const changedResult = prepareAndCommit({
|
|
operationRoot: join(
|
|
temporaryRoot,
|
|
'reuse-changed-variant-operation'
|
|
),
|
|
archive: changedNodePath,
|
|
expectedSha256: changedNodeVariant.sha256,
|
|
homeDirectory: changed.homeDirectory
|
|
})
|
|
expect(
|
|
lstatSync(changed.currentNode).ino
|
|
).not.toBe(
|
|
lstatSync(
|
|
installedVariantNode(
|
|
changed.homeDirectory,
|
|
changedResult.agent.installationId
|
|
)
|
|
).ino
|
|
)
|
|
expectRegistriesUnchangedAndPackageTrustPublished(
|
|
changed.globalMetadata
|
|
)
|
|
|
|
const corrupt = installCurrent('corrupt')
|
|
writeFileSync(corrupt.currentNode, Buffer.from('corrupt Node'))
|
|
const corruptResult = commitVariant('corrupt', corrupt.homeDirectory)
|
|
expect(
|
|
lstatSync(corrupt.currentNode).ino
|
|
).not.toBe(
|
|
lstatSync(
|
|
installedVariantNode(
|
|
corrupt.homeDirectory,
|
|
corruptResult.agent.installationId
|
|
)
|
|
).ino
|
|
)
|
|
expectRegistriesUnchangedAndPackageTrustPublished(
|
|
corrupt.globalMetadata
|
|
)
|
|
|
|
const untrusted = installCurrent('untrusted')
|
|
const currentRoot = dirname(untrusted.currentNode)
|
|
const signaturePath = join(currentRoot, 'manifest.sig')
|
|
const signature = readFileSync(signaturePath)
|
|
signature[0] = signature[0] === 65 ? 66 : 65
|
|
writeFileSync(signaturePath, signature)
|
|
const untrustedResult = commitVariant('untrusted', untrusted.homeDirectory)
|
|
expect(
|
|
lstatSync(untrusted.currentNode).ino
|
|
).not.toBe(
|
|
lstatSync(
|
|
installedVariantNode(
|
|
untrusted.homeDirectory,
|
|
untrustedResult.agent.installationId
|
|
)
|
|
).ino
|
|
)
|
|
expectRegistriesUnchangedAndPackageTrustPublished(
|
|
untrusted.globalMetadata
|
|
)
|
|
})
|
|
|
|
it('fails the remote installer closed on traversal, duplicates, corruption, and hash mismatch', () => {
|
|
const run = (archive: string, expectedSha256?: string) =>
|
|
packageInstaller.preparePackage({
|
|
operationRoot: join(
|
|
temporaryRoot,
|
|
`rejected-${basename(archive)}`
|
|
),
|
|
archive,
|
|
expectedSha256:
|
|
expectedSha256 ??
|
|
createHash('sha256').update(readFileSync(archive)).digest('hex'),
|
|
homeDirectory: join(temporaryRoot, 'rejected-home')
|
|
})
|
|
|
|
const traversal = join(temporaryRoot, 'installer-traversal.gbagent')
|
|
writeFileSync(
|
|
traversal,
|
|
renameStoredZipEntry(
|
|
readFileSync(packagePath),
|
|
'agent/node',
|
|
'../outside'
|
|
)
|
|
)
|
|
expect(() => run(traversal)).toThrow(/unsafe|unordered/u)
|
|
|
|
const duplicate = join(temporaryRoot, 'installer-duplicate.gbagent')
|
|
const names = Object.keys(
|
|
unzipSync(new Uint8Array(readFileSync(packagePath)))
|
|
)
|
|
const duplicatePair = names
|
|
.flatMap((left) =>
|
|
names.map((right) => [left, right] as const)
|
|
)
|
|
.find(
|
|
([left, right]) =>
|
|
left !== right && Buffer.byteLength(left) === Buffer.byteLength(right)
|
|
)
|
|
expect(duplicatePair).toBeDefined()
|
|
writeFileSync(
|
|
duplicate,
|
|
renameStoredZipEntry(
|
|
readFileSync(packagePath),
|
|
duplicatePair![0],
|
|
duplicatePair![1]
|
|
)
|
|
)
|
|
expect(() => run(duplicate)).toThrow(/duplicate|unordered/u)
|
|
|
|
const corrupt = join(temporaryRoot, 'installer-corrupt.gbagent')
|
|
const corruptBytes = corruptStoredZipEntry(
|
|
readFileSync(packagePath),
|
|
'agent/lib/agent.cjs'
|
|
)
|
|
writeFileSync(corrupt, corruptBytes)
|
|
expect(() => run(corrupt)).toThrow(/checksum|digest|header/u)
|
|
|
|
expect(() =>
|
|
run(packagePath, '0'.repeat(64))
|
|
).toThrow('SHA-256 does not match')
|
|
})
|
|
|
|
it('rejects traversal, undeclared payload, and untrusted outer signatures', async () => {
|
|
const traversal = join(temporaryRoot, 'traversal.gbagent')
|
|
writeFileSync(
|
|
traversal,
|
|
Buffer.from(
|
|
zipSync({
|
|
'../escape': new Uint8Array([1])
|
|
})
|
|
)
|
|
)
|
|
await expect(
|
|
extractAndVerifyAgentPackage({
|
|
archivePath: traversal,
|
|
destinationDirectory: join(
|
|
temporaryRoot,
|
|
'traversal-output'
|
|
),
|
|
desktopVersion: '0.11.0',
|
|
trustedRegistry: registry
|
|
})
|
|
).rejects.toThrow('unsafe')
|
|
|
|
const files = unzipSync(
|
|
new Uint8Array(readFileSync(packagePath))
|
|
)
|
|
files['undeclared.txt'] = new Uint8Array([1])
|
|
const undeclared = join(temporaryRoot, 'undeclared.gbagent')
|
|
writeFileSync(undeclared, Buffer.from(zipSync(files)))
|
|
await expect(
|
|
extractAndVerifyAgentPackage({
|
|
archivePath: undeclared,
|
|
destinationDirectory: join(
|
|
temporaryRoot,
|
|
'undeclared-output'
|
|
),
|
|
desktopVersion: '0.11.0',
|
|
trustedRegistry: registry
|
|
})
|
|
).rejects.toThrow('undeclared or missing')
|
|
|
|
await expect(
|
|
extractAndVerifyAgentPackage({
|
|
archivePath: packagePath,
|
|
destinationDirectory: join(
|
|
temporaryRoot,
|
|
'untrusted-output'
|
|
),
|
|
desktopVersion: '0.11.0',
|
|
trustedRegistry: {
|
|
formatVersion: 1,
|
|
keys: [],
|
|
revocations: []
|
|
}
|
|
})
|
|
).rejects.toThrow('signing key is not trusted')
|
|
})
|
|
|
|
it('enforces desktop compatibility and descriptor-to-inner-bundle identity', async () => {
|
|
await expect(
|
|
extractAndVerifyAgentPackage({
|
|
archivePath: packagePath,
|
|
destinationDirectory: join(
|
|
temporaryRoot,
|
|
'old-desktop-output'
|
|
),
|
|
desktopVersion: '0.10.9',
|
|
trustedRegistry: registry
|
|
})
|
|
).rejects.toThrow('not compatible')
|
|
|
|
const files = unzipSync(
|
|
new Uint8Array(readFileSync(packagePath))
|
|
)
|
|
const descriptor = agentPackageDescriptorSchema.parse(
|
|
JSON.parse(
|
|
Buffer.from(files['agent-package.json']!).toString('utf8')
|
|
)
|
|
)
|
|
const mismatched = {
|
|
...descriptor,
|
|
version:
|
|
descriptor.version === '999.0.0'
|
|
? '999.0.1'
|
|
: '999.0.0'
|
|
}
|
|
mismatched.contentDigest =
|
|
agentPackage.descriptorContentDigest(mismatched)
|
|
const descriptorBytes =
|
|
agentPackage.descriptorBytes(mismatched)
|
|
files['agent-package.json'] = new Uint8Array(descriptorBytes)
|
|
files['agent-package.sig'] = new Uint8Array(
|
|
Buffer.from(
|
|
`${sign(
|
|
null,
|
|
Buffer.concat([
|
|
agentPackage.signatureDomain,
|
|
descriptorBytes
|
|
]),
|
|
privateKey
|
|
).toString('base64')}\n`
|
|
)
|
|
)
|
|
const mismatchPath = join(temporaryRoot, 'mismatch.gbagent')
|
|
writeFileSync(mismatchPath, Buffer.from(zipSync(files)))
|
|
await expect(
|
|
extractAndVerifyAgentPackage({
|
|
archivePath: mismatchPath,
|
|
destinationDirectory: join(
|
|
temporaryRoot,
|
|
'mismatch-output'
|
|
),
|
|
desktopVersion: '0.11.0',
|
|
trustedRegistry: registry
|
|
})
|
|
).rejects.toThrow('does not match its Agent bundle')
|
|
})
|
|
|
|
it('accepts a signed package whose key registry uses CRLF', async () => {
|
|
const files = unzipSync(
|
|
new Uint8Array(readFileSync(packagePath))
|
|
)
|
|
const registryBytes = Buffer.from(
|
|
Buffer.from(files['agent-release-keys.json']!)
|
|
.toString('utf8')
|
|
.replace(/\n/gu, '\r\n')
|
|
)
|
|
files['agent-release-keys.json'] =
|
|
new Uint8Array(registryBytes)
|
|
const descriptor = agentPackageDescriptorSchema.parse(
|
|
JSON.parse(
|
|
Buffer.from(files['agent-package.json']!).toString('utf8')
|
|
)
|
|
)
|
|
const updatedDescriptor = {
|
|
...descriptor,
|
|
files: descriptor.files.map((file) =>
|
|
file.path === 'agent-release-keys.json'
|
|
? {
|
|
...file,
|
|
size: registryBytes.byteLength,
|
|
sha256: createHash('sha256')
|
|
.update(registryBytes)
|
|
.digest('hex')
|
|
}
|
|
: file
|
|
)
|
|
}
|
|
updatedDescriptor.contentDigest =
|
|
agentPackage.descriptorContentDigest(updatedDescriptor)
|
|
const descriptorBytes =
|
|
agentPackage.descriptorBytes(updatedDescriptor)
|
|
files['agent-package.json'] = new Uint8Array(descriptorBytes)
|
|
files['agent-package.sig'] = new Uint8Array(
|
|
Buffer.from(
|
|
`${sign(
|
|
null,
|
|
Buffer.concat([
|
|
agentPackage.signatureDomain,
|
|
descriptorBytes
|
|
]),
|
|
privateKey
|
|
).toString('base64')}\n`
|
|
)
|
|
)
|
|
const crlfPath = join(
|
|
temporaryRoot,
|
|
'crlf-registry.gbagent'
|
|
)
|
|
writeFileSync(crlfPath, Buffer.from(zipSync(files)))
|
|
|
|
await expect(
|
|
extractAndVerifyAgentPackage({
|
|
archivePath: crlfPath,
|
|
destinationDirectory: join(
|
|
temporaryRoot,
|
|
'crlf-registry-output'
|
|
),
|
|
desktopVersion: '0.11.0',
|
|
trustedRegistry: registry
|
|
})
|
|
).resolves.toMatchObject({
|
|
descriptor: { architecture: 'x64' }
|
|
})
|
|
})
|
|
|
|
it('orders SemVer prereleases and selects the latest compatible protocol', () => {
|
|
expect(compareSemanticVersions('1.0.0-e2e.10', '1.0.0-e2e.9'))
|
|
.toBeGreaterThan(0)
|
|
expect(compareSemanticVersions('1.0.0', '1.0.0-rc.1'))
|
|
.toBeGreaterThan(0)
|
|
expect(compareSemanticVersions('1.0.0-1', '1.0.0-alpha'))
|
|
.toBeLessThan(0)
|
|
|
|
const compatible = catalogEntry(packageResult.descriptor, {
|
|
version: '1.1.0',
|
|
minimumDesktopVersion: '0.11.0'
|
|
})
|
|
const tooNew = catalogEntry(packageResult.descriptor, {
|
|
version: '2.0.0',
|
|
minimumDesktopVersion: '0.12.0'
|
|
})
|
|
const incompatibleProtocol = catalogEntry(
|
|
packageResult.descriptor,
|
|
{
|
|
version: '3.0.0',
|
|
minimumDesktopVersion: '0.11.0',
|
|
agentProtocol: {
|
|
...packageResult.descriptor.agentProtocol,
|
|
major:
|
|
packageResult.descriptor.agentProtocol.major + 1
|
|
}
|
|
}
|
|
)
|
|
const catalog = agentPackageCatalogSchema.parse({
|
|
formatVersion: 1,
|
|
product: 'GoodBuddy',
|
|
component: 'agent',
|
|
signingKeyId: 'agent-package-fixture',
|
|
generatedAt: '2026-08-24T00:00:00.000Z',
|
|
entries: [tooNew, incompatibleProtocol, compatible]
|
|
})
|
|
expect(
|
|
selectLatestCompatibleEntry(catalog, 'x64', '0.11.0')
|
|
.version
|
|
).toBe('1.1.0')
|
|
})
|
|
|
|
it('builds and verifies a cumulative immutable dual-architecture catalog', () => {
|
|
const firstRoot = join(temporaryRoot, 'catalog-first')
|
|
const secondRoot = join(temporaryRoot, 'catalog-second')
|
|
mkdirSync(firstRoot)
|
|
mkdirSync(secondRoot)
|
|
const firstCatalog = join(firstRoot, 'agent-catalog.json')
|
|
const firstSignature = join(firstRoot, 'agent-catalog.sig')
|
|
const result = agentCatalog.createCatalog({
|
|
projectRoot: process.cwd(),
|
|
x64Package: packagePath,
|
|
arm64Package: arm64PackagePath,
|
|
outputCatalog: firstCatalog,
|
|
outputSignature: firstSignature,
|
|
generatedAt: '2026-08-24T00:00:00.000Z',
|
|
registry,
|
|
signingIdentity: {
|
|
keyId: 'agent-package-fixture',
|
|
privateKey
|
|
}
|
|
})
|
|
expect(result).toEqual({
|
|
version: agentLock.agentVersion,
|
|
catalog: 'agent-catalog.json',
|
|
signature: 'agent-catalog.sig',
|
|
entries: 2
|
|
})
|
|
const catalog = agentCatalog.readVerifiedCatalog(
|
|
firstCatalog,
|
|
firstSignature,
|
|
registry
|
|
)
|
|
expect(catalog.entries.map((entry) => entry.architecture))
|
|
.toEqual(['arm64', 'x64'])
|
|
expect(catalog.entries).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
architecture: 'x64',
|
|
sha256: packageResult.sha256
|
|
}),
|
|
expect.objectContaining({
|
|
architecture: 'arm64',
|
|
sha256: arm64PackageResult.sha256
|
|
})
|
|
])
|
|
)
|
|
const secondCatalog = join(secondRoot, 'agent-catalog.json')
|
|
const secondSignature = join(secondRoot, 'agent-catalog.sig')
|
|
agentCatalog.createCatalog({
|
|
projectRoot: process.cwd(),
|
|
x64Package: packagePath,
|
|
arm64Package: arm64PackagePath,
|
|
previousCatalog: firstCatalog,
|
|
previousSignature: firstSignature,
|
|
outputCatalog: secondCatalog,
|
|
outputSignature: secondSignature,
|
|
generatedAt: '2026-08-24T00:00:00.000Z',
|
|
registry,
|
|
signingIdentity: {
|
|
keyId: 'agent-package-fixture',
|
|
privateKey
|
|
}
|
|
})
|
|
expect(readFileSync(secondCatalog)).toEqual(
|
|
readFileSync(firstCatalog)
|
|
)
|
|
expect(readFileSync(secondSignature)).toEqual(
|
|
readFileSync(firstSignature)
|
|
)
|
|
|
|
const changedRoot = join(temporaryRoot, 'changed-package')
|
|
mkdirSync(changedRoot)
|
|
const changedPath = join(
|
|
changedRoot,
|
|
agentPackageArchiveName(agentLock.agentVersion, 'x64')
|
|
)
|
|
const originalPackage = readFileSync(packagePath)
|
|
const changedPackage = Buffer.concat([
|
|
originalPackage,
|
|
Buffer.from([0])
|
|
])
|
|
changedPackage.writeUInt16LE(1, originalPackage.length - 2)
|
|
writeFileSync(
|
|
changedPath,
|
|
changedPackage
|
|
)
|
|
expect(() =>
|
|
agentCatalog.createCatalog({
|
|
projectRoot: process.cwd(),
|
|
x64Package: changedPath,
|
|
arm64Package: arm64PackagePath,
|
|
previousCatalog: firstCatalog,
|
|
previousSignature: firstSignature,
|
|
outputCatalog: join(
|
|
temporaryRoot,
|
|
'changed-catalog.json'
|
|
),
|
|
outputSignature: join(
|
|
temporaryRoot,
|
|
'changed-catalog.sig'
|
|
),
|
|
generatedAt: '2026-08-24T00:00:00.000Z',
|
|
registry,
|
|
signingIdentity: {
|
|
keyId: 'agent-package-fixture',
|
|
privateKey
|
|
}
|
|
})
|
|
).toThrow('identity is immutable')
|
|
})
|
|
|
|
it('verifies stored payload bytes that contain a ZIP descriptor signature', () => {
|
|
expect(
|
|
agentCatalog.readPackageMetadata(packagePath, registry)
|
|
).toMatchObject({
|
|
descriptor: {
|
|
architecture: 'x64'
|
|
},
|
|
size: packageResult.size,
|
|
sha256: packageResult.sha256
|
|
})
|
|
})
|
|
|
|
it('imports and exports a fully verified offline package atomically', async () => {
|
|
const keyRegistryPath = join(
|
|
temporaryRoot,
|
|
'trusted-registry.json'
|
|
)
|
|
writeFileSync(
|
|
keyRegistryPath,
|
|
`${JSON.stringify(registry, null, 2)}\n`
|
|
)
|
|
const manager = new AgentPackageManager({
|
|
userDataPath: join(temporaryRoot, 'user-data'),
|
|
desktopVersion: '0.11.0',
|
|
keyRegistryPath,
|
|
getUpdateSource: vi.fn(async () => 'github' as const)
|
|
})
|
|
expect(
|
|
(await manager.getInventory()).entries.find(
|
|
(entry) => entry.architecture === 'x64'
|
|
)
|
|
).toMatchObject({
|
|
architecture: 'x64',
|
|
state: 'not-downloaded'
|
|
})
|
|
const invalidArm64 = join(
|
|
temporaryRoot,
|
|
'user-data',
|
|
'remote-components',
|
|
'agent-packages',
|
|
'linux-arm64',
|
|
'broken'
|
|
)
|
|
mkdirSync(invalidArm64, { recursive: true })
|
|
writeFileSync(join(invalidArm64, 'junk'), 'invalid')
|
|
expect(
|
|
(
|
|
await manager.getInventory({ refresh: true })
|
|
).entries.find(
|
|
(entry) => entry.architecture === 'arm64'
|
|
)
|
|
).toMatchObject({
|
|
architecture: 'arm64',
|
|
state: 'invalid'
|
|
})
|
|
await manager.importArchive(packagePath)
|
|
expect(
|
|
(await manager.getInventory()).entries.find(
|
|
(entry) => entry.architecture === 'x64'
|
|
)
|
|
).toMatchObject({
|
|
architecture: 'x64',
|
|
state: 'verified',
|
|
version: agentLock.agentVersion,
|
|
remoteRuntimeVersion:
|
|
remoteRuntimeLock.runtimes.opencode.version
|
|
})
|
|
|
|
const exported = join(temporaryRoot, 'offline-export.gbagent')
|
|
writeFileSync(exported, 'previous')
|
|
await manager.exportArchive('x64', exported)
|
|
expect(readFileSync(exported)).toEqual(readFileSync(packagePath))
|
|
})
|
|
|
|
it.each(['github', 'mirror'] as const)(
|
|
'checks the selected %s catalog without downloading until requested',
|
|
async (source) => {
|
|
const root = join(temporaryRoot, `online-${source}`)
|
|
mkdirSync(root)
|
|
const keyRegistryPath = join(root, 'registry.json')
|
|
writeFileSync(
|
|
keyRegistryPath,
|
|
`${JSON.stringify(registry, null, 2)}\n`
|
|
.replace(/\n/gu, '\r\n')
|
|
)
|
|
const catalog = signedCatalog()
|
|
const requested: string[] = []
|
|
const transport = vi.fn(async (input: string | URL) => {
|
|
const url = input.toString()
|
|
requested.push(url)
|
|
if (url.includes('/releases?per_page=10&page=1')) {
|
|
return jsonResponse([{
|
|
tag_name: `agent-v${agentLock.agentVersion}`,
|
|
draft: false,
|
|
prerelease: false,
|
|
assets: [
|
|
{
|
|
name: 'agent-catalog.json',
|
|
browser_download_url:
|
|
'https://github.com/mesalogo/goodbuddy/releases/download/' +
|
|
`agent-v${agentLock.agentVersion}/agent-catalog.json`
|
|
},
|
|
{
|
|
name: 'agent-catalog.sig',
|
|
browser_download_url:
|
|
'https://github.com/mesalogo/goodbuddy/releases/download/' +
|
|
`agent-v${agentLock.agentVersion}/agent-catalog.sig`
|
|
}
|
|
]
|
|
}])
|
|
}
|
|
if (url.endsWith('/latest.json')) {
|
|
return bytesResponse(mirrorPointer())
|
|
}
|
|
if (url.endsWith('/agent-catalog.json')) {
|
|
return bytesResponse(catalog.bytes)
|
|
}
|
|
if (url.endsWith('/agent-catalog.sig')) {
|
|
return bytesResponse(catalog.signature)
|
|
}
|
|
if (url.endsWith(packageResult.archive)) {
|
|
return bytesResponse(readFileSync(packagePath))
|
|
}
|
|
return new Response(null, { status: 404 })
|
|
})
|
|
const manager = new AgentPackageManager({
|
|
userDataPath: join(root, 'user-data'),
|
|
desktopVersion: '0.11.0',
|
|
keyRegistryPath,
|
|
getUpdateSource: vi.fn(async () => source),
|
|
fetch: transport as typeof fetch
|
|
})
|
|
|
|
await manager.getInventory()
|
|
expect(requested).toEqual([])
|
|
const snapshot = await manager.getSnapshot()
|
|
expect(snapshot.catalog).toMatchObject({
|
|
state: 'available',
|
|
error: null
|
|
})
|
|
expect(
|
|
snapshot.entries.find(
|
|
(entry) => entry.architecture === 'x64'
|
|
)
|
|
).toMatchObject({
|
|
state: 'not-downloaded',
|
|
latestVersion: agentLock.agentVersion,
|
|
updateAvailable: false
|
|
})
|
|
expect(requested).toContain(
|
|
source === 'github'
|
|
? 'https://api.github.com/repos/mesalogo/goodbuddy/releases?per_page=10&page=1'
|
|
: 'https://goodbuddy.oss-cn-beijing.aliyuncs.com/agent-releases/latest.json'
|
|
)
|
|
expect(requested).not.toContain(
|
|
source === 'github'
|
|
? packageEntry().downloads.github.url
|
|
: packageEntry().downloads.mirror.url
|
|
)
|
|
await manager.download('x64')
|
|
const installedSnapshot = await manager.getInventory()
|
|
expect(
|
|
installedSnapshot.entries.find(
|
|
(entry) => entry.architecture === 'x64'
|
|
)
|
|
).toMatchObject({
|
|
state: 'verified',
|
|
version: agentLock.agentVersion,
|
|
latestVersion: agentLock.agentVersion,
|
|
updateAvailable: false
|
|
})
|
|
await manager.download('x64')
|
|
expect(requested).toContain(
|
|
source === 'github'
|
|
? 'https://api.github.com/repos/mesalogo/goodbuddy/releases?per_page=10&page=1'
|
|
: 'https://goodbuddy.oss-cn-beijing.aliyuncs.com/agent-releases/latest.json'
|
|
)
|
|
expect(requested).toContain(
|
|
source === 'github'
|
|
? packageEntry().downloads.github.url
|
|
: packageEntry().downloads.mirror.url
|
|
)
|
|
expect(
|
|
requested.filter(
|
|
(url) =>
|
|
url === (
|
|
source === 'github'
|
|
? packageEntry().downloads.github.url
|
|
: packageEntry().downloads.mirror.url
|
|
)
|
|
)
|
|
).toHaveLength(1)
|
|
}
|
|
)
|
|
|
|
it('rejects a catalog with an invalid signature before package download', async () => {
|
|
const root = join(temporaryRoot, 'invalid-catalog')
|
|
mkdirSync(root)
|
|
const keyRegistryPath = join(root, 'registry.json')
|
|
writeFileSync(
|
|
keyRegistryPath,
|
|
`${JSON.stringify(registry, null, 2)}\n`
|
|
)
|
|
const catalog = signedCatalog()
|
|
let packageRequested = false
|
|
const transport = vi.fn(async (input: string | URL) => {
|
|
const url = input.toString()
|
|
if (url.includes('/releases?per_page=10&page=1')) {
|
|
return jsonResponse([{
|
|
tag_name: `agent-v${agentLock.agentVersion}`,
|
|
draft: false,
|
|
prerelease: false,
|
|
assets: [
|
|
{
|
|
name: 'agent-catalog.json',
|
|
browser_download_url:
|
|
'https://github.com/mesalogo/goodbuddy/releases/download/' +
|
|
`agent-v${agentLock.agentVersion}/agent-catalog.json`
|
|
},
|
|
{
|
|
name: 'agent-catalog.sig',
|
|
browser_download_url:
|
|
'https://github.com/mesalogo/goodbuddy/releases/download/' +
|
|
`agent-v${agentLock.agentVersion}/agent-catalog.sig`
|
|
}
|
|
]
|
|
}])
|
|
}
|
|
if (url.endsWith('/agent-catalog.json')) {
|
|
return bytesResponse(catalog.bytes)
|
|
}
|
|
if (url.endsWith('/agent-catalog.sig')) {
|
|
return bytesResponse(Buffer.from('invalid\n'))
|
|
}
|
|
packageRequested = true
|
|
return bytesResponse(readFileSync(packagePath))
|
|
})
|
|
const manager = new AgentPackageManager({
|
|
userDataPath: join(root, 'user-data'),
|
|
desktopVersion: '0.11.0',
|
|
keyRegistryPath,
|
|
getUpdateSource: vi.fn(async () => 'github' as const),
|
|
fetch: transport as typeof fetch
|
|
})
|
|
|
|
await expect(manager.getSnapshot()).resolves.toMatchObject({
|
|
catalog: {
|
|
state: 'unavailable',
|
|
error: 'Agent 发布目录签名校验失败'
|
|
},
|
|
entries: expect.any(Array)
|
|
})
|
|
expect(packageRequested).toBe(false)
|
|
await expect(manager.download('x64')).rejects.toThrow(
|
|
'Agent 发布目录签名校验失败'
|
|
)
|
|
expect(packageRequested).toBe(false)
|
|
})
|
|
|
|
it('rejects an online catalog rollback before package download', async () => {
|
|
const root = join(temporaryRoot, 'rollback-catalog')
|
|
mkdirSync(root)
|
|
const keyRegistryPath = join(root, 'registry.json')
|
|
writeFileSync(
|
|
keyRegistryPath,
|
|
`${JSON.stringify(registry, null, 2)}\n`
|
|
)
|
|
const rollback = signedCatalog([
|
|
catalogEntry(packageResult.descriptor, {
|
|
version: '0.11.1',
|
|
minimumDesktopVersion: '0.11.0'
|
|
})
|
|
])
|
|
let packageRequested = false
|
|
const transport = vi.fn(async (input: string | URL) => {
|
|
const url = input.toString()
|
|
if (url.includes('/releases?per_page=10&page=1')) {
|
|
return jsonResponse([{
|
|
tag_name: `agent-v${agentLock.agentVersion}`,
|
|
draft: false,
|
|
prerelease: false,
|
|
assets: [
|
|
{
|
|
name: 'agent-catalog.json',
|
|
browser_download_url:
|
|
'https://github.com/mesalogo/goodbuddy/releases/download/' +
|
|
`agent-v${agentLock.agentVersion}/agent-catalog.json`
|
|
},
|
|
{
|
|
name: 'agent-catalog.sig',
|
|
browser_download_url:
|
|
'https://github.com/mesalogo/goodbuddy/releases/download/' +
|
|
`agent-v${agentLock.agentVersion}/agent-catalog.sig`
|
|
}
|
|
]
|
|
}])
|
|
}
|
|
if (url.endsWith('/agent-catalog.json')) {
|
|
return bytesResponse(rollback.bytes)
|
|
}
|
|
if (url.endsWith('/agent-catalog.sig')) {
|
|
return bytesResponse(rollback.signature)
|
|
}
|
|
packageRequested = true
|
|
return bytesResponse(readFileSync(packagePath))
|
|
})
|
|
const manager = new AgentPackageManager({
|
|
userDataPath: join(root, 'user-data'),
|
|
desktopVersion: '0.11.0',
|
|
keyRegistryPath,
|
|
getUpdateSource: vi.fn(async () => 'github' as const),
|
|
fetch: transport as typeof fetch
|
|
})
|
|
await manager.importArchive(packagePath)
|
|
|
|
await expect(manager.download('x64')).rejects.toThrow(
|
|
'Agent 发布目录不能降级'
|
|
)
|
|
expect(packageRequested).toBe(false)
|
|
})
|
|
})
|
|
|
|
function writeAgentBundle(
|
|
directory: string,
|
|
architecture: 'x64' | 'arm64'
|
|
): void {
|
|
mkdirSync(join(directory, 'lib'), { recursive: true })
|
|
mkdirSync(join(directory, 'licenses'), { recursive: true })
|
|
mkdirSync(join(directory, 'helpers'), { recursive: true })
|
|
writeFileSync(join(directory, 'node'), elf(architecture))
|
|
writeFileSync(
|
|
join(directory, 'goodbuddy-agent'),
|
|
'#!/bin/sh\n'
|
|
)
|
|
writeFileSync(
|
|
join(directory, 'lib', 'agent.cjs'),
|
|
'module.exports={}\n'
|
|
)
|
|
writeFileSync(
|
|
join(directory, 'helpers', 'fixture-helper'),
|
|
'#!/bin/sh\n'
|
|
)
|
|
for (const [name, contents] of [
|
|
['GoodBuddy-0BSD.txt', '0BSD'],
|
|
['Node.js-MIT.txt', 'MIT'],
|
|
['zod-MIT.txt', 'MIT'],
|
|
['koffi-MIT.txt', 'MIT'],
|
|
['koffi-native-MIT.txt', 'MIT']
|
|
] as const) {
|
|
writeFileSync(join(directory, 'licenses', name), contents)
|
|
}
|
|
writeKoffiPayload(directory, architecture)
|
|
for (const path of [
|
|
'node',
|
|
'goodbuddy-agent',
|
|
'helpers/fixture-helper'
|
|
]) {
|
|
chmodSync(join(directory, path), 0o755)
|
|
}
|
|
signAgentBundleManifest(directory, architecture)
|
|
}
|
|
|
|
function signAgentBundleManifest(
|
|
directory: string,
|
|
architecture: 'x64' | 'arm64'
|
|
): void {
|
|
const manifest = agentBundle.createManifest(directory, {
|
|
agentVersion: agentLock.agentVersion,
|
|
nodeVersion: agentLock.node.version,
|
|
zodVersion: '4.4.3',
|
|
koffiVersion: agentLock.koffi.version,
|
|
koffiNativePackage:
|
|
`@koromix/koffi-linux-${architecture}`,
|
|
arch: architecture,
|
|
protocol: agentLock.protocol,
|
|
signingKeyId: 'agent-package-fixture'
|
|
})
|
|
const bytes = agentBundle.canonicalManifestBytes(manifest)
|
|
writeFileSync(join(directory, 'manifest.json'), bytes)
|
|
writeFileSync(
|
|
join(directory, 'manifest.sig'),
|
|
`${agentBundle
|
|
.signManifestForTest(bytes, privateKey)
|
|
.toString('base64')}\n`
|
|
)
|
|
chmodSync(join(directory, 'manifest.json'), 0o644)
|
|
chmodSync(join(directory, 'manifest.sig'), 0o644)
|
|
}
|
|
|
|
function writeKoffiPayload(
|
|
directory: string,
|
|
architecture: 'x64' | 'arm64'
|
|
): void {
|
|
const packageRoot = join(
|
|
directory,
|
|
'lib',
|
|
'node_modules',
|
|
'koffi'
|
|
)
|
|
const nativePackage = `@koromix/koffi-linux-${architecture}`
|
|
const nativeRoot = join(
|
|
directory,
|
|
'lib',
|
|
'node_modules',
|
|
...nativePackage.split('/')
|
|
)
|
|
mkdirSync(join(packageRoot, 'src', 'koffi', 'src'), {
|
|
recursive: true
|
|
})
|
|
mkdirSync(join(nativeRoot, `linux_${architecture}`), {
|
|
recursive: true
|
|
})
|
|
mkdirSync(join(nativeRoot, `musl_${architecture}`), {
|
|
recursive: true
|
|
})
|
|
const files = new Map<string, string | Buffer>([
|
|
[
|
|
join(packageRoot, 'package.json'),
|
|
JSON.stringify({
|
|
name: 'koffi',
|
|
version: agentLock.koffi.version,
|
|
type: 'module',
|
|
exports: { '.': { import: './index.js' } }
|
|
})
|
|
],
|
|
[
|
|
join(packageRoot, 'index.js'),
|
|
'export { default } from "./src/koffi/index.js";\n'
|
|
],
|
|
[
|
|
join(packageRoot, 'src', 'koffi', 'index.js'),
|
|
'export default {};\n'
|
|
],
|
|
[
|
|
join(packageRoot, 'src', 'koffi', 'src', 'static.js'),
|
|
'export function loadStatic() { return null }\n'
|
|
],
|
|
[
|
|
join(nativeRoot, 'package.json'),
|
|
JSON.stringify({
|
|
name: nativePackage,
|
|
version: agentLock.koffi.version,
|
|
main: './index.js',
|
|
os: ['linux'],
|
|
cpu: [architecture]
|
|
})
|
|
],
|
|
[
|
|
join(nativeRoot, 'index.js'),
|
|
`module.exports = require('./linux_${architecture}/koffi.node');\n`
|
|
],
|
|
[
|
|
join(nativeRoot, `linux_${architecture}`, 'koffi.node'),
|
|
elf(architecture)
|
|
],
|
|
[
|
|
join(nativeRoot, `musl_${architecture}`, 'koffi.node'),
|
|
elf(architecture)
|
|
]
|
|
])
|
|
for (const [filePath, contents] of files) {
|
|
writeFileSync(filePath, contents)
|
|
chmodSync(filePath, 0o644)
|
|
}
|
|
}
|
|
|
|
function createRuntimeInput(
|
|
architecture: 'x64' | 'arm64'
|
|
): string {
|
|
const sourceRoot = join(
|
|
temporaryRoot,
|
|
`runtime-input-${architecture}`
|
|
)
|
|
const packageRoot = join(sourceRoot, 'package')
|
|
mkdirSync(join(packageRoot, 'bin'), { recursive: true })
|
|
writeFileSync(
|
|
join(packageRoot, 'package.json'),
|
|
`${JSON.stringify({
|
|
name:
|
|
architecture === 'x64'
|
|
? 'opencode-linux-x64-baseline'
|
|
: 'opencode-linux-arm64',
|
|
version: '1.18.29'
|
|
})}\n`
|
|
)
|
|
writeFileSync(
|
|
join(packageRoot, 'bin', 'opencode'),
|
|
Buffer.concat([
|
|
elf(architecture),
|
|
Buffer.from([0x50, 0x4b, 0x07, 0x08]),
|
|
Buffer.from('runtime payload')
|
|
])
|
|
)
|
|
chmodSync(join(packageRoot, 'bin', 'opencode'), 0o755)
|
|
const archive = join(
|
|
temporaryRoot,
|
|
architecture === 'x64'
|
|
? 'opencode-linux-x64-baseline-1.18.29.tgz'
|
|
: 'opencode-linux-arm64-1.18.29.tgz'
|
|
)
|
|
createTar(
|
|
{
|
|
cwd: sourceRoot,
|
|
file: archive,
|
|
sync: true,
|
|
portable: true,
|
|
noPax: true,
|
|
gzip: true,
|
|
mtime: new Date(0)
|
|
},
|
|
['package/package.json', 'package/bin/opencode']
|
|
)
|
|
return archive
|
|
}
|
|
|
|
function createRemoteRuntimeLock(
|
|
runtimeArchive: string,
|
|
arm64RuntimeArchive: string
|
|
): RemoteRuntimeLock {
|
|
return {
|
|
formatVersion: 1,
|
|
runtimes: {
|
|
opencode: {
|
|
version: '1.18.29',
|
|
provider: 'opencode',
|
|
entrypoint: 'bin/opencode',
|
|
entrypointIdentity: 'opencode-acp',
|
|
argvPrefix: ['acp'],
|
|
allowedEnvironmentNames: [
|
|
'HOME',
|
|
'LANG',
|
|
'LC_ALL',
|
|
'PATH',
|
|
'TMPDIR',
|
|
'XDG_CACHE_HOME',
|
|
'XDG_CONFIG_HOME',
|
|
'XDG_DATA_HOME',
|
|
'XDG_STATE_HOME'
|
|
],
|
|
protocol: { major: 1, minor: 0 },
|
|
targets: {
|
|
x64: {
|
|
package: 'opencode-linux-x64-baseline',
|
|
integrity: `sha512-${createHash('sha512')
|
|
.update(readFileSync(runtimeArchive))
|
|
.digest('base64')}`
|
|
},
|
|
arm64: {
|
|
package: 'opencode-linux-arm64',
|
|
integrity: `sha512-${createHash('sha512')
|
|
.update(readFileSync(arm64RuntimeArchive))
|
|
.digest('base64')}`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function elf(architecture: 'x64' | 'arm64'): Buffer {
|
|
const contents = Buffer.alloc(64)
|
|
contents.set([0x7f, 0x45, 0x4c, 0x46])
|
|
contents[5] = 1
|
|
contents.writeUInt16LE(architecture === 'x64' ? 62 : 183, 18)
|
|
return contents
|
|
}
|
|
|
|
function findRuntimeBundleDirectory(): string {
|
|
const digest = packageResult.descriptor.remoteRuntime.bundleDigest
|
|
.slice('sha256:'.length)
|
|
return join(
|
|
temporaryRoot,
|
|
'runtime',
|
|
'linux-x64',
|
|
'opencode',
|
|
digest
|
|
)
|
|
}
|
|
|
|
function packageEntry(): AgentPackageCatalogEntry {
|
|
return catalogEntry(packageResult.descriptor, {
|
|
archive: packageResult.archive,
|
|
size: packageResult.size,
|
|
sha256: packageResult.sha256
|
|
})
|
|
}
|
|
|
|
function catalogEntry(
|
|
descriptor: AgentPackageDescriptor,
|
|
overrides: Partial<AgentPackageCatalogEntry> = {}
|
|
): AgentPackageCatalogEntry {
|
|
const {
|
|
contentDigest: _contentDigest,
|
|
files: _files,
|
|
signingKeyId: _signingKeyId,
|
|
...identity
|
|
} = descriptor
|
|
void _contentDigest
|
|
void _files
|
|
void _signingKeyId
|
|
const version = overrides.version ?? identity.version
|
|
const architecture =
|
|
overrides.architecture ?? identity.architecture
|
|
const archive =
|
|
overrides.archive ??
|
|
agentPackageArchiveName(version, architecture)
|
|
return {
|
|
...identity,
|
|
...overrides,
|
|
version,
|
|
architecture,
|
|
archive,
|
|
size: overrides.size ?? packageResult.size,
|
|
sha256: overrides.sha256 ?? packageResult.sha256,
|
|
downloads: overrides.downloads ?? {
|
|
github: {
|
|
url:
|
|
'https://github.com/mesalogo/goodbuddy/releases/download/' +
|
|
`agent-v${version}/${archive}`
|
|
},
|
|
mirror: {
|
|
url:
|
|
'https://goodbuddy.oss-cn-beijing.aliyuncs.com/' +
|
|
`agent-releases/v${version}/${archive}`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function signedCatalog(
|
|
entries: AgentPackageCatalogEntry[] = [packageEntry()]
|
|
): {
|
|
catalog: AgentPackageCatalog
|
|
bytes: Buffer
|
|
signature: Buffer
|
|
} {
|
|
const catalog = agentPackageCatalogSchema.parse({
|
|
formatVersion: 1,
|
|
product: 'GoodBuddy',
|
|
component: 'agent',
|
|
signingKeyId: 'agent-package-fixture',
|
|
generatedAt: '2026-08-24T00:00:00.000Z',
|
|
entries
|
|
})
|
|
const bytes = Buffer.from(
|
|
`${JSON.stringify(catalog, null, 2)}\n`
|
|
)
|
|
const signature = Buffer.from(
|
|
`${sign(
|
|
null,
|
|
Buffer.concat([catalogSignatureDomain, bytes]),
|
|
privateKey
|
|
).toString('base64')}\n`
|
|
)
|
|
return { catalog, bytes, signature }
|
|
}
|
|
|
|
function bytesResponse(bytes: Buffer): Response {
|
|
return new Response(new Uint8Array(bytes), {
|
|
status: 200,
|
|
headers: {
|
|
'content-length': String(bytes.byteLength)
|
|
}
|
|
})
|
|
}
|
|
|
|
function jsonResponse(value: unknown): Response {
|
|
return bytesResponse(Buffer.from(JSON.stringify(value)))
|
|
}
|
|
|
|
function mirrorPointer(): Buffer {
|
|
return Buffer.from(
|
|
`${JSON.stringify({
|
|
formatVersion: 1,
|
|
version: agentLock.agentVersion,
|
|
catalog:
|
|
`v${agentLock.agentVersion}/agent-catalog.json`,
|
|
signature:
|
|
`v${agentLock.agentVersion}/agent-catalog.sig`
|
|
}, null, 2)}\n`
|
|
)
|
|
}
|
|
|
|
function renameStoredZipEntry(
|
|
archive: Buffer,
|
|
oldName: string,
|
|
newName: string
|
|
): Buffer {
|
|
const oldBytes = Buffer.from(oldName, 'utf8')
|
|
const newBytes = Buffer.from(newName, 'utf8')
|
|
if (oldBytes.length !== newBytes.length) {
|
|
throw new Error('ZIP fixture names must have equal byte lengths')
|
|
}
|
|
const output = Buffer.from(archive)
|
|
let replacements = 0
|
|
for (let offset = 0; offset <= output.length - 46; offset += 1) {
|
|
const signature = output.readUInt32LE(offset)
|
|
const nameOffset =
|
|
signature === 0x04034b50
|
|
? offset + 30
|
|
: signature === 0x02014b50
|
|
? offset + 46
|
|
: -1
|
|
const lengthOffset =
|
|
signature === 0x04034b50
|
|
? offset + 26
|
|
: signature === 0x02014b50
|
|
? offset + 28
|
|
: -1
|
|
if (
|
|
nameOffset >= 0 &&
|
|
output.readUInt16LE(lengthOffset) === oldBytes.length &&
|
|
output.subarray(nameOffset, nameOffset + oldBytes.length)
|
|
.equals(oldBytes)
|
|
) {
|
|
newBytes.copy(output, nameOffset)
|
|
replacements += 1
|
|
}
|
|
}
|
|
if (replacements !== 2) {
|
|
throw new Error(
|
|
`Expected local and central ZIP names, received ${replacements}`
|
|
)
|
|
}
|
|
return output
|
|
}
|
|
|
|
function corruptStoredZipEntry(
|
|
archive: Buffer,
|
|
name: string
|
|
): Buffer {
|
|
const output = Buffer.from(archive)
|
|
const nameBytes = Buffer.from(name, 'utf8')
|
|
for (let offset = 0; offset <= output.length - 30; offset += 1) {
|
|
if (output.readUInt32LE(offset) !== 0x04034b50) {
|
|
continue
|
|
}
|
|
const nameLength = output.readUInt16LE(offset + 26)
|
|
const extraLength = output.readUInt16LE(offset + 28)
|
|
if (
|
|
nameLength === nameBytes.length &&
|
|
output.subarray(offset + 30, offset + 30 + nameLength)
|
|
.equals(nameBytes)
|
|
) {
|
|
const dataOffset = offset + 30 + nameLength + extraLength
|
|
output[dataOffset] = (output[dataOffset] ?? 0) ^ 1
|
|
return output
|
|
}
|
|
}
|
|
throw new Error(`ZIP fixture entry was not found: ${name}`)
|
|
}
|
|
|
|
function seedGlobalManagedMetadata(
|
|
homeDirectory: string,
|
|
digestCharacter: string
|
|
): Map<string, Buffer> {
|
|
const managedRoot = join(homeDirectory, '.goodbuddy')
|
|
const files = new Map<string, Buffer>([
|
|
[
|
|
join(managedRoot, 'agent', 'release-keys.json'),
|
|
Buffer.from(`${JSON.stringify(registry, null, 4)}\r\n`)
|
|
],
|
|
[
|
|
join(managedRoot, 'agent', 'registry.json'),
|
|
Buffer.from(
|
|
`${JSON.stringify({
|
|
formatVersion: 1,
|
|
current: {
|
|
installationId:
|
|
`agent-${digestCharacter.repeat(64)}`,
|
|
agentVersion: '0.10.0',
|
|
manifestSha256: digestCharacter.repeat(64),
|
|
arch: 'x64'
|
|
}
|
|
}, null, 4)}\r\n`
|
|
)
|
|
],
|
|
[
|
|
join(managedRoot, 'runtimes', 'release-keys.json'),
|
|
Buffer.from(`${JSON.stringify(registry, null, 3)}\r\n`)
|
|
],
|
|
[
|
|
join(
|
|
managedRoot,
|
|
'runtimes',
|
|
'remote-runtime-lock.json'
|
|
),
|
|
Buffer.from(`${JSON.stringify(remoteRuntimeLock, null, 3)}\r\n`)
|
|
],
|
|
[
|
|
join(managedRoot, 'runtimes', 'registry.json'),
|
|
Buffer.from(
|
|
`${JSON.stringify({
|
|
formatVersion: 1,
|
|
current: [{
|
|
runtimeId: 'opencode',
|
|
runtimeVersion: '1.0.0',
|
|
architecture: 'x64',
|
|
bundleDigest:
|
|
`sha256:${digestCharacter.repeat(64)}`,
|
|
manifestDigest: `sha256:${'1'.repeat(64)}`,
|
|
acpCapabilitiesDigest: `sha256:${'2'.repeat(64)}`
|
|
}]
|
|
}, null, 4)}\r\n`
|
|
)
|
|
]
|
|
])
|
|
for (const [path, bytes] of files) {
|
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 })
|
|
writeFileSync(path, bytes, { mode: 0o600 })
|
|
}
|
|
return files
|
|
}
|
|
|
|
function expectGlobalMetadataUnchanged(
|
|
files: ReadonlyMap<string, Buffer>
|
|
): void {
|
|
for (const [path, bytes] of files) {
|
|
expect(readFileSync(path)).toEqual(bytes)
|
|
}
|
|
}
|
|
|
|
function expectRegistriesUnchangedAndPackageTrustPublished(
|
|
files: ReadonlyMap<string, Buffer>
|
|
): void {
|
|
for (const [path, bytes] of files) {
|
|
if (path.endsWith('registry.json')) {
|
|
expect(readFileSync(path)).toEqual(bytes)
|
|
continue
|
|
}
|
|
const actual = JSON.parse(readFileSync(path, 'utf8'))
|
|
expect(actual).toEqual(
|
|
path.endsWith('remote-runtime-lock.json')
|
|
? remoteRuntimeLock
|
|
: registry
|
|
)
|
|
}
|
|
}
|