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
@@ -0,0 +1,673 @@
import { isDeepStrictEqual } from 'node:util'
import type {
AssistantProject,
ProjectRuntimeValidation,
SshExecutionValidation
} from '../../shared/assistant-contracts'
import { projectRuntimeValidationSchema } from '../../shared/assistant-contracts'
import {
remoteProjectSaveRequestSchema,
type RemoteProjectSavePhase,
type RemoteProjectSaveProgress,
type RemoteProjectSaveRequest
} from '../../shared/remote-project-candidate-contracts'
import { sha256DigestSchema } from '../../shared/agent-protocol/contracts'
import {
REMOTE_WORKSPACE_READ_CAPABILITIES,
remoteWorkspaceValidateResultSchema,
type RemoteWorkspaceHandle
} from '../../shared/remote-agent-contracts'
import {
agentRuntimeSelectionKey,
type AgentRuntimeSelection
} from '../../shared/runtime-selection-contracts'
import type {
AssistantDatabase,
ValidatedSshHostPrecondition,
ValidatedSshProjectWrite
} from '../assistant/assistant-database'
import type {
CurrentSshConnectionTarget,
SshConnectionTarget,
SshHostStore
} from '../ssh/ssh-host-store'
import type {
AgentInstallationIdentity,
AgentInstallationManager
} from './agent-installation-manager'
import type {
RemoteAgentConnection,
RemoteAgentConnectionManager
} from './remote-agent-connection-manager'
const CLEANUP_TIMEOUT_MS = 5_000
const WORKSPACE_READ_CAPABILITY = {
name: 'workspace/read',
minimumVersion: 1,
critical: true
} as const
export interface RemoteProjectSaveOwner {
readonly id: number
isDestroyed(): boolean
on(event: 'destroyed', listener: () => void): unknown
removeListener(event: 'destroyed', listener: () => void): unknown
}
export type RemoteProjectRuntimeValidationInput = Readonly<{
selection: AgentRuntimeSelection
runtimeSelectionKey: string
workMode: 'ask' | 'execute'
host: Readonly<{
hostId: string
hostRevision: number
hostKeyGeneration: number
remoteUsername: string
}>
agent: Readonly<{
installationId: string
binaryDigest: string
version: string
architecture: 'x64' | 'arm64'
protocolMajor: number
}>
workspace: Readonly<{
canonicalRemoteRoot: string
workspaceIdentity: string
workspaceId: string
generation: number
access: 'read-only'
capabilities: readonly string[]
}>
connection: RemoteAgentConnection
signal: AbortSignal
}>
export type RemoteProjectRuntimeValidationEvidence =
ProjectRuntimeValidation
export interface RemoteProjectRuntimeValidationLease {
readonly evidence: RemoteProjectRuntimeValidationEvidence
assertCurrent(): void
release(): void
}
export interface RemoteProjectRuntimeValidator {
validate(
input: RemoteProjectRuntimeValidationInput
): Promise<RemoteProjectRuntimeValidationLease>
}
export class UnavailableRemoteProjectRuntimeValidator
implements RemoteProjectRuntimeValidator
{
async validate(): Promise<never> {
throw new Error('Remote Runtime validation is unavailable')
}
}
type ActiveSave = {
controller: AbortController
done: Promise<void>
resolveDone: () => void
destroyedListener: () => void
installation?: AgentInstallationIdentity
connection?: RemoteAgentConnection
workspace?: RemoteWorkspaceHandle
runtimeLease?: RemoteProjectRuntimeValidationLease
}
export type RemoteProjectSaveServiceOptions = {
database: Pick<
AssistantDatabase,
| 'getProject'
| 'createValidatedSshProject'
| 'updateValidatedSshProject'
>
sshHosts: Pick<
SshHostStore,
'resolveConnectionTarget' | 'assertConnectionTargetCurrent'
>
installationManager: Pick<AgentInstallationManager, 'ensureInstalled'>
connectionManager: Pick<RemoteAgentConnectionManager, 'acquire'>
resolveRuntimeSelection(
selection: AgentRuntimeSelection
): Promise<AgentRuntimeSelection>
runtimeValidator?: RemoteProjectRuntimeValidator
notify?: (
owner: RemoteProjectSaveOwner,
progress: RemoteProjectSaveProgress
) => void
cleanupTimeoutMs?: number
}
export class RemoteProjectSaveService {
readonly #database: RemoteProjectSaveServiceOptions['database']
readonly #sshHosts: RemoteProjectSaveServiceOptions['sshHosts']
readonly #installationManager: RemoteProjectSaveServiceOptions['installationManager']
readonly #connectionManager: RemoteProjectSaveServiceOptions['connectionManager']
readonly #resolveRuntimeSelection: RemoteProjectSaveServiceOptions['resolveRuntimeSelection']
readonly #runtimeValidator: RemoteProjectRuntimeValidator
readonly #notify?: RemoteProjectSaveServiceOptions['notify']
readonly #cleanupTimeoutMs: number
readonly #active = new Map<RemoteProjectSaveOwner, ActiveSave>()
readonly #activations = new Map<
RemoteProjectSaveOwner,
{ projectId: string; promise: Promise<AssistantProject> }
>()
#disposed = false
constructor(options: RemoteProjectSaveServiceOptions) {
this.#database = options.database
this.#sshHosts = options.sshHosts
this.#installationManager = options.installationManager
this.#connectionManager = options.connectionManager
this.#resolveRuntimeSelection = options.resolveRuntimeSelection
this.#runtimeValidator =
options.runtimeValidator ??
new UnavailableRemoteProjectRuntimeValidator()
this.#notify = options.notify
this.#cleanupTimeoutMs =
options.cleanupTimeoutMs ?? CLEANUP_TIMEOUT_MS
if (
!Number.isSafeInteger(this.#cleanupTimeoutMs) ||
this.#cleanupTimeoutMs < 1
) {
throw new RangeError('Invalid remote project cleanup timeout')
}
}
async save(
owner: RemoteProjectSaveOwner,
input: unknown
): Promise<AssistantProject> {
this.#assertOwnerAvailable(owner)
if (this.#active.has(owner)) {
throw new Error('A remote project save is already in progress')
}
const request = remoteProjectSaveRequestSchema.parse(input)
let resolveDone!: () => void
const active: ActiveSave = {
controller: new AbortController(),
done: new Promise<void>((resolve) => {
resolveDone = resolve
}),
resolveDone,
destroyedListener: () => {
active.controller.abort(
new DOMException('Remote project owner was destroyed', 'AbortError')
)
}
}
this.#active.set(owner, active)
owner.on('destroyed', active.destroyedListener)
try {
return await this.#performSave(owner, active, request)
} finally {
await this.#cleanup(active)
owner.removeListener('destroyed', active.destroyedListener)
if (this.#active.get(owner) === active) {
this.#active.delete(owner)
}
active.resolveDone()
}
}
async activate(
owner: RemoteProjectSaveOwner,
projectId: string
): Promise<AssistantProject> {
this.#assertOwnerAvailable(owner)
const existing = this.#activations.get(owner)
if (existing) {
if (existing.projectId === projectId) {
return await existing.promise
}
throw new Error('A remote project save is already in progress')
}
const project = this.#database.getProject(projectId)
if (
project.kind !== 'user' ||
project.channel !== undefined ||
project.status !== 'active' ||
project.executionSpace.kind !== 'ssh' ||
project.runtimeSelection === undefined
) {
throw new Error('Only active managed SSH projects can be activated')
}
const promise = this.save(owner, {
intent: 'update',
draft: {
projectId: project.id,
name: project.name,
description: project.description,
defaultWorkMode: project.defaultWorkMode,
runtimeSelection: project.runtimeSelection,
hostId: project.executionSpace.hostId,
remoteRootPath: project.executionSpace.remoteRootPath
}
})
this.#activations.set(owner, { projectId, promise })
try {
return await promise
} finally {
if (this.#activations.get(owner)?.promise === promise) {
this.#activations.delete(owner)
}
}
}
cancelCurrent(owner: RemoteProjectSaveOwner): void {
this.#active.get(owner)?.controller.abort(
new DOMException('Remote project save cancelled', 'AbortError')
)
}
async dispose(): Promise<void> {
if (this.#disposed) return
this.#disposed = true
const saves = [...this.#active.entries()]
for (const [, active] of saves) {
active.controller.abort(
new DOMException('Remote project service disposed', 'AbortError')
)
}
await Promise.allSettled(saves.map(([, active]) => active.done))
}
async #performSave(
owner: RemoteProjectSaveOwner,
active: ActiveSave,
request: RemoteProjectSaveRequest
): Promise<AssistantProject> {
const { signal } = active.controller
const expectedUpdatedAt =
request.intent === 'update'
? this.#readUpdateRevision(request)
: undefined
const runtimeSelection = await this.#resolveRuntimeSelection(
request.draft.runtimeSelection
)
signal.throwIfAborted()
this.#progress(owner, 'host')
const target = await this.#sshHosts.resolveConnectionTarget(
request.draft.hostId
)
signal.throwIfAborted()
assertTarget(request.draft.hostId, target)
this.#progress(owner, 'agent')
const installation = await this.#installationManager.ensureInstalled(
request.draft.hostId,
{ signal }
)
signal.throwIfAborted()
assertInstallation(installation)
active.installation = installation
const connection = await this.#connectionManager.acquire(
request.draft.hostId,
{
...installation,
requiredCapabilities: [WORKSPACE_READ_CAPABILITY]
},
signal
)
active.connection = connection
signal.throwIfAborted()
assertConnectionIdentity(target, installation, connection)
this.#progress(owner, 'workspace')
const validated = remoteWorkspaceValidateResultSchema.parse(
await connection.client.request(
'workspace/validate',
{
remoteRootPath: request.draft.remoteRootPath,
requestedAccess: 'read-only',
requiredCapabilities: [...REMOTE_WORKSPACE_READ_CAPABILITIES]
},
{ signal }
)
)
active.workspace = validated.handle
signal.throwIfAborted()
assertWorkspace(validated.handle)
if (
validated.handle.canonicalDisplayPath !==
request.draft.remoteRootPath
) {
throw new Error('Remote workspace path changed during validation')
}
assertCriticalWorkspaceRead(connection)
this.#progress(owner, 'runtime')
const selectionKey = agentRuntimeSelectionKey(
runtimeSelection
)
const runtimeLease = await this.#runtimeValidator.validate({
selection: runtimeSelection,
runtimeSelectionKey: selectionKey,
workMode: request.draft.defaultWorkMode,
host: {
hostId: target.host.id,
hostRevision: target.hostRevision,
hostKeyGeneration: target.hostKeyGeneration,
remoteUsername: target.host.username
},
agent: {
installationId: installation.installationId,
binaryDigest: installation.binaryDigest,
version: installation.agentVersion,
architecture: installation.architecture,
protocolMajor: installation.protocol.major
},
workspace: {
canonicalRemoteRoot: validated.handle.canonicalDisplayPath,
workspaceIdentity: validated.handle.workspaceIdentity,
workspaceId: validated.handle.workspaceId,
generation: validated.handle.generation,
access: 'read-only',
capabilities: validated.handle.capabilities
},
connection,
signal
})
active.runtimeLease = runtimeLease
signal.throwIfAborted()
const runtimeEvidence = projectRuntimeValidationSchema.parse(
runtimeLease.evidence
)
assertRuntimeEvidence(
runtimeEvidence,
selectionKey,
installation.installationId,
request.draft.defaultWorkMode
)
this.#progress(owner, 'saving')
signal.throwIfAborted()
const write = this.#validatedWrite(
request,
runtimeSelection,
target,
installation,
connection,
validated.handle,
runtimeLease,
runtimeEvidence,
validated.validatedAt
)
return request.intent === 'create'
? this.#database.createValidatedSshProject(write)
: this.#database.updateValidatedSshProject(
request.draft.projectId,
expectedUpdatedAt!,
write
)
}
#readUpdateRevision(
request: Extract<RemoteProjectSaveRequest, { intent: 'update' }>
): string {
const project = this.#database.getProject(request.draft.projectId)
if (
project.kind !== 'user' ||
project.channel !== undefined ||
project.executionSpace.kind !== 'ssh' ||
project.executionSpace.hostId !== request.draft.hostId ||
project.executionSpace.remoteRootPath !==
request.draft.remoteRootPath
) {
throw new Error(
'Only an existing user SSH project with the same Host and root can be updated'
)
}
return project.updatedAt
}
#validatedWrite(
request: RemoteProjectSaveRequest,
runtimeSelection: AgentRuntimeSelection,
target: SshConnectionTarget,
installation: AgentInstallationIdentity,
connection: RemoteAgentConnection,
workspace: RemoteWorkspaceHandle,
runtimeLease: RemoteProjectRuntimeValidationLease,
runtime: ProjectRuntimeValidation,
validatedAt: string
): ValidatedSshProjectWrite {
const executionValidation: SshExecutionValidation = {
hostRevision: target.hostRevision,
hostKeyGeneration: target.hostKeyGeneration,
remoteUsername: target.host.username,
workspaceIdentity: workspace.workspaceIdentity,
agentProtocolMajor: installation.protocol.major,
agentInstallationIdAtValidation: installation.installationId,
agentBinaryDigestAtValidation: installation.binaryDigest,
agentVersionAtValidation: installation.agentVersion,
agentArchitectureAtValidation: installation.architecture,
validatedAt
}
return {
project: {
name: request.draft.name,
description: request.draft.description,
rootPath: workspace.canonicalDisplayPath,
defaultWorkMode: request.draft.defaultWorkMode,
runtimeSelection
},
executionSpace: {
kind: 'ssh',
hostId: target.host.id,
remoteRootPath: workspace.canonicalDisplayPath,
validation: executionValidation
},
runtimeValidation: structuredClone(runtime),
assertSshHostCurrent: (expected) => {
assertPrecondition(
expected,
target,
installation,
workspace
)
this.#sshHosts.assertConnectionTargetCurrent(
currentTargetEvidence(target)
)
assertConnectionIdentity(target, installation, connection)
assertCriticalWorkspaceRead(connection)
runtimeLease.assertCurrent()
if (
!isDeepStrictEqual(
projectRuntimeValidationSchema.parse(runtimeLease.evidence),
runtime
)
) {
throw new Error('Remote Runtime validation changed before save')
}
}
}
}
async #cleanup(active: ActiveSave): Promise<void> {
try {
active.runtimeLease?.release()
} catch {
// Continue releasing independently owned resources.
}
const workspace = active.workspace
const connection = active.connection
if (workspace && connection) {
try {
await connection.client.request(
'workspace/close',
{
workspaceId: workspace.workspaceId,
generation: workspace.generation
},
{ signal: AbortSignal.timeout(this.#cleanupTimeoutMs) }
)
} catch {
// The connection may already be offline after cancellation.
}
}
try {
connection?.release()
} catch {
// A concurrently closed connection is already released.
}
}
#progress(
owner: RemoteProjectSaveOwner,
phase: RemoteProjectSavePhase
): void {
if (!this.#notify || owner.isDestroyed()) return
try {
this.#notify(owner, { phase })
} catch {
// Renderer progress observers cannot affect the save.
}
}
#assertOwnerAvailable(owner: RemoteProjectSaveOwner): void {
if (this.#disposed) {
throw new Error('Remote project save service is disposed')
}
if (
!owner ||
!Number.isSafeInteger(owner.id) ||
owner.id < 0 ||
owner.isDestroyed()
) {
throw new Error('Remote project owner is unavailable')
}
}
}
function assertTarget(hostId: string, target: SshConnectionTarget): void {
if (
target.host.id !== hostId ||
!target.host.hostKey ||
target.host.hostKey.generation !== target.hostKeyGeneration ||
target.hostRevision < 1 ||
target.hostKeyGeneration < 1
) {
throw new Error('SSH Host binding is unavailable')
}
}
function assertInstallation(
installation: AgentInstallationIdentity
): void {
if (
!/^[A-Za-z0-9](?:[A-Za-z0-9._:-]{0,127})$/u.test(
installation.installationId
) ||
!sha256DigestSchema.safeParse(installation.binaryDigest).success ||
installation.platform !== 'linux' ||
!['x64', 'arm64'].includes(installation.architecture) ||
installation.supervisor !== 'detached-on-demand'
) {
throw new Error('Remote Agent installation is incompatible')
}
}
function assertConnectionIdentity(
target: SshConnectionTarget,
installation: AgentInstallationIdentity,
connection: RemoteAgentConnection
): void {
if (
connection.state !== 'ready' ||
connection.identity.hostId !== target.host.id ||
connection.identity.hostRevision !== target.hostRevision ||
connection.identity.hostKeyGeneration !== target.hostKeyGeneration ||
connection.identity.remoteUsername !== target.host.username ||
connection.identity.installationId !== installation.installationId ||
connection.identity.binaryDigest !== installation.binaryDigest ||
connection.status.state !== 'ready' ||
connection.status.draining ||
connection.status.installationId !== installation.installationId ||
connection.status.binaryDigest !== installation.binaryDigest
) {
throw new Error('Remote Agent identity is incompatible')
}
}
function assertCriticalWorkspaceRead(
connection: RemoteAgentConnection
): void {
const capability = connection.capabilities.capabilities.find(
(entry) => entry.name === WORKSPACE_READ_CAPABILITY.name
)
if (
!capability ||
capability.version < WORKSPACE_READ_CAPABILITY.minimumVersion ||
!capability.critical
) {
throw new Error('Remote Agent lacks workspace read capability')
}
}
function assertWorkspace(handle: RemoteWorkspaceHandle): void {
if (
handle.access !== 'read-only' ||
REMOTE_WORKSPACE_READ_CAPABILITIES.some(
(capability) => !handle.capabilities.includes(capability)
) ||
handle.capabilities.includes('write-text-atomic') ||
handle.capabilities.includes('apply-change-set')
) {
throw new Error('Remote workspace validation is incompatible')
}
}
function assertRuntimeEvidence(
evidence: ProjectRuntimeValidation,
selectionKey: string,
installationId: string,
workMode: 'ask' | 'execute'
): void {
if (
evidence.runtimeSelectionKey !== selectionKey ||
evidence.agentInstallationIdAtValidation !== installationId ||
evidence.workMode !== workMode
) {
throw new Error('Remote Runtime validation does not match the project')
}
}
function assertPrecondition(
expected: ValidatedSshHostPrecondition,
target: SshConnectionTarget,
installation: AgentInstallationIdentity,
workspace: RemoteWorkspaceHandle
): void {
if (
expected.hostId !== target.host.id ||
expected.hostRevision !== target.hostRevision ||
expected.hostKeyGeneration !== target.hostKeyGeneration ||
expected.remoteUsername !== target.host.username ||
expected.remoteRootPath !== workspace.canonicalDisplayPath ||
expected.workspaceIdentity !== workspace.workspaceIdentity ||
expected.agentProtocolMajor !== installation.protocol.major ||
expected.agentInstallationId !== installation.installationId ||
expected.agentBinaryDigest !== installation.binaryDigest ||
expected.agentVersion !== installation.agentVersion ||
expected.agentArchitecture !== installation.architecture
) {
throw new Error('Validated SSH project precondition changed')
}
}
function currentTargetEvidence(
target: SshConnectionTarget
): CurrentSshConnectionTarget {
return {
hostId: target.host.id,
hostRevision: target.hostRevision,
hostKeyGeneration: target.hostKeyGeneration,
username: target.host.username
}
}