OpenCode startup on Linux ARM could run a --version probe before launching the configured Runtime and recheck registered remote binary metadata. Execution now trusts the selected path and registered manifest identity after setup-time verification, so switching and cold launch avoid duplicate binary work. Release note: 优化 Linux ARM 上切换 OpenCode 的启动速度;已安装或已登记的 Runtime 在执行时不再重复启动或检查二进制。
744 lines
19 KiB
TypeScript
744 lines
19 KiB
TypeScript
import {
|
|
createHash,
|
|
createPublicKey,
|
|
verify
|
|
} from 'node:crypto'
|
|
import { createReadStream, lstatSync } from 'node:fs'
|
|
import {
|
|
lstat,
|
|
open,
|
|
readFile,
|
|
readdir
|
|
} from 'node:fs/promises'
|
|
import {
|
|
basename,
|
|
isAbsolute,
|
|
join,
|
|
parse,
|
|
relative,
|
|
resolve,
|
|
sep
|
|
} from 'node:path'
|
|
import {
|
|
agentReleaseKeyRegistrySchema,
|
|
type AgentArchitecture,
|
|
type AgentReleaseKey,
|
|
type AgentReleaseKeyRegistry
|
|
} from '../agent-installation-contracts'
|
|
import {
|
|
digestRemoteRuntimeBundleIdentity,
|
|
digestRemoteRuntimeBundleManifest,
|
|
remoteRuntimeBundleManifestSchema,
|
|
remoteRuntimeLockSchema,
|
|
type RemoteRuntimeBundleManifest,
|
|
type RemoteRuntimeLock
|
|
} from '../remote-runtime-launch-contracts'
|
|
import {
|
|
runtimeRegistryEntrySchema,
|
|
type RuntimeRegistryEntry
|
|
} from '../remote-environment-registry-contracts'
|
|
|
|
export type InstalledBundleVerificationEnvironment =
|
|
| 'production'
|
|
| 'test'
|
|
|
|
const SIGNATURE_DOMAIN = Buffer.from(
|
|
'GoodBuddy Remote Runtime Bundle Manifest Signature v1\0',
|
|
'utf8'
|
|
)
|
|
const MANIFEST_FILE_NAME = 'manifest.json'
|
|
const SIGNATURE_FILE_NAME = 'manifest.sig'
|
|
const MAXIMUM_METADATA_BYTES = 1024 * 1024
|
|
|
|
export type VerifiedRuntimeBundle = {
|
|
bundleDirectory: string
|
|
executablePath: string
|
|
manifest: RemoteRuntimeBundleManifest
|
|
manifestDigest: string
|
|
}
|
|
|
|
export type VerifyRuntimeBundleOptions = {
|
|
architecture: AgentArchitecture
|
|
releaseKeyRegistry: AgentReleaseKeyRegistry
|
|
runtimeLock: RemoteRuntimeLock
|
|
verificationEnvironment?: InstalledBundleVerificationEnvironment
|
|
enforceFilesystemMode?: boolean
|
|
filesystemPlatform?: NodeJS.Platform
|
|
uid?: number
|
|
}
|
|
|
|
export type LoadRegisteredRuntimeBundleOptions =
|
|
VerifyRuntimeBundleOptions & {
|
|
registered: RuntimeRegistryEntry
|
|
}
|
|
|
|
export async function verifyRuntimeBundle(
|
|
bundleDirectoryInput: string,
|
|
options: VerifyRuntimeBundleOptions
|
|
): Promise<VerifiedRuntimeBundle> {
|
|
const verified = await verifyPublishedRuntimeBundle(
|
|
bundleDirectoryInput,
|
|
options
|
|
)
|
|
const { bundleDirectory, manifest } = verified
|
|
const inventory = await inventoryBundleFiles(
|
|
bundleDirectory,
|
|
options.uid
|
|
)
|
|
const declaredPaths = new Set(
|
|
manifest.files.map((file) => file.path)
|
|
)
|
|
for (const file of manifest.files) {
|
|
const filePath = join(
|
|
bundleDirectory,
|
|
...file.path.split('/')
|
|
)
|
|
const stat = inventory.get(file.path)
|
|
if (stat === undefined) {
|
|
throw new Error(`Runtime payload is missing: ${file.path}`)
|
|
}
|
|
if (stat.size !== file.size) {
|
|
throw new Error(`Runtime payload size mismatch: ${file.path}`)
|
|
}
|
|
if ((await sha256File(filePath)) !== file.sha256) {
|
|
throw new Error(`Runtime payload hash mismatch: ${file.path}`)
|
|
}
|
|
if (
|
|
shouldEnforceMode(options) &&
|
|
modeString(stat.mode) !== file.mode
|
|
) {
|
|
throw new Error(`Runtime payload mode mismatch: ${file.path}`)
|
|
}
|
|
}
|
|
|
|
const actualPaths = new Set(inventory.keys())
|
|
const expectedPaths = new Set([
|
|
...declaredPaths,
|
|
MANIFEST_FILE_NAME,
|
|
SIGNATURE_FILE_NAME
|
|
])
|
|
if (
|
|
actualPaths.size !== expectedPaths.size ||
|
|
[...actualPaths].some((path) => !expectedPaths.has(path))
|
|
) {
|
|
throw new Error(
|
|
'Runtime bundle contains undeclared or missing files'
|
|
)
|
|
}
|
|
|
|
return verified
|
|
}
|
|
|
|
/**
|
|
* Authenticates a package-installer-published Runtime from signed metadata and
|
|
* bounded entrypoint metadata. The installer already hashed every payload
|
|
* file while extracting it.
|
|
*/
|
|
export async function verifyPublishedRuntimeBundle(
|
|
bundleDirectoryInput: string,
|
|
options: VerifyRuntimeBundleOptions
|
|
): Promise<VerifiedRuntimeBundle> {
|
|
const bundleDirectory = assertAbsoluteManagedPath(
|
|
resolve(bundleDirectoryInput)
|
|
)
|
|
const registry = parseReleaseKeyRegistry(
|
|
options.releaseKeyRegistry
|
|
)
|
|
const runtimeLock = remoteRuntimeLockSchema.parse(options.runtimeLock)
|
|
const verificationEnvironment =
|
|
options.verificationEnvironment ?? 'production'
|
|
assertRegistryAvailable(registry, verificationEnvironment)
|
|
|
|
const rootStat = await lstat(bundleDirectory)
|
|
assertOwnedDirectory(rootStat, 'Runtime bundle directory', options.uid)
|
|
if (
|
|
shouldEnforceMode(options) &&
|
|
modeString(rootStat.mode) !== '0700'
|
|
) {
|
|
throw new Error('Runtime bundle directory permissions must be 0700')
|
|
}
|
|
|
|
const manifestBytes = await readMetadataFile(
|
|
join(bundleDirectory, MANIFEST_FILE_NAME),
|
|
'Runtime manifest',
|
|
options
|
|
)
|
|
const signatureBytes = await readSignature(
|
|
join(bundleDirectory, SIGNATURE_FILE_NAME),
|
|
options
|
|
)
|
|
const manifest = await verifyRuntimeManifestSignature(
|
|
manifestBytes,
|
|
signatureBytes,
|
|
registry,
|
|
verificationEnvironment
|
|
)
|
|
assertRuntimeManifestMatchesLock(
|
|
manifest,
|
|
runtimeLock,
|
|
options.architecture
|
|
)
|
|
if (
|
|
basename(bundleDirectory) !==
|
|
manifest.bundleDigest.slice('sha256:'.length)
|
|
) {
|
|
throw new Error(
|
|
'Runtime bundle digest does not match its managed directory'
|
|
)
|
|
}
|
|
|
|
const executableRecord = manifest.files.find(
|
|
(file) => file.path === manifest.entrypoint.path
|
|
)
|
|
if (executableRecord === undefined) {
|
|
throw new Error(
|
|
'Published Runtime entrypoint is not declared'
|
|
)
|
|
}
|
|
const executablePath = join(
|
|
bundleDirectory,
|
|
...manifest.entrypoint.path.split('/')
|
|
)
|
|
const executableStat = await lstat(executablePath)
|
|
assertOwnedRegularFile(
|
|
executableStat,
|
|
'Runtime executable',
|
|
options.uid
|
|
)
|
|
if (
|
|
executableStat.size !== executableRecord.size ||
|
|
(
|
|
shouldEnforceMode(options) &&
|
|
modeString(executableStat.mode) !== executableRecord.mode
|
|
)
|
|
) {
|
|
throw new Error(
|
|
'Published Runtime entrypoint metadata changed'
|
|
)
|
|
}
|
|
await assertElfArchitecture(
|
|
executablePath,
|
|
options.architecture,
|
|
'Runtime executable'
|
|
)
|
|
return {
|
|
bundleDirectory,
|
|
executablePath,
|
|
manifest,
|
|
manifestDigest:
|
|
await digestRemoteRuntimeBundleManifest(manifest)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reconstructs a Runtime identity that was fully verified when it was
|
|
* registered. Normal capabilities and prompt launches use this bounded
|
|
* metadata path instead of re-verifying or executing the Runtime binary.
|
|
*/
|
|
export async function loadRegisteredRuntimeBundle(
|
|
bundleDirectoryInput: string,
|
|
options: LoadRegisteredRuntimeBundleOptions
|
|
): Promise<VerifiedRuntimeBundle> {
|
|
const registered = runtimeRegistryEntrySchema.parse(
|
|
options.registered
|
|
)
|
|
const bundleDirectory = assertAbsoluteManagedPath(
|
|
resolve(bundleDirectoryInput)
|
|
)
|
|
const rootStat = await lstat(bundleDirectory)
|
|
assertOwnedDirectory(
|
|
rootStat,
|
|
'Runtime bundle directory',
|
|
options.uid
|
|
)
|
|
if (
|
|
shouldEnforceMode(options) &&
|
|
modeString(rootStat.mode) !== '0700'
|
|
) {
|
|
throw new Error('Runtime bundle directory permissions must be 0700')
|
|
}
|
|
const manifestBytes = await readMetadataFile(
|
|
join(bundleDirectory, MANIFEST_FILE_NAME),
|
|
'Runtime manifest',
|
|
options
|
|
)
|
|
let value: unknown
|
|
try {
|
|
value = JSON.parse(manifestBytes.toString('utf8'))
|
|
} catch (error) {
|
|
throw new Error('Runtime manifest is invalid JSON', {
|
|
cause: error
|
|
})
|
|
}
|
|
const manifest = remoteRuntimeBundleManifestSchema.parse(value)
|
|
if (!canonicalRuntimeManifestBytes(manifest).equals(manifestBytes)) {
|
|
throw new Error(
|
|
'Runtime manifest is not in canonical deterministic form'
|
|
)
|
|
}
|
|
assertRuntimeManifestMatchesLock(
|
|
manifest,
|
|
remoteRuntimeLockSchema.parse(options.runtimeLock),
|
|
options.architecture
|
|
)
|
|
const manifestDigest =
|
|
await digestRemoteRuntimeBundleManifest(manifest)
|
|
if (
|
|
basename(bundleDirectory) !==
|
|
registered.bundleDigest.slice('sha256:'.length) ||
|
|
manifest.runtimeId !== registered.runtimeId ||
|
|
manifest.runtimeVersion !== registered.runtimeVersion ||
|
|
manifest.architecture !== registered.architecture ||
|
|
manifest.bundleDigest !== registered.bundleDigest ||
|
|
manifestDigest !== registered.manifestDigest ||
|
|
(
|
|
registered.runtimeAdapterDigest !== undefined &&
|
|
manifest.adapterDigest !== registered.runtimeAdapterDigest
|
|
) ||
|
|
manifest.acpCapabilitiesDigest !==
|
|
registered.acpCapabilitiesDigest
|
|
) {
|
|
throw new Error(
|
|
'Runtime manifest does not match its registered identity'
|
|
)
|
|
}
|
|
|
|
return {
|
|
bundleDirectory,
|
|
executablePath: join(
|
|
bundleDirectory,
|
|
...manifest.entrypoint.path.split('/')
|
|
),
|
|
manifest,
|
|
manifestDigest
|
|
}
|
|
}
|
|
|
|
export function canonicalRuntimeManifestBytes(
|
|
manifest: RemoteRuntimeBundleManifest
|
|
): Buffer {
|
|
return Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
|
|
}
|
|
|
|
export function runtimeManifestSignaturePayload(
|
|
manifestBytes: Uint8Array
|
|
): Buffer {
|
|
return Buffer.concat([
|
|
SIGNATURE_DOMAIN,
|
|
Buffer.from(manifestBytes)
|
|
])
|
|
}
|
|
|
|
export async function verifyRuntimeManifestSignature(
|
|
manifestBytes: Buffer,
|
|
signatureBytes: Buffer,
|
|
registryInput: AgentReleaseKeyRegistry,
|
|
verificationEnvironment: InstalledBundleVerificationEnvironment =
|
|
'production'
|
|
): Promise<RemoteRuntimeBundleManifest> {
|
|
const registry = parseReleaseKeyRegistry(registryInput)
|
|
assertRegistryAvailable(registry, verificationEnvironment)
|
|
let untrusted: unknown
|
|
try {
|
|
untrusted = JSON.parse(manifestBytes.toString('utf8'))
|
|
} catch (error) {
|
|
throw new Error('Runtime manifest JSON is invalid', { cause: error })
|
|
}
|
|
const parsed = remoteRuntimeBundleManifestSchema.safeParse(untrusted)
|
|
if (!parsed.success) {
|
|
throw new Error(
|
|
`Runtime manifest contract is invalid: ${parsed.error.message}`
|
|
)
|
|
}
|
|
const manifest = parsed.data
|
|
if (!canonicalRuntimeManifestBytes(manifest).equals(manifestBytes)) {
|
|
throw new Error(
|
|
'Runtime manifest is not in canonical deterministic form'
|
|
)
|
|
}
|
|
const key = trustedKeyForManifest(
|
|
manifest,
|
|
registry,
|
|
verificationEnvironment
|
|
)
|
|
const publicKey = importPublicKey(key)
|
|
if (
|
|
signatureBytes.length !== 64 ||
|
|
!verify(
|
|
null,
|
|
runtimeManifestSignaturePayload(manifestBytes),
|
|
publicKey,
|
|
signatureBytes
|
|
)
|
|
) {
|
|
throw new Error('Runtime manifest signature verification failed')
|
|
}
|
|
if (
|
|
await digestRemoteRuntimeBundleIdentity(manifest) !==
|
|
manifest.bundleDigest
|
|
) {
|
|
throw new Error('Runtime bundle identity digest does not match')
|
|
}
|
|
return manifest
|
|
}
|
|
|
|
export async function readRemoteRuntimeLock(
|
|
filePathInput: string,
|
|
options: {
|
|
uid?: number
|
|
filesystemPlatform?: NodeJS.Platform
|
|
} = {}
|
|
): Promise<RemoteRuntimeLock> {
|
|
const filePath = assertAbsoluteManagedPath(resolve(filePathInput))
|
|
const contents = await readMetadataFile(
|
|
filePath,
|
|
'Remote Runtime lock',
|
|
options
|
|
)
|
|
let value: unknown
|
|
try {
|
|
value = JSON.parse(contents.toString('utf8'))
|
|
} catch (error) {
|
|
throw new Error('Remote Runtime lock is invalid JSON', {
|
|
cause: error
|
|
})
|
|
}
|
|
return remoteRuntimeLockSchema.parse(value)
|
|
}
|
|
|
|
export function assertRuntimeManifestMatchesLock(
|
|
manifest: RemoteRuntimeBundleManifest,
|
|
lock: RemoteRuntimeLock,
|
|
architecture: AgentArchitecture
|
|
): void {
|
|
const expected = lock.runtimes.opencode
|
|
const target = expected.targets[architecture]
|
|
if (
|
|
manifest.runtimeId !== 'opencode' ||
|
|
manifest.provider !== expected.provider ||
|
|
manifest.runtimeVersion !== expected.version ||
|
|
manifest.architecture !== architecture ||
|
|
manifest.entrypoint.path !== expected.entrypoint ||
|
|
manifest.entrypoint.identity !== expected.entrypointIdentity ||
|
|
manifest.entrypoint.argvPrefix.length !==
|
|
expected.argvPrefix.length ||
|
|
manifest.entrypoint.argvPrefix.some(
|
|
(argument, index) => argument !== expected.argvPrefix[index]
|
|
) ||
|
|
manifest.protocol.major !== expected.protocol.major ||
|
|
manifest.protocol.minor !== expected.protocol.minor ||
|
|
manifest.allowedEnvironmentNames.length !==
|
|
expected.allowedEnvironmentNames.length ||
|
|
manifest.allowedEnvironmentNames.some(
|
|
(name, index) =>
|
|
name !== expected.allowedEnvironmentNames[index]
|
|
) ||
|
|
manifest.sourcePackage.name !== target.package ||
|
|
manifest.sourcePackage.integrity !== target.integrity
|
|
) {
|
|
throw new Error(
|
|
'Runtime manifest does not match the locked OpenCode profile'
|
|
)
|
|
}
|
|
}
|
|
|
|
function parseReleaseKeyRegistry(
|
|
value: unknown
|
|
): AgentReleaseKeyRegistry {
|
|
const parsed = agentReleaseKeyRegistrySchema.safeParse(value)
|
|
if (!parsed.success) {
|
|
throw new Error(
|
|
`Runtime trusted key registry contract is invalid: ${parsed.error.message}`
|
|
)
|
|
}
|
|
for (const key of parsed.data.keys) {
|
|
importPublicKey(key)
|
|
}
|
|
return parsed.data
|
|
}
|
|
|
|
function assertRegistryAvailable(
|
|
registry: AgentReleaseKeyRegistry,
|
|
environment: InstalledBundleVerificationEnvironment
|
|
): void {
|
|
if (
|
|
environment === 'production' &&
|
|
!registry.keys.some((key) => key.environment === 'production')
|
|
) {
|
|
throw new Error(
|
|
'Runtime production key registry is empty; verification is unavailable'
|
|
)
|
|
}
|
|
}
|
|
|
|
function trustedKeyForManifest(
|
|
manifest: RemoteRuntimeBundleManifest,
|
|
registry: AgentReleaseKeyRegistry,
|
|
environment: InstalledBundleVerificationEnvironment
|
|
): AgentReleaseKey {
|
|
const key = registry.keys.find(
|
|
(candidate) => candidate.keyId === manifest.signingKeyId
|
|
)
|
|
if (key === undefined) {
|
|
throw new Error(
|
|
`Runtime manifest uses an unknown signing key: ${manifest.signingKeyId}`
|
|
)
|
|
}
|
|
if (environment === 'production' && key.environment !== 'production') {
|
|
throw new Error(
|
|
`Production Runtime verification rejects non-production key: ${key.keyId}`
|
|
)
|
|
}
|
|
if (
|
|
registry.revocations.some(
|
|
(revocation) => revocation.keyId === key.keyId
|
|
)
|
|
) {
|
|
throw new Error(`Runtime signing key is revoked: ${key.keyId}`)
|
|
}
|
|
return key
|
|
}
|
|
|
|
function importPublicKey(key: AgentReleaseKey) {
|
|
let publicKey
|
|
try {
|
|
publicKey = createPublicKey({
|
|
key: Buffer.from(key.publicKeySpkiBase64, 'base64'),
|
|
format: 'der',
|
|
type: 'spki'
|
|
})
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Runtime trusted public key is invalid: ${key.keyId}`,
|
|
{ cause: error }
|
|
)
|
|
}
|
|
if (publicKey.asymmetricKeyType !== 'ed25519') {
|
|
throw new Error(
|
|
`Runtime trusted public key is not Ed25519: ${key.keyId}`
|
|
)
|
|
}
|
|
return publicKey
|
|
}
|
|
|
|
async function readMetadataFile(
|
|
filePath: string,
|
|
label: string,
|
|
options: {
|
|
enforceFilesystemMode?: boolean
|
|
filesystemPlatform?: NodeJS.Platform
|
|
uid?: number
|
|
}
|
|
): Promise<Buffer> {
|
|
const stat = await lstat(filePath)
|
|
assertOwnedRegularFile(stat, label, options.uid)
|
|
if (
|
|
shouldEnforceMode(options) &&
|
|
modeString(stat.mode) !== '0644' &&
|
|
modeString(stat.mode) !== '0600'
|
|
) {
|
|
throw new Error(`${label} permissions must be 0600 or 0644`)
|
|
}
|
|
if (stat.size > MAXIMUM_METADATA_BYTES) {
|
|
throw new Error(`${label} exceeds its size limit`)
|
|
}
|
|
return readFile(filePath)
|
|
}
|
|
|
|
async function readSignature(
|
|
filePath: string,
|
|
options: VerifyRuntimeBundleOptions
|
|
): Promise<Buffer> {
|
|
const contents = await readMetadataFile(
|
|
filePath,
|
|
'Runtime detached signature',
|
|
options
|
|
)
|
|
const text = contents.toString('utf8')
|
|
if (!/^[A-Za-z0-9+/]{86}==\n$/u.test(text)) {
|
|
throw new Error('Runtime detached signature encoding is invalid')
|
|
}
|
|
return Buffer.from(text.trim(), 'base64')
|
|
}
|
|
|
|
async function inventoryBundleFiles(
|
|
bundleDirectory: string,
|
|
uid = process.getuid?.()
|
|
): Promise<Map<string, Awaited<ReturnType<typeof lstat>>>> {
|
|
const files = new Map<
|
|
string,
|
|
Awaited<ReturnType<typeof lstat>>
|
|
>()
|
|
const pending = [bundleDirectory]
|
|
while (pending.length > 0) {
|
|
const current = pending.pop()
|
|
if (current === undefined) {
|
|
continue
|
|
}
|
|
for (const entry of await readdir(current, {
|
|
withFileTypes: true
|
|
})) {
|
|
const absolutePath = join(current, entry.name)
|
|
if (entry.isSymbolicLink()) {
|
|
throw new Error(
|
|
`Runtime bundle cannot contain a symlink: ${absolutePath}`
|
|
)
|
|
}
|
|
const stat = await lstat(absolutePath)
|
|
if (entry.isDirectory()) {
|
|
assertOwnedDirectory(
|
|
stat,
|
|
absolutePath,
|
|
uid
|
|
)
|
|
pending.push(absolutePath)
|
|
} else if (entry.isFile()) {
|
|
assertOwnedRegularFile(
|
|
stat,
|
|
absolutePath,
|
|
uid
|
|
)
|
|
files.set(
|
|
relative(bundleDirectory, absolutePath)
|
|
.split(sep)
|
|
.join('/'),
|
|
stat
|
|
)
|
|
} else {
|
|
throw new Error(
|
|
`Runtime bundle contains an unsupported file: ${absolutePath}`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
return files
|
|
}
|
|
|
|
function assertOwnedDirectory(
|
|
stat: Awaited<ReturnType<typeof lstat>>,
|
|
label: string,
|
|
uid = process.getuid?.()
|
|
): void {
|
|
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
throw new Error(`${label} is not a real directory`)
|
|
}
|
|
if (uid !== undefined && stat.uid !== uid) {
|
|
throw new Error(`${label} is not owned by the current user`)
|
|
}
|
|
}
|
|
|
|
function assertOwnedRegularFile(
|
|
stat: Awaited<ReturnType<typeof lstat>>,
|
|
label: string,
|
|
uid = process.getuid?.()
|
|
): void {
|
|
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
throw new Error(`${label} is not a regular file`)
|
|
}
|
|
if (uid !== undefined && stat.uid !== uid) {
|
|
throw new Error(`${label} is not owned by the current user`)
|
|
}
|
|
}
|
|
|
|
function shouldEnforceMode(options: {
|
|
enforceFilesystemMode?: boolean
|
|
filesystemPlatform?: NodeJS.Platform
|
|
}): boolean {
|
|
return (
|
|
options.enforceFilesystemMode !== false &&
|
|
(options.filesystemPlatform ?? process.platform) !== 'win32'
|
|
)
|
|
}
|
|
|
|
function modeString(mode: number | bigint): string {
|
|
return (Number(mode) & 0o777).toString(8).padStart(4, '0')
|
|
}
|
|
|
|
function assertAbsoluteManagedPath(path: string): string {
|
|
if (
|
|
path.includes('\0') ||
|
|
!isAbsolute(path) ||
|
|
resolve(path) !== path
|
|
) {
|
|
throw new Error('Managed paths must be normalized absolute paths')
|
|
}
|
|
const root = parse(path).root
|
|
const components = relative(root, path).split(sep).filter(Boolean)
|
|
let current = root
|
|
for (const component of components) {
|
|
current = resolve(current, component)
|
|
try {
|
|
if (lstatSync(current).isSymbolicLink()) {
|
|
throw new Error('Managed paths cannot traverse a symlink')
|
|
}
|
|
} catch (error) {
|
|
if (
|
|
error instanceof Error &&
|
|
'code' in error &&
|
|
error.code === 'ENOENT'
|
|
) {
|
|
return path
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
return path
|
|
}
|
|
|
|
async function sha256File(filePath: string): Promise<string> {
|
|
const hash = createHash('sha256')
|
|
for await (const chunk of createReadStream(filePath)) {
|
|
hash.update(chunk as Buffer)
|
|
}
|
|
return hash.digest('hex')
|
|
}
|
|
|
|
async function assertElfArchitecture(
|
|
filePath: string,
|
|
expected: AgentArchitecture,
|
|
label: string
|
|
): Promise<void> {
|
|
const handle = await open(filePath, 'r')
|
|
try {
|
|
const header = Buffer.alloc(64)
|
|
const { bytesRead } = await handle.read(
|
|
header,
|
|
0,
|
|
header.length,
|
|
0
|
|
)
|
|
const actual = detectElfArchitecture(
|
|
header.subarray(0, bytesRead)
|
|
)
|
|
if (actual !== expected) {
|
|
throw new Error(
|
|
`${label} architecture mismatch: expected ${expected}, received ${actual ?? 'unknown'}`
|
|
)
|
|
}
|
|
} finally {
|
|
await handle.close()
|
|
}
|
|
}
|
|
|
|
function detectElfArchitecture(
|
|
header: Buffer
|
|
): AgentArchitecture | undefined {
|
|
if (
|
|
header.length < 20 ||
|
|
header[0] !== 0x7f ||
|
|
header.toString('ascii', 1, 4) !== 'ELF' ||
|
|
(header[5] !== 1 && header[5] !== 2)
|
|
) {
|
|
return undefined
|
|
}
|
|
const machine =
|
|
header[5] === 2
|
|
? header.readUInt16BE(18)
|
|
: header.readUInt16LE(18)
|
|
return machine === 62
|
|
? 'x64'
|
|
: machine === 183
|
|
? 'arm64'
|
|
: undefined
|
|
}
|