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 工作和同步未确认输出。
235 lines
6.2 KiB
TypeScript
235 lines
6.2 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
import { dirname, resolve } from 'node:path'
|
|
import {
|
|
installationRegistryEntrySchema,
|
|
installationRegistryIdSchema,
|
|
installationRegistryStateSchema,
|
|
parseInstallationRegistryState,
|
|
type InstallationRegistryEntry,
|
|
type InstallationRegistryState
|
|
} from '../shared/remote-environment-registry-contracts'
|
|
import {
|
|
ensurePrivateDirectory,
|
|
assertPrivateRegularFile,
|
|
writePrivateFileAtomic
|
|
} from './managed-paths'
|
|
import type { VerifiedInstalledAgentBundle } from './installed-bundle-verifier'
|
|
|
|
export {
|
|
installationRegistryEntrySchema,
|
|
installationRegistryIdSchema,
|
|
installationRegistryStateSchema
|
|
}
|
|
export type {
|
|
InstallationRegistryEntry,
|
|
InstallationRegistryState
|
|
}
|
|
|
|
export class InstallationRegistry {
|
|
readonly #storagePath: string
|
|
#state: InstallationRegistryState
|
|
#needsCanonicalization = false
|
|
|
|
constructor(options: { storagePath: string }) {
|
|
this.#storagePath = resolve(options.storagePath)
|
|
ensurePrivateDirectory(dirname(this.#storagePath))
|
|
this.#state = this.#load()
|
|
}
|
|
|
|
snapshot(): InstallationRegistryState {
|
|
return structuredClone(this.#state)
|
|
}
|
|
|
|
stageCandidate(
|
|
verified: VerifiedInstalledAgentBundle
|
|
): InstallationRegistryEntry {
|
|
const entry = entryFromVerified(verified)
|
|
if (
|
|
this.#state.current !== undefined &&
|
|
entryEquals(this.#state.current, entry)
|
|
) {
|
|
this.#canonicalizeIfNeeded()
|
|
return this.#state.current
|
|
}
|
|
if (
|
|
this.#state.candidate !== undefined &&
|
|
entryEquals(this.#state.candidate, entry)
|
|
) {
|
|
this.#canonicalizeIfNeeded()
|
|
return this.#state.candidate
|
|
}
|
|
try {
|
|
this.#commit({ ...this.#state, candidate: entry })
|
|
} catch (error) {
|
|
if (!isConcurrentRegistryError(error)) {
|
|
throw error
|
|
}
|
|
this.#state = this.#load()
|
|
const converged = [
|
|
this.#state.current,
|
|
this.#state.candidate
|
|
].find(
|
|
(candidate) =>
|
|
candidate !== undefined && entryEquals(candidate, entry)
|
|
)
|
|
if (converged === undefined) {
|
|
throw error
|
|
}
|
|
return converged
|
|
}
|
|
return entry
|
|
}
|
|
|
|
promoteCandidate(
|
|
installationIdInput: string
|
|
): InstallationRegistryState {
|
|
const installationId = installationRegistryIdSchema.parse(
|
|
installationIdInput
|
|
)
|
|
const candidate = this.#state.candidate
|
|
if (
|
|
candidate === undefined ||
|
|
candidate.installationId !== installationId
|
|
) {
|
|
throw new Error('Agent candidate is not staged for promotion')
|
|
}
|
|
this.#commit({
|
|
formatVersion: 1,
|
|
current: candidate
|
|
})
|
|
return this.snapshot()
|
|
}
|
|
|
|
assertVerifiedRole(
|
|
verified: VerifiedInstalledAgentBundle,
|
|
roles: readonly ('current' | 'candidate')[]
|
|
): InstallationRegistryEntry {
|
|
const entry = entryFromVerified(verified)
|
|
const registered = roles
|
|
.map((role) => this.#state[role])
|
|
.find(
|
|
(candidate) =>
|
|
candidate !== undefined &&
|
|
candidate.installationId === entry.installationId
|
|
)
|
|
if (
|
|
registered === undefined ||
|
|
!entryEquals(registered, entry)
|
|
) {
|
|
throw new Error(
|
|
'Verified Agent installation does not match its authorized registry role'
|
|
)
|
|
}
|
|
return registered
|
|
}
|
|
|
|
#load(): InstallationRegistryState {
|
|
let value: unknown
|
|
try {
|
|
assertPrivateRegularFile(this.#storagePath)
|
|
value = JSON.parse(readFileSync(this.#storagePath, 'utf8'))
|
|
} catch (error) {
|
|
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
const initial: InstallationRegistryState = {
|
|
formatVersion: 1
|
|
}
|
|
writePrivateFileAtomic(
|
|
this.#storagePath,
|
|
`${JSON.stringify(initial, null, 2)}\n`
|
|
)
|
|
return initial
|
|
}
|
|
throw new Error('Agent installation registry is corrupt', {
|
|
cause: error
|
|
})
|
|
}
|
|
let parsed: InstallationRegistryState
|
|
try {
|
|
parsed = parseInstallationRegistryState(value)
|
|
} catch (error) {
|
|
throw new Error(
|
|
'Agent installation registry is corrupt',
|
|
{ cause: error }
|
|
)
|
|
}
|
|
this.#needsCanonicalization =
|
|
JSON.stringify(parsed) !== JSON.stringify(value)
|
|
return parsed
|
|
}
|
|
|
|
#commit(stateInput: InstallationRegistryState): void {
|
|
const state = installationRegistryStateSchema.parse(stateInput)
|
|
let persisted: unknown
|
|
try {
|
|
assertPrivateRegularFile(this.#storagePath)
|
|
persisted = JSON.parse(readFileSync(this.#storagePath, 'utf8'))
|
|
} catch (error) {
|
|
throw new Error(
|
|
'Agent installation registry changed or became corrupt before commit',
|
|
{ cause: error }
|
|
)
|
|
}
|
|
let parsedPersisted: InstallationRegistryState
|
|
try {
|
|
parsedPersisted = parseInstallationRegistryState(persisted)
|
|
} catch {
|
|
throw new Error(
|
|
'Agent installation registry changed or became corrupt before commit'
|
|
)
|
|
}
|
|
if (
|
|
JSON.stringify(parsedPersisted) !== JSON.stringify(this.#state)
|
|
) {
|
|
throw new Error(
|
|
'Agent installation registry changed concurrently; refusing to overwrite it'
|
|
)
|
|
}
|
|
writePrivateFileAtomic(
|
|
this.#storagePath,
|
|
`${JSON.stringify(state, null, 2)}\n`
|
|
)
|
|
this.#state = state
|
|
this.#needsCanonicalization = false
|
|
}
|
|
|
|
#canonicalizeIfNeeded(): void {
|
|
if (this.#needsCanonicalization) {
|
|
this.#commit(this.#state)
|
|
}
|
|
}
|
|
}
|
|
|
|
function entryFromVerified(
|
|
verified: VerifiedInstalledAgentBundle
|
|
): InstallationRegistryEntry {
|
|
return installationRegistryEntrySchema.parse({
|
|
installationId: verified.installationId,
|
|
agentVersion: verified.manifest.agentVersion,
|
|
manifestSha256: verified.manifestSha256,
|
|
arch: verified.manifest.arch
|
|
})
|
|
}
|
|
|
|
function entryEquals(
|
|
left: InstallationRegistryEntry,
|
|
right: InstallationRegistryEntry
|
|
): boolean {
|
|
return (
|
|
left.installationId === right.installationId &&
|
|
left.agentVersion === right.agentVersion &&
|
|
left.manifestSha256 === right.manifestSha256 &&
|
|
left.arch === right.arch
|
|
)
|
|
}
|
|
|
|
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
return error instanceof Error && 'code' in error
|
|
}
|
|
|
|
function isConcurrentRegistryError(error: unknown): boolean {
|
|
return (
|
|
error instanceof Error &&
|
|
error.message.includes('changed concurrently')
|
|
)
|
|
}
|