feat: add managed SSH execution spaces

Add managed SSH Hosts and projects with signed side-by-side GoodBuddy Agent and OpenCode Runtime installation, Ask and Execute workspace access, a Main-owned model bridge, detached Agent lifecycle, durable ACP output recovery, and project activation UI.

Use protocol v2 with bounded FIFO frames over SSH instead of custom credit, priority, batching, or model-message chunking. Mismatched Host Agents now upgrade automatically, an exactly verified Node runtime is reused on-host, and local Portable builds can embed strictly verified Agent artifacts.

Release note: 新增托管 SSH 远程项目,可自动安装或升级签名 Agent/OpenCode Runtime,并在 SSH 断线后继续 Agent 工作和同步未确认输出。
This commit is contained in:
mesalogo
2026-08-24 23:02:18 +08:00
parent 3829a871f5
commit 607bb33249
258 changed files with 99449 additions and 2427 deletions
+612
View File
@@ -0,0 +1,612 @@
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 {
assertDetachedRemoteRuntimeBundleDigest,
digestRemoteRuntimeBundleManifest,
remoteRuntimeBundleManifestSchema,
remoteRuntimeLockSchema,
type RemoteRuntimeBundleManifest,
type RemoteRuntimeLock
} from '../remote-runtime-launch-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 async function verifyRuntimeBundle(
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 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'
)
}
const executablePath = join(
bundleDirectory,
...manifest.entrypoint.path.split('/')
)
await assertElfArchitecture(
executablePath,
options.architecture,
'Runtime executable'
)
return {
bundleDirectory,
executablePath,
manifest,
manifestDigest:
await digestRemoteRuntimeBundleManifest(manifest)
}
}
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')
}
await assertDetachedRemoteRuntimeBundleDigest({
manifest,
detachedSignature: {
manifestDigest:
await digestRemoteRuntimeBundleManifest(manifest),
algorithm: 'ed25519',
keyId: manifest.signingKeyId,
signatureBase64: signatureBytes.toString('base64')
}
})
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
}