Extend GoodBuddy Agent packaging, installation, SSH attachment, Runtime lifecycle, and Desktop package management to macOS arm64. Linux x64, Linux arm64, and Darwin arm64 share the existing signed catalog. Preserve Ask read-only behavior and Execute account permissions, and stop detached tool descendants during Mac cancellation. Upgrade Desktop before publishing the first mixed-platform catalog because older Linux-only readers reject Darwin entries. Intel Macs are not supported; no launchd service or administrator access is required. This commit does not publish a release. Validation: 3520 tests passed, 57 skipped; typecheck, lint, and development build passed. Real Mac installation, Ask/Execute, cancellation and reconnect passed; final Linux installation, Ask and reconnect passed. Nine real model requests were recorded. Release note: 支持将 Apple Silicon Mac 用作远程项目 Host,可安装 macOS arm64 Agent 并使用 Ask、Execute 和断线重连;与 Linux 共用一个包目录,发布混合目录前需升级 Desktop。
1249 lines
35 KiB
JavaScript
1249 lines
35 KiB
JavaScript
const {
|
|
createHash,
|
|
createPublicKey,
|
|
sign,
|
|
verify
|
|
} = require('node:crypto')
|
|
const {
|
|
chmodSync,
|
|
copyFileSync,
|
|
existsSync,
|
|
lstatSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
renameSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync
|
|
} = require('node:fs')
|
|
const { tmpdir } = require('node:os')
|
|
const {
|
|
basename,
|
|
dirname,
|
|
join,
|
|
posix,
|
|
relative,
|
|
resolve,
|
|
sep
|
|
} = require('node:path')
|
|
const tar = require('tar')
|
|
const { detectElfArchitecture } = require('./binary-architecture.cjs')
|
|
const {
|
|
targetName,
|
|
assertTargetBinary
|
|
} = require('./agent-build-target.cjs')
|
|
const {
|
|
preflightRegisteredProductionKey
|
|
} = require('./signing-key-preflight.cjs')
|
|
|
|
const root = join(__dirname, '..')
|
|
const domainSeparator = Buffer.from(
|
|
'GoodBuddy Agent Bundle Manifest Signature v1\0',
|
|
'utf8'
|
|
)
|
|
const supportedArchitectures = Object.freeze(['x64', 'arm64'])
|
|
const allowedModes = new Set(['0644', '0755'])
|
|
const manifestFileName = 'manifest.json'
|
|
const signatureFileName = 'manifest.sig'
|
|
const koffiPackageName = 'koffi'
|
|
const signingPrivateKeyEnvironment =
|
|
'GOODBUDDY_SIGNING_PRIVATE_KEY'
|
|
const signingKeyIdEnvironment =
|
|
'GOODBUDDY_SIGNING_KEY_ID'
|
|
const semanticVersionPattern =
|
|
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+((?:[0-9a-zA-Z-]+)(?:\.[0-9a-zA-Z-]+)*))?$/u
|
|
|
|
function sha256Bytes(contents) {
|
|
return createHash('sha256').update(contents).digest('hex')
|
|
}
|
|
|
|
function sha256FileSync(filePath) {
|
|
return sha256Bytes(readFileSync(filePath))
|
|
}
|
|
|
|
function readJson(filePath, description) {
|
|
let value
|
|
try {
|
|
value = JSON.parse(readFileSync(filePath, 'utf8'))
|
|
} catch (error) {
|
|
throw new Error(
|
|
`${description} is missing or invalid: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`,
|
|
{ cause: error }
|
|
)
|
|
}
|
|
return value
|
|
}
|
|
|
|
function readRuntimeLock(projectRoot = root) {
|
|
const lock = readJson(
|
|
join(projectRoot, 'agent-runtime-lock.json'),
|
|
'Agent runtime lock'
|
|
)
|
|
if (
|
|
lock.formatVersion !== 1 ||
|
|
typeof lock.agentVersion !== 'string' ||
|
|
!semanticVersionPattern.test(lock.agentVersion) ||
|
|
lock.node?.version !== '24.19.0' ||
|
|
lock.koffi?.version !== '3.1.4' ||
|
|
!Number.isSafeInteger(lock.protocol?.major) ||
|
|
!Number.isSafeInteger(lock.protocol?.minor)
|
|
) {
|
|
throw new Error('Agent runtime lock contract is invalid')
|
|
}
|
|
return lock
|
|
}
|
|
|
|
function readTrustedKeyRegistry(projectRoot = root) {
|
|
const registry = readJson(
|
|
join(projectRoot, 'resources', 'agent-release-keys.json'),
|
|
'Agent trusted key registry'
|
|
)
|
|
if (
|
|
registry.formatVersion !== 1 ||
|
|
!Array.isArray(registry.keys) ||
|
|
!Array.isArray(registry.revocations)
|
|
) {
|
|
throw new Error('Agent trusted key registry contract is invalid')
|
|
}
|
|
const ids = new Set()
|
|
for (const key of registry.keys) {
|
|
if (
|
|
typeof key.keyId !== 'string' ||
|
|
!/^[a-z0-9][a-z0-9._-]{0,63}$/u.test(key.keyId) ||
|
|
ids.has(key.keyId) ||
|
|
typeof key.publicKeySpkiBase64 !== 'string' ||
|
|
!['production', 'test'].includes(key.environment)
|
|
) {
|
|
throw new Error('Agent trusted key entry is invalid')
|
|
}
|
|
ids.add(key.keyId)
|
|
try {
|
|
createPublicKey({
|
|
key: Buffer.from(key.publicKeySpkiBase64, 'base64'),
|
|
format: 'der',
|
|
type: 'spki'
|
|
})
|
|
} catch {
|
|
throw new Error(`Agent trusted public key is invalid: ${key.keyId}`)
|
|
}
|
|
}
|
|
return registry
|
|
}
|
|
|
|
function lockedRuntimeInput(lock, arch, archivePath, platform = 'linux') {
|
|
const target = targetName(arch, platform)
|
|
const input = lock.node?.targets?.[target]
|
|
if (
|
|
!input ||
|
|
typeof input.archive !== 'string' ||
|
|
typeof input.sha256 !== 'string' ||
|
|
!/^[a-f0-9]{64}$/u.test(input.sha256) ||
|
|
typeof input.binaryPath !== 'string' ||
|
|
typeof input.licensePath !== 'string'
|
|
) {
|
|
throw new Error(`Locked Node runtime input is missing for ${target}`)
|
|
}
|
|
if (!archivePath || !existsSync(archivePath)) {
|
|
throw new Error(
|
|
`Verified official Node ${lock.node.version} runtime input is required for ${target}: ${input.archive}`
|
|
)
|
|
}
|
|
if (!statSync(archivePath).isFile()) {
|
|
throw new Error(`Node runtime input is not a file: ${archivePath}`)
|
|
}
|
|
if (basename(archivePath) !== input.archive) {
|
|
throw new Error(
|
|
`Node runtime input filename mismatch: expected ${input.archive}`
|
|
)
|
|
}
|
|
const actualDigest = sha256FileSync(archivePath)
|
|
if (actualDigest !== input.sha256) {
|
|
throw new Error(
|
|
`Node runtime input digest mismatch for ${target}: expected ${input.sha256}, received ${actualDigest}`
|
|
)
|
|
}
|
|
return { ...input, target }
|
|
}
|
|
|
|
function assertElfArchitecture(
|
|
filePath,
|
|
expectedArchitecture,
|
|
description = 'Agent Node runtime',
|
|
platform = 'linux'
|
|
) {
|
|
assertTargetBinary(filePath, expectedArchitecture, platform, description)
|
|
}
|
|
|
|
function koffiNativePackageName(arch, platform = 'linux') {
|
|
return `@koromix/koffi-${targetName(arch, platform)}`
|
|
}
|
|
|
|
function koffiPayloadPaths(arch, platform = 'linux') {
|
|
const packageRoot = `lib/node_modules/${koffiPackageName}`
|
|
const nativeRoot =
|
|
`lib/node_modules/${koffiNativePackageName(arch, platform)}`
|
|
const native = platform === 'linux'
|
|
? [`${nativeRoot}/linux_${arch}/koffi.node`, `${nativeRoot}/musl_${arch}/koffi.node`]
|
|
: [`${nativeRoot}/darwin_${arch}/koffi.node`]
|
|
return {
|
|
packageRoot,
|
|
nativeRoot,
|
|
required: [
|
|
`${packageRoot}/package.json`,
|
|
`${packageRoot}/index.js`,
|
|
`${packageRoot}/src/koffi/index.js`,
|
|
`${packageRoot}/src/koffi/src/static.js`,
|
|
`${nativeRoot}/package.json`,
|
|
`${nativeRoot}/index.js`,
|
|
...native
|
|
],
|
|
native
|
|
}
|
|
}
|
|
|
|
function assertSafeManifestPath(filePath) {
|
|
if (
|
|
typeof filePath !== 'string' ||
|
|
filePath.length === 0 ||
|
|
filePath.length > 240 ||
|
|
filePath.includes('\\') ||
|
|
filePath.includes('\0') ||
|
|
posix.isAbsolute(filePath) ||
|
|
filePath.split('/').some(
|
|
(part) => part.length === 0 || part === '.' || part === '..'
|
|
)
|
|
) {
|
|
throw new Error(`Unsafe Agent manifest path: ${String(filePath)}`)
|
|
}
|
|
}
|
|
|
|
function compareUtf8(left, right) {
|
|
return Buffer.compare(
|
|
Buffer.from(left, 'utf8'),
|
|
Buffer.from(right, 'utf8')
|
|
)
|
|
}
|
|
|
|
function listFiles(directory) {
|
|
const files = []
|
|
const pending = [directory]
|
|
while (pending.length > 0) {
|
|
const current = pending.pop()
|
|
const entries = readdirSync(current, { withFileTypes: true })
|
|
.sort((left, right) => compareUtf8(right.name, left.name))
|
|
for (const entry of entries) {
|
|
const absolutePath = join(current, entry.name)
|
|
if (entry.isSymbolicLink()) {
|
|
throw new Error(`Agent bundle cannot contain a symlink: ${absolutePath}`)
|
|
}
|
|
if (entry.isDirectory()) {
|
|
pending.push(absolutePath)
|
|
} else if (entry.isFile()) {
|
|
files.push(absolutePath)
|
|
} else {
|
|
throw new Error(
|
|
`Agent bundle contains an unsupported file: ${absolutePath}`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
return files.sort(compareUtf8)
|
|
}
|
|
|
|
function posixRelative(directory, filePath) {
|
|
return relative(directory, filePath).split(sep).join('/')
|
|
}
|
|
|
|
function modeString(filePath) {
|
|
return (statSync(filePath).mode & 0o777).toString(8).padStart(4, '0')
|
|
}
|
|
|
|
function expectedManifestMode(path) {
|
|
return path === 'node' ||
|
|
path === 'goodbuddy-agent' ||
|
|
path.startsWith('helpers/')
|
|
? '0755'
|
|
: '0644'
|
|
}
|
|
|
|
function canonicalManifestBytes(manifest) {
|
|
return Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
|
|
}
|
|
|
|
function createManifest(bundleDirectory, metadata) {
|
|
const files = listFiles(bundleDirectory)
|
|
.filter((filePath) => {
|
|
const name = posixRelative(bundleDirectory, filePath)
|
|
return name !== manifestFileName && name !== signatureFileName
|
|
})
|
|
.map((filePath) => {
|
|
const path = posixRelative(bundleDirectory, filePath)
|
|
assertSafeManifestPath(path)
|
|
const mode = expectedManifestMode(path)
|
|
if (
|
|
process.platform !== 'win32' &&
|
|
modeString(filePath) !== mode
|
|
) {
|
|
throw new Error(
|
|
`Agent bundle mode must be ${mode}: ${path}`
|
|
)
|
|
}
|
|
return {
|
|
path,
|
|
size: statSync(filePath).size,
|
|
sha256: sha256FileSync(filePath),
|
|
mode
|
|
}
|
|
})
|
|
const licenses = [
|
|
{
|
|
package: 'GoodBuddy Agent',
|
|
version: metadata.agentVersion,
|
|
spdx: '0BSD',
|
|
path: 'licenses/GoodBuddy-0BSD.txt'
|
|
},
|
|
{
|
|
package: 'Node.js',
|
|
version: metadata.nodeVersion,
|
|
spdx: 'MIT',
|
|
path: 'licenses/Node.js-MIT.txt'
|
|
},
|
|
{
|
|
package: 'zod',
|
|
version: metadata.zodVersion,
|
|
spdx: 'MIT',
|
|
path: 'licenses/zod-MIT.txt'
|
|
},
|
|
...(files.some(
|
|
(file) =>
|
|
file.path ===
|
|
'licenses/agent-client-protocol-Apache-2.0.txt'
|
|
)
|
|
? [
|
|
{
|
|
package: '@agentclientprotocol/sdk',
|
|
version: metadata.acpSdkVersion,
|
|
spdx: 'Apache-2.0',
|
|
path: 'licenses/agent-client-protocol-Apache-2.0.txt'
|
|
}
|
|
]
|
|
: []),
|
|
{
|
|
package: koffiPackageName,
|
|
version: metadata.koffiVersion,
|
|
spdx: 'MIT',
|
|
path: 'licenses/koffi-MIT.txt'
|
|
},
|
|
{
|
|
package: metadata.koffiNativePackage,
|
|
version: metadata.koffiVersion,
|
|
spdx: 'MIT',
|
|
path: 'licenses/koffi-native-MIT.txt'
|
|
}
|
|
]
|
|
return {
|
|
formatVersion: 1,
|
|
product: 'GoodBuddy',
|
|
agentVersion: metadata.agentVersion,
|
|
platform: metadata.platform ?? 'linux',
|
|
arch: metadata.arch,
|
|
protocol: metadata.protocol,
|
|
signingKeyId: metadata.signingKeyId,
|
|
entrypoint: {
|
|
path: 'goodbuddy-agent',
|
|
runtimePath: 'node',
|
|
scriptPath: 'lib/agent.cjs'
|
|
},
|
|
files,
|
|
licenses
|
|
}
|
|
}
|
|
|
|
function signaturePayload(manifestBytes) {
|
|
return Buffer.concat([domainSeparator, manifestBytes])
|
|
}
|
|
|
|
function signManifestForTest(manifestBytes, privateKey) {
|
|
return sign(null, signaturePayload(manifestBytes), privateKey)
|
|
}
|
|
|
|
function productionSigningKey(environment = process.env) {
|
|
const keyId = environment[signingKeyIdEnvironment]
|
|
const privateKey = environment[signingPrivateKeyEnvironment]
|
|
if (!keyId || !privateKey) {
|
|
throw new Error(
|
|
`${signingKeyIdEnvironment} and ${signingPrivateKeyEnvironment} are required for production GoodBuddy signing`
|
|
)
|
|
}
|
|
return { keyId, privateKey }
|
|
}
|
|
|
|
function preflightProductionSigningKey(options = {}) {
|
|
const projectRoot = options.projectRoot ?? root
|
|
const environment = options.environment ?? process.env
|
|
const keyId = options.keyId ?? environment[signingKeyIdEnvironment]
|
|
const registry =
|
|
options.registry ?? readTrustedKeyRegistry(projectRoot)
|
|
return preflightRegisteredProductionKey({
|
|
component: 'GoodBuddy',
|
|
keyId,
|
|
registry,
|
|
missingKeyIdMessage:
|
|
`${signingKeyIdEnvironment} is not configured; provision the production GoodBuddy signing key ID before building a release`
|
|
})
|
|
}
|
|
|
|
function publicKeySpkiBase64(key) {
|
|
const publicKey = key?.type === 'public'
|
|
? key
|
|
: createPublicKey(key)
|
|
return publicKey
|
|
.export({ format: 'der', type: 'spki' })
|
|
.toString('base64')
|
|
}
|
|
|
|
function trustedKeyForManifest(
|
|
manifest,
|
|
registry,
|
|
verificationEnvironment = 'production'
|
|
) {
|
|
const key = registry.keys.find(
|
|
(candidate) => candidate.keyId === manifest.signingKeyId
|
|
)
|
|
if (!key) {
|
|
throw new Error(
|
|
`Agent manifest uses an unknown signing key: ${manifest.signingKeyId}`
|
|
)
|
|
}
|
|
if (
|
|
verificationEnvironment === 'production' &&
|
|
key.environment !== 'production'
|
|
) {
|
|
throw new Error(
|
|
`Production Agent verification rejects non-production key: ${key.keyId}`
|
|
)
|
|
}
|
|
const revocation = registry.revocations.find(
|
|
(candidate) => candidate.keyId === key.keyId
|
|
)
|
|
if (revocation) {
|
|
throw new Error(`GoodBuddy signing key is revoked: ${key.keyId}`)
|
|
}
|
|
return key
|
|
}
|
|
|
|
function validateManifestShape(manifest, expected) {
|
|
if (
|
|
manifest?.formatVersion !== 1 ||
|
|
manifest.product !== 'GoodBuddy' ||
|
|
typeof manifest.agentVersion !== 'string' ||
|
|
!semanticVersionPattern.test(manifest.agentVersion) ||
|
|
!(
|
|
manifest.platform === 'linux' ||
|
|
(manifest.platform === 'darwin' && manifest.arch === 'arm64')
|
|
) ||
|
|
!supportedArchitectures.includes(manifest.arch) ||
|
|
!Number.isSafeInteger(manifest.protocol?.major) ||
|
|
!Number.isSafeInteger(manifest.protocol?.minor) ||
|
|
typeof manifest.signingKeyId !== 'string' ||
|
|
!Array.isArray(manifest.files) ||
|
|
!Array.isArray(manifest.licenses)
|
|
) {
|
|
throw new Error('Agent manifest contract is invalid')
|
|
}
|
|
if (
|
|
manifest.entrypoint?.path !== 'goodbuddy-agent' ||
|
|
manifest.entrypoint?.runtimePath !== 'node' ||
|
|
manifest.entrypoint?.scriptPath !== 'lib/agent.cjs'
|
|
) {
|
|
throw new Error('Agent manifest entrypoint is invalid')
|
|
}
|
|
if (expected) {
|
|
for (const [field, value] of Object.entries(expected)) {
|
|
if (field === 'protocol') {
|
|
if (
|
|
manifest.protocol.major !== value.major ||
|
|
manifest.protocol.minor !== value.minor
|
|
) {
|
|
throw new Error('Agent manifest protocol does not match the lock')
|
|
}
|
|
} else if (manifest[field] !== value) {
|
|
throw new Error(
|
|
`Agent manifest ${field} does not match the locked value`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function verifyManifestSignature(
|
|
manifestBytes,
|
|
signatureBytes,
|
|
registry,
|
|
options = {}
|
|
) {
|
|
let manifest
|
|
try {
|
|
manifest = JSON.parse(manifestBytes.toString('utf8'))
|
|
} catch {
|
|
throw new Error('Agent manifest JSON is invalid')
|
|
}
|
|
validateManifestShape(manifest, options.expected)
|
|
if (!canonicalManifestBytes(manifest).equals(manifestBytes)) {
|
|
throw new Error('Agent manifest is not in canonical deterministic form')
|
|
}
|
|
const key = trustedKeyForManifest(
|
|
manifest,
|
|
registry,
|
|
options.verificationEnvironment
|
|
)
|
|
const publicKey = createPublicKey({
|
|
key: Buffer.from(key.publicKeySpkiBase64, 'base64'),
|
|
format: 'der',
|
|
type: 'spki'
|
|
})
|
|
if (
|
|
signatureBytes.length !== 64 ||
|
|
!verify(
|
|
null,
|
|
signaturePayload(manifestBytes),
|
|
publicKey,
|
|
signatureBytes
|
|
)
|
|
) {
|
|
throw new Error('Agent manifest signature verification failed')
|
|
}
|
|
return manifest
|
|
}
|
|
|
|
function verifyBundleDirectory(bundleDirectory, options = {}) {
|
|
const manifestPath = join(bundleDirectory, manifestFileName)
|
|
const signaturePath = join(bundleDirectory, signatureFileName)
|
|
const manifestBytes = readFileSync(manifestPath)
|
|
const signatureText = readFileSync(signaturePath, 'utf8')
|
|
if (!/^[A-Za-z0-9+/]{86}==\n$/u.test(signatureText)) {
|
|
throw new Error('Agent detached signature encoding is invalid')
|
|
}
|
|
const registry =
|
|
options.registry ?? readTrustedKeyRegistry(options.projectRoot)
|
|
const manifest = verifyManifestSignature(
|
|
manifestBytes,
|
|
Buffer.from(signatureText.trim(), 'base64'),
|
|
registry,
|
|
options
|
|
)
|
|
const declaredPaths = new Set()
|
|
for (const file of manifest.files) {
|
|
assertSafeManifestPath(file.path)
|
|
if (
|
|
declaredPaths.has(file.path) ||
|
|
!Number.isSafeInteger(file.size) ||
|
|
file.size < 0 ||
|
|
!/^[a-f0-9]{64}$/u.test(file.sha256) ||
|
|
!allowedModes.has(file.mode)
|
|
) {
|
|
throw new Error(`Agent manifest file entry is invalid: ${file.path}`)
|
|
}
|
|
if (file.mode !== expectedManifestMode(file.path)) {
|
|
throw new Error(`Agent payload mode mismatch: ${file.path}`)
|
|
}
|
|
declaredPaths.add(file.path)
|
|
const filePath = join(bundleDirectory, ...file.path.split('/'))
|
|
const entryStat = lstatSync(filePath)
|
|
if (!entryStat.isFile() || entryStat.isSymbolicLink()) {
|
|
throw new Error(`Agent payload is not a regular file: ${file.path}`)
|
|
}
|
|
if (entryStat.size !== file.size) {
|
|
throw new Error(`Agent payload size mismatch: ${file.path}`)
|
|
}
|
|
if (sha256FileSync(filePath) !== file.sha256) {
|
|
throw new Error(`Agent payload hash mismatch: ${file.path}`)
|
|
}
|
|
if (
|
|
options.enforceFilesystemMode !== false &&
|
|
process.platform !== 'win32' &&
|
|
modeString(filePath) !== file.mode
|
|
) {
|
|
throw new Error(`Agent payload mode mismatch: ${file.path}`)
|
|
}
|
|
}
|
|
const actualPaths = new Set(
|
|
listFiles(bundleDirectory).map((filePath) =>
|
|
posixRelative(bundleDirectory, filePath)
|
|
)
|
|
)
|
|
const expectedPaths = new Set([
|
|
...declaredPaths,
|
|
manifestFileName,
|
|
signatureFileName
|
|
])
|
|
if (
|
|
actualPaths.size !== expectedPaths.size ||
|
|
[...actualPaths].some((filePath) => !expectedPaths.has(filePath))
|
|
) {
|
|
throw new Error('Agent bundle contains undeclared or missing files')
|
|
}
|
|
const licensedPaths = new Set()
|
|
for (const license of manifest.licenses) {
|
|
assertSafeManifestPath(license.path)
|
|
if (
|
|
typeof license.package !== 'string' ||
|
|
typeof license.version !== 'string' ||
|
|
typeof license.spdx !== 'string' ||
|
|
licensedPaths.has(license.path) ||
|
|
!declaredPaths.has(license.path)
|
|
) {
|
|
throw new Error('Agent manifest license entry is invalid')
|
|
}
|
|
licensedPaths.add(license.path)
|
|
}
|
|
for (const required of [
|
|
manifest.entrypoint.path,
|
|
manifest.entrypoint.runtimePath,
|
|
manifest.entrypoint.scriptPath
|
|
]) {
|
|
if (!declaredPaths.has(required)) {
|
|
throw new Error(`Agent entrypoint payload is missing: ${required}`)
|
|
}
|
|
}
|
|
const koffiPaths = koffiPayloadPaths(manifest.arch, manifest.platform)
|
|
for (const required of koffiPaths.required) {
|
|
if (!declaredPaths.has(required)) {
|
|
throw new Error(`Agent Koffi payload is missing: ${required}`)
|
|
}
|
|
}
|
|
const koffiLicense = manifest.licenses.find(
|
|
(license) => license.package === koffiPackageName
|
|
)
|
|
const nativePackage = koffiNativePackageName(manifest.arch, manifest.platform)
|
|
const nativeLicense = manifest.licenses.find(
|
|
(license) => license.package === nativePackage
|
|
)
|
|
if (
|
|
koffiLicense?.spdx !== 'MIT' ||
|
|
koffiLicense.path !== 'licenses/koffi-MIT.txt' ||
|
|
nativeLicense?.spdx !== 'MIT' ||
|
|
nativeLicense.path !== 'licenses/koffi-native-MIT.txt' ||
|
|
nativeLicense.version !== koffiLicense.version
|
|
) {
|
|
throw new Error('Agent Koffi license declaration is invalid')
|
|
}
|
|
assertElfArchitecture(
|
|
join(bundleDirectory, manifest.entrypoint.runtimePath),
|
|
manifest.arch,
|
|
'Agent Node runtime',
|
|
manifest.platform
|
|
)
|
|
for (const nativePath of koffiPaths.native) {
|
|
assertElfArchitecture(
|
|
join(bundleDirectory, ...nativePath.split('/')),
|
|
manifest.arch,
|
|
'Agent Koffi native binding',
|
|
manifest.platform
|
|
)
|
|
}
|
|
return {
|
|
manifest,
|
|
manifestSha256: sha256Bytes(manifestBytes)
|
|
}
|
|
}
|
|
|
|
function assertReplaceableBundle(directory) {
|
|
if (!existsSync(directory) || readdirSync(directory).length === 0) {
|
|
return
|
|
}
|
|
try {
|
|
const manifest = readJson(
|
|
join(directory, manifestFileName),
|
|
'Existing Agent manifest'
|
|
)
|
|
if (
|
|
manifest.formatVersion === 1 &&
|
|
manifest.product === 'GoodBuddy' &&
|
|
(manifest.platform === 'linux' ||
|
|
(manifest.platform === 'darwin' && manifest.arch === 'arm64'))
|
|
) {
|
|
return
|
|
}
|
|
} catch {
|
|
// Use the explicit refusal below.
|
|
}
|
|
throw new Error(`Refusing to replace unrecognized Agent directory: ${directory}`)
|
|
}
|
|
|
|
function replaceBundle(staging, destination) {
|
|
assertReplaceableBundle(destination)
|
|
const backup = `${destination}.previous-${process.pid}`
|
|
rmSync(backup, { recursive: true, force: true })
|
|
if (existsSync(destination)) {
|
|
renameSync(destination, backup)
|
|
}
|
|
try {
|
|
renameSync(staging, destination)
|
|
} catch (error) {
|
|
if (!existsSync(destination) && existsSync(backup)) {
|
|
renameSync(backup, destination)
|
|
}
|
|
throw error
|
|
}
|
|
rmSync(backup, { recursive: true, force: true })
|
|
}
|
|
|
|
function copyLicense(source, destination) {
|
|
if (!existsSync(source) || !statSync(source).isFile()) {
|
|
throw new Error(`Required Agent bundle license is missing: ${source}`)
|
|
}
|
|
copyFileSync(source, destination)
|
|
chmodSync(destination, 0o644)
|
|
}
|
|
|
|
function copyRegularFile(source, destination) {
|
|
if (
|
|
!existsSync(source) ||
|
|
!lstatSync(source).isFile() ||
|
|
lstatSync(source).isSymbolicLink()
|
|
) {
|
|
throw new Error(`Required Agent dependency file is missing: ${source}`)
|
|
}
|
|
mkdirSync(dirname(destination), { recursive: true })
|
|
copyFileSync(source, destination)
|
|
chmodSync(destination, 0o644)
|
|
}
|
|
|
|
function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion, platform = 'linux') {
|
|
const sourceRoot = join(projectRoot, 'node_modules', koffiPackageName)
|
|
const packageMetadata = readJson(
|
|
join(sourceRoot, 'package.json'),
|
|
'Koffi package metadata'
|
|
)
|
|
const nativePackage = koffiNativePackageName(arch, platform)
|
|
const nativeSourceRoot = join(
|
|
projectRoot,
|
|
'node_modules',
|
|
...nativePackage.split('/')
|
|
)
|
|
const nativeMetadata = readJson(
|
|
join(nativeSourceRoot, 'package.json'),
|
|
'Koffi native package metadata'
|
|
)
|
|
if (
|
|
packageMetadata.name !== koffiPackageName ||
|
|
packageMetadata.version !== lockedVersion ||
|
|
nativeMetadata.name !== nativePackage ||
|
|
nativeMetadata.version !== lockedVersion ||
|
|
!Array.isArray(nativeMetadata.os) ||
|
|
!nativeMetadata.os.includes(platform) ||
|
|
!Array.isArray(nativeMetadata.cpu) ||
|
|
!nativeMetadata.cpu.includes(arch)
|
|
) {
|
|
throw new Error(
|
|
`Koffi dependencies do not match the locked ${nativePackage}@${lockedVersion}`
|
|
)
|
|
}
|
|
|
|
const paths = koffiPayloadPaths(arch, platform)
|
|
const files = [
|
|
['package.json', `${paths.packageRoot}/package.json`],
|
|
['index.js', `${paths.packageRoot}/index.js`],
|
|
['index.cjs', `${paths.packageRoot}/index.cjs`],
|
|
['src/koffi/index.cjs', `${paths.packageRoot}/src/koffi/index.cjs`],
|
|
['src/koffi/src/static.cjs', `${paths.packageRoot}/src/koffi/src/static.cjs`],
|
|
[
|
|
'src/koffi/index.js',
|
|
`${paths.packageRoot}/src/koffi/index.js`
|
|
],
|
|
[
|
|
'src/koffi/src/static.js',
|
|
`${paths.packageRoot}/src/koffi/src/static.js`
|
|
]
|
|
]
|
|
for (const [source, destination] of files) {
|
|
copyRegularFile(
|
|
join(sourceRoot, ...source.split('/')),
|
|
join(staging, ...destination.split('/'))
|
|
)
|
|
}
|
|
for (const source of [
|
|
'package.json',
|
|
'index.js',
|
|
...paths.native.map((path) => path.slice(paths.nativeRoot.length + 1))
|
|
]) {
|
|
const destination = `${paths.nativeRoot}/${source}`
|
|
copyRegularFile(
|
|
join(nativeSourceRoot, ...source.split('/')),
|
|
join(staging, ...destination.split('/'))
|
|
)
|
|
}
|
|
for (const nativePath of paths.native) {
|
|
assertElfArchitecture(
|
|
join(staging, ...nativePath.split('/')),
|
|
arch,
|
|
'Agent Koffi native binding',
|
|
platform
|
|
)
|
|
}
|
|
return {
|
|
version: lockedVersion,
|
|
nativePackage
|
|
}
|
|
}
|
|
|
|
function buildAgentBundle(options) {
|
|
const projectRoot = options.projectRoot ?? root
|
|
const arch = options.arch
|
|
const platform = options.platform ?? 'linux'
|
|
const lock = options.lock ?? readRuntimeLock(projectRoot)
|
|
const runtimeInput = lockedRuntimeInput(
|
|
lock,
|
|
arch,
|
|
options.runtimeArchive,
|
|
platform
|
|
)
|
|
const registry =
|
|
options.registry ?? readTrustedKeyRegistry(projectRoot)
|
|
const signingEnvironment = options.environment ?? process.env
|
|
const signingIdentity =
|
|
options.testSigningIdentity ??
|
|
options.signingIdentity ??
|
|
productionSigningKey(signingEnvironment)
|
|
const signingRecord = options.testSigningIdentity
|
|
? registry.keys.find(
|
|
(key) => key.keyId === signingIdentity.keyId
|
|
)
|
|
: preflightProductionSigningKey({
|
|
projectRoot,
|
|
registry,
|
|
lock,
|
|
environment: signingEnvironment,
|
|
keyId: signingIdentity.keyId
|
|
})
|
|
if (!signingRecord) {
|
|
throw new Error(
|
|
`Agent test signing key is not registered: ${signingIdentity.keyId}`
|
|
)
|
|
}
|
|
if (
|
|
publicKeySpkiBase64(signingIdentity.privateKey) !==
|
|
signingRecord.publicKeySpkiBase64
|
|
) {
|
|
throw new Error(
|
|
'GoodBuddy signing private key does not match the trusted key'
|
|
)
|
|
}
|
|
if (
|
|
!options.testSigningIdentity &&
|
|
!options.signingIdentity &&
|
|
signingEnvironment === process.env
|
|
) {
|
|
delete process.env[signingPrivateKeyEnvironment]
|
|
delete process.env[signingKeyIdEnvironment]
|
|
}
|
|
|
|
const destination =
|
|
options.outputDirectory ??
|
|
join(projectRoot, '.agent-resources', targetName(arch, platform))
|
|
mkdirSync(dirname(destination), { recursive: true })
|
|
const staging = mkdtempSync(
|
|
join(dirname(destination), `.stage-${targetName(arch, platform)}-`)
|
|
)
|
|
const runtimeStaging = mkdtempSync(
|
|
join(tmpdir(), `goodbuddy-node-${targetName(arch, platform)}-`)
|
|
)
|
|
try {
|
|
tar.x({
|
|
file: options.runtimeArchive,
|
|
cwd: runtimeStaging,
|
|
sync: true,
|
|
strict: true,
|
|
filter(path) {
|
|
return (
|
|
path === runtimeInput.binaryPath ||
|
|
path === runtimeInput.licensePath
|
|
)
|
|
}
|
|
})
|
|
const runtimeBinary = join(
|
|
runtimeStaging,
|
|
...runtimeInput.binaryPath.split('/')
|
|
)
|
|
const runtimeLicense = join(
|
|
runtimeStaging,
|
|
...runtimeInput.licensePath.split('/')
|
|
)
|
|
assertElfArchitecture(runtimeBinary, arch, 'Agent Node runtime', platform)
|
|
copyFileSync(runtimeBinary, join(staging, 'node'))
|
|
chmodSync(join(staging, 'node'), 0o755)
|
|
|
|
mkdirSync(join(staging, 'lib'), { recursive: true })
|
|
const esbuild = require('esbuild')
|
|
esbuild.buildSync({
|
|
stdin: {
|
|
contents: [
|
|
"import { runAgentCli } from './src/agent-daemon/cli.ts'",
|
|
'void runAgentCli(process.argv.slice(2)).then(',
|
|
' (code) => { process.exitCode = code },',
|
|
' () => {',
|
|
" process.stderr.write('Agent startup failed\\n')",
|
|
' process.exitCode = 2',
|
|
' }',
|
|
')',
|
|
''
|
|
].join('\n'),
|
|
loader: 'ts',
|
|
resolveDir: projectRoot,
|
|
sourcefile: 'goodbuddy-agent-cjs-entry.ts'
|
|
},
|
|
outfile: join(staging, 'lib', 'agent.cjs'),
|
|
bundle: true,
|
|
platform: 'node',
|
|
target: ['node24.19'],
|
|
format: 'cjs',
|
|
packages: 'bundle',
|
|
external: [koffiPackageName],
|
|
sourcemap: false,
|
|
legalComments: 'none',
|
|
charset: 'utf8',
|
|
logLevel: 'silent'
|
|
})
|
|
chmodSync(join(staging, 'lib', 'agent.cjs'), 0o644)
|
|
const koffi = copyKoffiRuntime(
|
|
projectRoot,
|
|
staging,
|
|
arch,
|
|
lock.koffi.version,
|
|
platform
|
|
)
|
|
const launcher = [
|
|
'#!/bin/sh',
|
|
'set -eu',
|
|
'SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)',
|
|
'exec "$SCRIPT_DIR/node" "$SCRIPT_DIR/lib/agent.cjs" "$@"',
|
|
''
|
|
].join('\n')
|
|
writeFileSync(join(staging, 'goodbuddy-agent'), launcher, 'utf8')
|
|
chmodSync(join(staging, 'goodbuddy-agent'), 0o755)
|
|
|
|
mkdirSync(join(staging, 'licenses'), { recursive: true })
|
|
copyLicense(
|
|
join(projectRoot, 'LICENSE'),
|
|
join(staging, 'licenses', 'GoodBuddy-0BSD.txt')
|
|
)
|
|
copyLicense(
|
|
runtimeLicense,
|
|
join(staging, 'licenses', 'Node.js-MIT.txt')
|
|
)
|
|
copyLicense(
|
|
join(projectRoot, 'node_modules', 'zod', 'LICENSE'),
|
|
join(staging, 'licenses', 'zod-MIT.txt')
|
|
)
|
|
copyLicense(
|
|
join(
|
|
projectRoot,
|
|
'node_modules',
|
|
'@agentclientprotocol',
|
|
'sdk',
|
|
'LICENSE'
|
|
),
|
|
join(
|
|
staging,
|
|
'licenses',
|
|
'agent-client-protocol-Apache-2.0.txt'
|
|
)
|
|
)
|
|
copyLicense(
|
|
join(projectRoot, 'node_modules', 'koffi', 'LICENSE.txt'),
|
|
join(staging, 'licenses', 'koffi-MIT.txt')
|
|
)
|
|
copyLicense(
|
|
join(projectRoot, 'node_modules', 'koffi', 'LICENSE.txt'),
|
|
join(staging, 'licenses', 'koffi-native-MIT.txt')
|
|
)
|
|
const zodPackage = readJson(
|
|
join(projectRoot, 'node_modules', 'zod', 'package.json'),
|
|
'zod package metadata'
|
|
)
|
|
const acpSdkPackage = readJson(
|
|
join(
|
|
projectRoot,
|
|
'node_modules',
|
|
'@agentclientprotocol',
|
|
'sdk',
|
|
'package.json'
|
|
),
|
|
'ACP SDK package metadata'
|
|
)
|
|
const manifest = createManifest(staging, {
|
|
agentVersion: lock.agentVersion,
|
|
nodeVersion: lock.node.version,
|
|
zodVersion: zodPackage.version,
|
|
acpSdkVersion: acpSdkPackage.version,
|
|
koffiVersion: koffi.version,
|
|
koffiNativePackage: koffi.nativePackage,
|
|
platform,
|
|
arch,
|
|
protocol: lock.protocol,
|
|
signingKeyId: signingIdentity.keyId
|
|
})
|
|
const manifestBytes = canonicalManifestBytes(manifest)
|
|
const signatureBytes = sign(
|
|
null,
|
|
signaturePayload(manifestBytes),
|
|
signingIdentity.privateKey
|
|
)
|
|
writeFileSync(join(staging, manifestFileName), manifestBytes)
|
|
writeFileSync(
|
|
join(staging, signatureFileName),
|
|
`${signatureBytes.toString('base64')}\n`,
|
|
'utf8'
|
|
)
|
|
chmodSync(join(staging, manifestFileName), 0o644)
|
|
chmodSync(join(staging, signatureFileName), 0o644)
|
|
verifyBundleDirectory(staging, {
|
|
registry,
|
|
verificationEnvironment: options.testSigningIdentity
|
|
? 'test'
|
|
: 'production',
|
|
expected: {
|
|
platform,
|
|
agentVersion: lock.agentVersion,
|
|
arch,
|
|
protocol: lock.protocol
|
|
}
|
|
})
|
|
replaceBundle(staging, destination)
|
|
return destination
|
|
} finally {
|
|
rmSync(staging, { recursive: true, force: true })
|
|
rmSync(runtimeStaging, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
function createAgentArchive(bundleDirectory, archivePath) {
|
|
const entries = listFiles(bundleDirectory)
|
|
.map((filePath) => posixRelative(bundleDirectory, filePath))
|
|
.sort(compareUtf8)
|
|
mkdirSync(dirname(archivePath), { recursive: true })
|
|
rmSync(archivePath, { force: true })
|
|
tar.c({
|
|
cwd: bundleDirectory,
|
|
file: archivePath,
|
|
sync: true,
|
|
portable: true,
|
|
noPax: true,
|
|
mtime: new Date(0)
|
|
}, entries)
|
|
}
|
|
|
|
function importAgentArchive(archivePath, options = {}) {
|
|
if (!existsSync(archivePath)) {
|
|
throw new Error(`Agent artifact archive is missing: ${archivePath}`)
|
|
}
|
|
const projectRoot = options.projectRoot ?? root
|
|
const arch = options.arch
|
|
const destination =
|
|
options.outputDirectory ??
|
|
join(projectRoot, '.agent-resources', targetName(arch))
|
|
mkdirSync(dirname(destination), { recursive: true })
|
|
const staging = mkdtempSync(
|
|
join(dirname(destination), `.import-${targetName(arch)}-`)
|
|
)
|
|
try {
|
|
tar.x({
|
|
cwd: staging,
|
|
file: archivePath,
|
|
sync: true,
|
|
strict: true,
|
|
preservePaths: false
|
|
})
|
|
verifyLockedBundle(staging, arch, {
|
|
projectRoot,
|
|
registry: options.registry
|
|
})
|
|
replaceBundle(staging, destination)
|
|
return destination
|
|
} finally {
|
|
rmSync(staging, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
function verifyLockedBundle(bundleDirectory, arch, options = {}) {
|
|
const projectRoot = options.projectRoot ?? root
|
|
const lock = options.lock ?? readRuntimeLock(projectRoot)
|
|
return verifyBundleDirectory(bundleDirectory, {
|
|
registry:
|
|
options.registry ?? readTrustedKeyRegistry(projectRoot),
|
|
verificationEnvironment: options.verificationEnvironment ?? 'production',
|
|
expected: {
|
|
platform: options.platform ?? 'linux',
|
|
agentVersion: lock.agentVersion,
|
|
arch,
|
|
protocol: lock.protocol
|
|
}
|
|
})
|
|
}
|
|
|
|
function verifyAgentBundleMatrix(resourcesRoot, options = {}) {
|
|
const projectRoot = options.projectRoot ?? root
|
|
const registryPath =
|
|
options.registryPath ??
|
|
join(projectRoot, 'resources', 'agent-release-keys.json')
|
|
const lockPath =
|
|
options.lockPath ??
|
|
join(projectRoot, 'agent-runtime-lock.json')
|
|
const registry = readJson(registryPath, 'Agent trusted key registry')
|
|
const lock = readJson(lockPath, 'Agent runtime lock')
|
|
for (const arch of supportedArchitectures) {
|
|
verifyLockedBundle(
|
|
join(resourcesRoot, targetName(arch)),
|
|
arch,
|
|
{
|
|
projectRoot,
|
|
registry,
|
|
lock,
|
|
verificationEnvironment: 'production'
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
function redactSecrets(value, environment = process.env) {
|
|
let redacted = String(value)
|
|
for (const secret of [
|
|
environment[signingPrivateKeyEnvironment],
|
|
environment[signingKeyIdEnvironment]
|
|
]) {
|
|
if (secret && secret.length >= 4) {
|
|
redacted = redacted.split(secret).join('[redacted]')
|
|
}
|
|
}
|
|
return redacted
|
|
.replace(
|
|
/-----BEGIN (?:PRIVATE KEY|OPENSSH PRIVATE KEY)-----[\s\S]*?-----END (?:PRIVATE KEY|OPENSSH PRIVATE KEY)-----/gu,
|
|
'[redacted private key]'
|
|
)
|
|
.replace(
|
|
/((?:private[_-]?key|signing[_-]?key|authorization|password|token)\s*[:=]\s*)\S+/giu,
|
|
'$1[redacted]'
|
|
)
|
|
}
|
|
|
|
function parseArguments(argv) {
|
|
const command = argv[0]
|
|
const options = {}
|
|
for (let index = 1; index < argv.length; index += 1) {
|
|
const argument = argv[index]
|
|
if (['--arch', '--runtime-archive', '--archive', '--output'].includes(argument)) {
|
|
const value = argv[index + 1]
|
|
if (!value) {
|
|
throw new Error(`${argument} requires a value`)
|
|
}
|
|
options[argument.slice(2).replaceAll('-', '')] = value
|
|
index += 1
|
|
} else {
|
|
throw new Error(`Unknown Agent bundle argument: ${argument}`)
|
|
}
|
|
}
|
|
if (!['build', 'import', 'verify', 'preflight'].includes(command)) {
|
|
throw new Error('Usage: agent-bundle.cjs <build|import|verify|preflight> --arch <x64|arm64>')
|
|
}
|
|
if (command === 'preflight') {
|
|
if (Object.keys(options).length > 0) {
|
|
throw new Error('GoodBuddy signing preflight does not accept arguments')
|
|
}
|
|
return { command }
|
|
}
|
|
targetName(options.arch)
|
|
return { command, ...options }
|
|
}
|
|
|
|
function main(argv = process.argv.slice(2)) {
|
|
const options = parseArguments(argv)
|
|
if (options.command === 'preflight') {
|
|
const signingRecord = preflightProductionSigningKey()
|
|
console.log(
|
|
`Production GoodBuddy signing registry preflight passed: ${signingRecord.keyId}`
|
|
)
|
|
return
|
|
}
|
|
if (options.command === 'build') {
|
|
const outputDirectory = buildAgentBundle({
|
|
arch: options.arch,
|
|
runtimeArchive: resolve(options.runtimearchive ?? ''),
|
|
outputDirectory: options.output
|
|
? resolve(options.output)
|
|
: undefined
|
|
})
|
|
const archivePath = options.archive
|
|
? resolve(options.archive)
|
|
: join(
|
|
root,
|
|
'dist',
|
|
'agent-bundles',
|
|
`goodbuddy-agent-${targetName(options.arch)}.tar`
|
|
)
|
|
createAgentArchive(outputDirectory, archivePath)
|
|
console.log(`Agent bundle built and verified: ${targetName(options.arch)}`)
|
|
return
|
|
}
|
|
if (options.command === 'import') {
|
|
importAgentArchive(resolve(options.archive ?? ''), {
|
|
arch: options.arch,
|
|
outputDirectory: options.output
|
|
? resolve(options.output)
|
|
: undefined
|
|
})
|
|
console.log(`Agent bundle imported and verified: ${targetName(options.arch)}`)
|
|
return
|
|
}
|
|
verifyLockedBundle(
|
|
options.output
|
|
? resolve(options.output)
|
|
: join(root, '.agent-resources', targetName(options.arch)),
|
|
options.arch
|
|
)
|
|
console.log(`Agent bundle verified: ${targetName(options.arch)}`)
|
|
}
|
|
|
|
module.exports = {
|
|
koffiPayloadPaths,
|
|
assertElfArchitecture,
|
|
assertSafeManifestPath,
|
|
buildAgentBundle,
|
|
canonicalManifestBytes,
|
|
createAgentArchive,
|
|
createManifest,
|
|
detectElfArchitecture,
|
|
expectedManifestMode,
|
|
importAgentArchive,
|
|
lockedRuntimeInput,
|
|
preflightProductionSigningKey,
|
|
productionSigningKey,
|
|
publicKeySpkiBase64,
|
|
readRuntimeLock,
|
|
readTrustedKeyRegistry,
|
|
redactSecrets,
|
|
signManifestForTest,
|
|
signaturePayload,
|
|
supportedArchitectures,
|
|
targetName,
|
|
verifyAgentBundleMatrix,
|
|
verifyBundleDirectory,
|
|
verifyLockedBundle,
|
|
verifyManifestSignature
|
|
}
|
|
|
|
if (require.main === module) {
|
|
try {
|
|
main()
|
|
} catch (error) {
|
|
console.error(
|
|
redactSecrets(error instanceof Error ? error.message : String(error))
|
|
)
|
|
process.exitCode = 1
|
|
}
|
|
}
|