fix: make Host environment updates fast and recoverable
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
Published Host updates could fail while the desktop reverified Runtime against its bundled lock and repeatedly traversed remote payloads. Host preparation now verifies and extracts once, adopts Agent and Runtime on one pinned connection, and uses registered metadata for normal health, capability, and prompt paths. Interrupted commits and adoption retain bounded recovery and rollback state. Desktop and Agent versions are 0.11.8. Remote Projects remain a technical preview; the Agent targets Linux x64 and arm64, with no LoongArch preview. Release note: 修复正式环境 Host 更新失败并移除重复远端包体扫描;中断恢复与清理失败不再破坏已健康环境。
This commit is contained in:
@@ -192,11 +192,13 @@ describe('Agent CLI fixed command contract', () => {
|
||||
state: 'ready' as const,
|
||||
installationId
|
||||
}))
|
||||
const verifyInstallation = vi.fn(async () => verified)
|
||||
const dependencies: AgentCliDependencies = {
|
||||
installationPaths: () => installationPaths,
|
||||
releaseKeyRegistry,
|
||||
installationRegistry,
|
||||
verifyInstallation: async () => verified,
|
||||
verifyInstallation,
|
||||
loadRegisteredInstallation: async () => verified,
|
||||
createLifecycle: (() => ({
|
||||
status,
|
||||
bootstrap
|
||||
@@ -227,6 +229,21 @@ describe('Agent CLI fixed command contract', () => {
|
||||
).toBe(0)
|
||||
expect(status).toHaveBeenCalledOnce()
|
||||
expect(bootstrap).toHaveBeenCalledOnce()
|
||||
expect(
|
||||
installationRegistry.snapshot().current?.installationId
|
||||
).toBe(installationId)
|
||||
|
||||
expect(
|
||||
await runAgentCli(
|
||||
buildFixedAgentCliArgv(installationId, {
|
||||
kind: 'lifecycle',
|
||||
action: 'adopt'
|
||||
}),
|
||||
{ ...dependencies, io: cliIo() }
|
||||
)
|
||||
).toBe(0)
|
||||
expect(verifyInstallation).toHaveBeenCalledOnce()
|
||||
expect(bootstrap).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('dispatches generated attach argv using only derived managed paths', async () => {
|
||||
@@ -469,6 +486,31 @@ describe('Agent CLI fixed command contract', () => {
|
||||
architecture: 'x64',
|
||||
runtimeVersion: '1.18.9'
|
||||
})
|
||||
|
||||
expect(
|
||||
await runAgentCli(
|
||||
buildFixedAgentCliArgv(
|
||||
verifyAgentInstallationId('install-1'),
|
||||
{
|
||||
kind: 'runtime-activate',
|
||||
runtimeId: 'opencode',
|
||||
bundleDigest,
|
||||
architecture: 'x64',
|
||||
forceVerification: true
|
||||
}
|
||||
),
|
||||
{
|
||||
runtimePaths: () => paths,
|
||||
runtimeReleaseKeyRegistry: releaseKeyRegistry,
|
||||
runtimeLock: {} as RemoteRuntimeLock,
|
||||
runtimeRegistry: registry,
|
||||
verifyRuntime,
|
||||
currentArchitecture: () => 'x64',
|
||||
io: cliIo()
|
||||
}
|
||||
)
|
||||
).toBe(0)
|
||||
expect(verifyRuntime).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('derives Runtime paths and rejects activation argv injection', async () => {
|
||||
|
||||
+183
-40
@@ -39,6 +39,7 @@ import {
|
||||
RuntimeBundleRegistry
|
||||
} from './runtime-bundle-registry'
|
||||
import {
|
||||
loadRegisteredRuntimeBundle,
|
||||
readRemoteRuntimeLock,
|
||||
verifyRuntimeBundle,
|
||||
type VerifiedRuntimeBundle
|
||||
@@ -111,6 +112,7 @@ export type ManagedRuntimePaths = {
|
||||
}
|
||||
|
||||
const LIFECYCLE_ACTIONS = new Set([
|
||||
'adopt',
|
||||
'bootstrap',
|
||||
'status',
|
||||
'health',
|
||||
@@ -225,7 +227,8 @@ async function runRuntime(
|
||||
'installation-id',
|
||||
'runtime-id',
|
||||
'bundle-digest',
|
||||
'architecture'
|
||||
'architecture',
|
||||
'force-verification'
|
||||
])
|
||||
requireOptions(options, [
|
||||
'installation-id',
|
||||
@@ -243,6 +246,16 @@ async function runRuntime(
|
||||
const architecture = validateRuntimeArchitecture(
|
||||
options.architecture!
|
||||
)
|
||||
const forceVerification =
|
||||
options['force-verification'] === 'true'
|
||||
if (
|
||||
options['force-verification'] !== undefined &&
|
||||
!forceVerification
|
||||
) {
|
||||
throw new Error(
|
||||
'Invalid Runtime force-verification option'
|
||||
)
|
||||
}
|
||||
const hostArchitecture =
|
||||
dependencies.currentArchitecture?.() ??
|
||||
currentAgentArchitecture()
|
||||
@@ -271,15 +284,30 @@ async function runRuntime(
|
||||
runtimeRoot: paths.runtimeRoot,
|
||||
storagePath: paths.registryPath
|
||||
})
|
||||
const verified = await (
|
||||
dependencies.verifyRuntime ?? verifyRuntimeBundle
|
||||
)(paths.bundleDirectory, {
|
||||
const verificationOptions = {
|
||||
architecture,
|
||||
releaseKeyRegistry,
|
||||
runtimeLock,
|
||||
verificationEnvironment:
|
||||
dependencies.runtimeVerificationEnvironment ?? 'production'
|
||||
})
|
||||
} as const
|
||||
const registered = registry.find(
|
||||
runtimeId,
|
||||
bundleDigest,
|
||||
architecture
|
||||
)
|
||||
const verified =
|
||||
registered === undefined || forceVerification
|
||||
? await (
|
||||
dependencies.verifyRuntime ?? verifyRuntimeBundle
|
||||
)(paths.bundleDirectory, verificationOptions)
|
||||
: await loadRegisteredRuntimeBundle(
|
||||
registered.bundleDirectory,
|
||||
{
|
||||
...verificationOptions,
|
||||
registered: registered.entry
|
||||
}
|
||||
)
|
||||
assertActivatedRuntimeIdentity(
|
||||
verified,
|
||||
runtimeId,
|
||||
@@ -352,7 +380,8 @@ async function runDaemon(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies,
|
||||
'registered'
|
||||
'registered',
|
||||
verified
|
||||
).recordCurrentDaemonReady(daemon.status().daemonBootId)
|
||||
await (dependencies.waitForShutdown?.() ?? waitForShutdownSignal())
|
||||
} finally {
|
||||
@@ -374,7 +403,7 @@ async function runAttach(
|
||||
installationId,
|
||||
dependencies
|
||||
)
|
||||
await loadManagedInstallation(
|
||||
const { verified } = await loadManagedInstallation(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies
|
||||
@@ -399,7 +428,8 @@ async function runAttach(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies,
|
||||
'registered'
|
||||
'registered',
|
||||
verified
|
||||
).bootstrap()
|
||||
}
|
||||
})
|
||||
@@ -451,10 +481,17 @@ async function runDoctor(
|
||||
options['installation-id']!
|
||||
)
|
||||
const paths = resolveInstallationPaths(installationId, dependencies)
|
||||
const status = await createDetachedLifecycle(
|
||||
const { verified } = await loadManagedInstallation(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies
|
||||
)
|
||||
const status = await createDetachedLifecycle(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies,
|
||||
'registered',
|
||||
verified
|
||||
).status()
|
||||
io.output.write(`${JSON.stringify({
|
||||
platform: process.platform,
|
||||
@@ -479,48 +516,150 @@ async function runLifecycle(
|
||||
options['installation-id']!
|
||||
)
|
||||
const paths = resolveInstallationPaths(installationId, dependencies)
|
||||
const { verified, registry } = await verifyManagedInstallation(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies
|
||||
)
|
||||
const lifecycle = createDetachedLifecycle(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies
|
||||
)
|
||||
switch (action) {
|
||||
case 'bootstrap': {
|
||||
registry.stageCandidate(verified)
|
||||
prepareManagedSocketDirectory(paths)
|
||||
const status = await lifecycle.bootstrap()
|
||||
io.output.write(`${JSON.stringify(status)}\n`)
|
||||
break
|
||||
}
|
||||
case 'status': {
|
||||
registry.assertVerifiedRole(verified, ['current', 'candidate'])
|
||||
io.output.write(`${JSON.stringify(await lifecycle.status())}\n`)
|
||||
break
|
||||
}
|
||||
case 'health': {
|
||||
registry.assertVerifiedRole(verified, ['current', 'candidate'])
|
||||
const status = await lifecycle.health()
|
||||
if (
|
||||
registry.snapshot().candidate?.installationId ===
|
||||
case 'adopt': {
|
||||
const { verified, registry } =
|
||||
await verifyManagedInstallation(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies
|
||||
)
|
||||
const alreadyCurrent =
|
||||
registry.snapshot().current?.installationId ===
|
||||
installationId
|
||||
) {
|
||||
if (!alreadyCurrent) {
|
||||
registry.stageCandidate(verified)
|
||||
}
|
||||
prepareManagedSocketDirectory(paths)
|
||||
const status = await createDetachedLifecycle(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies,
|
||||
'verified',
|
||||
verified
|
||||
).bootstrap()
|
||||
if (!alreadyCurrent) {
|
||||
registry.promoteCandidate(installationId)
|
||||
}
|
||||
io.output.write(`${JSON.stringify(status)}\n`)
|
||||
break
|
||||
}
|
||||
case 'bootstrap': {
|
||||
const agentRoot = dirname(
|
||||
dirname(dirname(paths.executablePath))
|
||||
)
|
||||
const registry =
|
||||
dependencies.installationRegistry ??
|
||||
new InstallationRegistry({
|
||||
storagePath: resolve(agentRoot, 'registry.json')
|
||||
})
|
||||
const snapshot = registry.snapshot()
|
||||
const current = snapshot.current?.installationId ===
|
||||
installationId
|
||||
? snapshot.current
|
||||
: undefined
|
||||
const candidate = snapshot.candidate?.installationId ===
|
||||
installationId
|
||||
? snapshot.candidate
|
||||
: undefined
|
||||
const registered = current ?? candidate
|
||||
const isNewInstallation = registered === undefined
|
||||
const verified = isNewInstallation
|
||||
? (
|
||||
await verifyManagedInstallation(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies
|
||||
)
|
||||
).verified
|
||||
: await (
|
||||
dependencies.loadRegisteredInstallation ??
|
||||
loadRegisteredAgentBundle
|
||||
)(dirname(paths.executablePath), {
|
||||
installationId,
|
||||
architecture: currentAgentArchitecture(),
|
||||
registered
|
||||
})
|
||||
if (isNewInstallation) {
|
||||
registry.stageCandidate(verified)
|
||||
}
|
||||
prepareManagedSocketDirectory(paths)
|
||||
const lifecycle = createDetachedLifecycle(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies,
|
||||
'verified',
|
||||
verified
|
||||
)
|
||||
const status = await lifecycle.bootstrap()
|
||||
if (current === undefined) {
|
||||
registry.promoteCandidate(installationId)
|
||||
}
|
||||
io.output.write(`${JSON.stringify(status)}\n`)
|
||||
break
|
||||
}
|
||||
case 'status': {
|
||||
const { verified } = await loadManagedInstallation(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies
|
||||
)
|
||||
const lifecycle = createDetachedLifecycle(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies,
|
||||
'registered',
|
||||
verified
|
||||
)
|
||||
io.output.write(`${JSON.stringify(await lifecycle.status())}\n`)
|
||||
break
|
||||
}
|
||||
case 'health': {
|
||||
const { verified } = await loadManagedInstallation(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies
|
||||
)
|
||||
const lifecycle = createDetachedLifecycle(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies,
|
||||
'registered',
|
||||
verified
|
||||
)
|
||||
const status = await lifecycle.health()
|
||||
io.output.write(`${JSON.stringify(status)}\n`)
|
||||
break
|
||||
}
|
||||
case 'stop': {
|
||||
registry.assertVerifiedRole(verified, ['current', 'candidate'])
|
||||
const { verified } = await loadManagedInstallation(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies
|
||||
)
|
||||
const lifecycle = createDetachedLifecycle(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies,
|
||||
'registered',
|
||||
verified
|
||||
)
|
||||
io.output.write(`${JSON.stringify(await lifecycle.stop())}\n`)
|
||||
break
|
||||
}
|
||||
case 'retire': {
|
||||
registry.assertVerifiedRole(verified, ['current', 'candidate'])
|
||||
const { verified } = await loadManagedInstallation(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies
|
||||
)
|
||||
const lifecycle = createDetachedLifecycle(
|
||||
installationId,
|
||||
paths,
|
||||
dependencies,
|
||||
'registered',
|
||||
verified
|
||||
)
|
||||
io.output.write(`${JSON.stringify(await lifecycle.retire())}\n`)
|
||||
break
|
||||
}
|
||||
@@ -531,7 +670,8 @@ function createDetachedLifecycle(
|
||||
installationId: string,
|
||||
paths: ManagedInstallationPaths,
|
||||
dependencies: AgentCliDependencies,
|
||||
identitySource: 'verified' | 'registered' = 'verified'
|
||||
identitySource: 'verified' | 'registered' = 'verified',
|
||||
verifiedInstallation?: VerifiedInstalledAgentBundle
|
||||
): DetachedAgentLifecycle {
|
||||
const options: DetachedAgentLifecycleOptions = {
|
||||
installationId,
|
||||
@@ -539,6 +679,9 @@ function createDetachedLifecycle(
|
||||
stateDirectory: paths.stateDirectory,
|
||||
socketPath: paths.socketPath,
|
||||
verifyInstallation: async () => {
|
||||
if (verifiedInstallation !== undefined) {
|
||||
return verifiedInstallation
|
||||
}
|
||||
if (identitySource === 'registered') {
|
||||
return (
|
||||
await loadManagedInstallation(
|
||||
|
||||
@@ -116,7 +116,8 @@ describe('Agent installation registry', () => {
|
||||
installationId: 'install-a',
|
||||
agentVersion: '0.11.0',
|
||||
manifestSha256: 'a'.repeat(64),
|
||||
arch: 'x64'
|
||||
arch: 'x64',
|
||||
protocol: { major: 1, minor: 0 }
|
||||
}
|
||||
})
|
||||
expect(readFileSync(storagePath, 'utf8')).toBe(legacyBytes)
|
||||
|
||||
@@ -221,7 +221,8 @@ function entryFromVerified(
|
||||
installationId: verified.installationId,
|
||||
agentVersion: verified.manifest.agentVersion,
|
||||
manifestSha256: verified.manifestSha256,
|
||||
arch: verified.manifest.arch
|
||||
arch: verified.manifest.arch,
|
||||
protocol: verified.manifest.protocol
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -310,7 +310,16 @@ async function loadRegisteredAgentBundleUnchecked(
|
||||
manifestBytes
|
||||
) ||
|
||||
manifest.agentVersion !== registered.agentVersion ||
|
||||
manifest.arch !== registered.arch
|
||||
manifest.arch !== registered.arch ||
|
||||
(
|
||||
registered.protocol !== undefined &&
|
||||
(
|
||||
manifest.protocol.major !==
|
||||
registered.protocol.major ||
|
||||
manifest.protocol.minor !==
|
||||
registered.protocol.minor
|
||||
)
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'Agent manifest does not match the Host-managed registry'
|
||||
|
||||
@@ -268,7 +268,7 @@ function harness(input: {
|
||||
const backend = new RuntimeAcpBackend({
|
||||
journal,
|
||||
resolveRuntimeBundle: vi.fn(async () => resolved),
|
||||
reverifyRuntimeBundle: vi.fn(async () => verifiedBundle()),
|
||||
loadRegisteredRuntimeBundle: vi.fn(async () => verifiedBundle()),
|
||||
resolveWorkspace: vi.fn(async (preparation) => ({
|
||||
workspaceIdentity: preparation.workspaceIdentity,
|
||||
workspaceDirectory: '/workspace',
|
||||
|
||||
@@ -227,7 +227,7 @@ export type RuntimeAcpBackendOptions = {
|
||||
runtimeId: string,
|
||||
runtimeBundleDigest: string
|
||||
): RuntimeAcpResolvedBundle | Promise<RuntimeAcpResolvedBundle>
|
||||
reverifyRuntimeBundle(
|
||||
loadRegisteredRuntimeBundle(
|
||||
resolved: RuntimeAcpResolvedBundle,
|
||||
preparation: RemotePromptOperationPreparation
|
||||
): RuntimeAcpVerifiedBundle | Promise<RuntimeAcpVerifiedBundle>
|
||||
@@ -947,7 +947,7 @@ export class RuntimeAcpBackend {
|
||||
|
||||
let verified: RuntimeAcpVerifiedBundle
|
||||
try {
|
||||
verified = await this.#options.reverifyRuntimeBundle(
|
||||
verified = await this.#options.loadRegisteredRuntimeBundle(
|
||||
binding.resolvedBundle,
|
||||
preparation
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from './managed-paths'
|
||||
import type { InstalledBundleVerificationEnvironment } from './installed-bundle-verifier'
|
||||
import {
|
||||
verifyRuntimeBundle,
|
||||
loadRegisteredRuntimeBundle,
|
||||
type VerifiedRuntimeBundle
|
||||
} from './runtime-bundle-verifier'
|
||||
|
||||
@@ -120,6 +120,27 @@ export class RuntimeBundleRegistry {
|
||||
entry: RuntimeRegistryEntry
|
||||
bundleDirectory: string
|
||||
} {
|
||||
const resolved = this.find(
|
||||
runtimeIdInput,
|
||||
bundleDigestInput,
|
||||
architecture
|
||||
)
|
||||
if (resolved === undefined) {
|
||||
throw new Error(
|
||||
'Runtime bundle is not the verified current bundle'
|
||||
)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
find(
|
||||
runtimeIdInput: string,
|
||||
bundleDigestInput: string,
|
||||
architecture: AgentArchitecture
|
||||
): {
|
||||
entry: RuntimeRegistryEntry
|
||||
bundleDirectory: string
|
||||
} | undefined {
|
||||
const runtimeId = agentIdentifierSchema.parse(runtimeIdInput)
|
||||
const bundleDigest = sha256DigestSchema.parse(bundleDigestInput)
|
||||
this.#state = this.#load({ createIfMissing: false })
|
||||
@@ -130,9 +151,7 @@ export class RuntimeBundleRegistry {
|
||||
candidate.architecture === architecture
|
||||
)
|
||||
if (entry === undefined) {
|
||||
throw new Error(
|
||||
'Runtime bundle is not the verified current bundle'
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
entry: { ...entry },
|
||||
@@ -231,7 +250,7 @@ export class RuntimeBundleRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
export function createVerifiedRuntimeCapabilitySource(options: {
|
||||
type VerifiedRuntimeCapabilitySourceOptions = {
|
||||
registry: RuntimeBundleRegistry
|
||||
architecture: AgentArchitecture
|
||||
releaseKeyRegistry: AgentReleaseKeyRegistry
|
||||
@@ -241,53 +260,65 @@ export function createVerifiedRuntimeCapabilitySource(options: {
|
||||
filesystemPlatform?: NodeJS.Platform
|
||||
uid?: number
|
||||
reportError?: (message: string, error: unknown) => void
|
||||
}): () => Promise<DaemonCapabilities['runtimes']> {
|
||||
loadRegistered?: (
|
||||
entry: RuntimeRegistryEntry,
|
||||
bundleDirectory: string
|
||||
) => Promise<VerifiedRuntimeBundle>
|
||||
}
|
||||
|
||||
export function createVerifiedRuntimeCapabilitySource(
|
||||
options: VerifiedRuntimeCapabilitySourceOptions
|
||||
): () => Promise<DaemonCapabilities['runtimes']> {
|
||||
return async () => await loadVerifiedRuntimeCapabilities(options)
|
||||
}
|
||||
|
||||
async function loadVerifiedRuntimeCapabilities(options: {
|
||||
registry: RuntimeBundleRegistry
|
||||
architecture: AgentArchitecture
|
||||
releaseKeyRegistry: AgentReleaseKeyRegistry
|
||||
runtimeLock: RemoteRuntimeLock
|
||||
verificationEnvironment?: InstalledBundleVerificationEnvironment
|
||||
enforceFilesystemMode?: boolean
|
||||
filesystemPlatform?: NodeJS.Platform
|
||||
uid?: number
|
||||
reportError?: (message: string, error: unknown) => void
|
||||
}): Promise<DaemonCapabilities['runtimes']> {
|
||||
async function loadVerifiedRuntimeCapabilities(
|
||||
options: VerifiedRuntimeCapabilitySourceOptions
|
||||
): Promise<DaemonCapabilities['runtimes']> {
|
||||
const capabilities: DaemonCapabilities['runtimes'] = []
|
||||
for (const entry of options.registry.current(options.architecture)) {
|
||||
try {
|
||||
const verified = await verifyRuntimeBundle(
|
||||
const bundleDirectory =
|
||||
options.registry.bundleDirectory(
|
||||
entry.runtimeId,
|
||||
entry.bundleDigest
|
||||
),
|
||||
{
|
||||
architecture: options.architecture,
|
||||
releaseKeyRegistry: options.releaseKeyRegistry,
|
||||
runtimeLock: options.runtimeLock,
|
||||
...(options.verificationEnvironment === undefined
|
||||
? {}
|
||||
: {
|
||||
verificationEnvironment:
|
||||
options.verificationEnvironment
|
||||
}),
|
||||
...(options.enforceFilesystemMode === undefined
|
||||
? {}
|
||||
: {
|
||||
enforceFilesystemMode:
|
||||
options.enforceFilesystemMode
|
||||
}),
|
||||
...(options.filesystemPlatform === undefined
|
||||
? {}
|
||||
: {
|
||||
filesystemPlatform: options.filesystemPlatform
|
||||
}),
|
||||
...(options.uid === undefined ? {} : { uid: options.uid })
|
||||
}
|
||||
)
|
||||
)
|
||||
const verified = options.loadRegistered === undefined
|
||||
? await loadRegisteredRuntimeBundle(
|
||||
bundleDirectory,
|
||||
{
|
||||
registered: entry,
|
||||
architecture: options.architecture,
|
||||
releaseKeyRegistry:
|
||||
options.releaseKeyRegistry,
|
||||
runtimeLock: options.runtimeLock,
|
||||
...(options.verificationEnvironment === undefined
|
||||
? {}
|
||||
: {
|
||||
verificationEnvironment:
|
||||
options.verificationEnvironment
|
||||
}),
|
||||
...(options.enforceFilesystemMode === undefined
|
||||
? {}
|
||||
: {
|
||||
enforceFilesystemMode:
|
||||
options.enforceFilesystemMode
|
||||
}),
|
||||
...(options.filesystemPlatform === undefined
|
||||
? {}
|
||||
: {
|
||||
filesystemPlatform:
|
||||
options.filesystemPlatform
|
||||
}),
|
||||
...(options.uid === undefined
|
||||
? {}
|
||||
: { uid: options.uid })
|
||||
}
|
||||
)
|
||||
: await options.loadRegistered(
|
||||
entry,
|
||||
bundleDirectory
|
||||
)
|
||||
if (
|
||||
verified.manifest.runtimeVersion !== entry.runtimeVersion ||
|
||||
verified.manifestDigest !== entry.manifestDigest ||
|
||||
@@ -326,6 +357,7 @@ function entryFromVerified(
|
||||
architecture: verified.manifest.architecture,
|
||||
bundleDigest: verified.manifest.bundleDigest,
|
||||
manifestDigest: verified.manifestDigest,
|
||||
runtimeAdapterDigest: verified.manifest.adapterDigest,
|
||||
acpCapabilitiesDigest:
|
||||
verified.manifest.acpCapabilitiesDigest
|
||||
})
|
||||
|
||||
@@ -155,7 +155,7 @@ describe('Remote Runtime bundle verifier', () => {
|
||||
).rejects.toThrow(/managed directory/iu)
|
||||
})
|
||||
|
||||
it('advertises only a bundle that still verifies against its registry', async () => {
|
||||
it('uses registered metadata without rereading Runtime payload files', async () => {
|
||||
const fixture = await createBundle()
|
||||
const runtimeRoot = dirname(dirname(fixture.bundleDirectory))
|
||||
const registry = new RuntimeBundleRegistry({
|
||||
@@ -198,6 +198,18 @@ describe('Remote Runtime bundle verifier', () => {
|
||||
join(fixture.bundleDirectory, 'licenses', 'opencode.txt'),
|
||||
'mutated'
|
||||
)
|
||||
await expect(capabilities()).resolves.toHaveLength(1)
|
||||
expect(reportError).not.toHaveBeenCalled()
|
||||
|
||||
writeFileSync(
|
||||
join(fixture.bundleDirectory, 'bin', 'opencode'),
|
||||
Buffer.concat([
|
||||
readFileSync(
|
||||
join(fixture.bundleDirectory, 'bin', 'opencode')
|
||||
),
|
||||
Buffer.from('changed')
|
||||
])
|
||||
)
|
||||
await expect(capabilities()).resolves.toEqual([])
|
||||
expect(reportError).toHaveBeenCalledWith(
|
||||
'Verified Runtime unavailable: opencode',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export {
|
||||
canonicalRuntimeManifestBytes,
|
||||
loadRegisteredRuntimeBundle,
|
||||
readRemoteRuntimeLock,
|
||||
runtimeManifestSignaturePayload,
|
||||
verifyRuntimeBundle,
|
||||
@@ -8,5 +9,6 @@ export {
|
||||
|
||||
export type {
|
||||
VerifiedRuntimeBundle,
|
||||
LoadRegisteredRuntimeBundleOptions,
|
||||
VerifyRuntimeBundleOptions
|
||||
} from '../shared/node/runtime-bundle-verifier'
|
||||
|
||||
@@ -37,7 +37,7 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('production Runtime composition', () => {
|
||||
it('keeps ACP live and reloads managed metadata installed after daemon startup', async () => {
|
||||
it('loads metadata installed after startup and caches the registered Runtime identity', async () => {
|
||||
const fixture = await createRuntimeBundleTestFixture()
|
||||
const root = fixture.root
|
||||
temporaryPaths.push(root)
|
||||
@@ -121,11 +121,8 @@ describe('production Runtime composition', () => {
|
||||
}, null, 2)}\n`,
|
||||
{ mode: 0o600 }
|
||||
)
|
||||
await expect(protocol.runtimes()).resolves.toEqual([])
|
||||
expect(reportError).toHaveBeenCalledWith(
|
||||
'Verified Runtime unavailable: opencode',
|
||||
expect.any(Error)
|
||||
)
|
||||
await expect(protocol.runtimes()).resolves.toHaveLength(1)
|
||||
expect(reportError).not.toHaveBeenCalled()
|
||||
|
||||
await expect(protocol.dispose()).resolves.toBeUndefined()
|
||||
events.close()
|
||||
|
||||
@@ -7,6 +7,9 @@ import type {
|
||||
AgentReleaseKeyRegistry
|
||||
} from '../shared/agent-installation-contracts'
|
||||
import type { AgentFrame } from '../shared/agent-protocol'
|
||||
import type {
|
||||
RuntimeRegistryEntry
|
||||
} from '../shared/remote-environment-registry-contracts'
|
||||
import type { RemoteRuntimeLock } from '../shared/remote-runtime-launch-contracts'
|
||||
import type { EventJournal } from './event-journal'
|
||||
import {
|
||||
@@ -22,7 +25,8 @@ import {
|
||||
} from './runtime-bundle-registry'
|
||||
import {
|
||||
readRemoteRuntimeLock,
|
||||
verifyRuntimeBundle
|
||||
loadRegisteredRuntimeBundle,
|
||||
type VerifiedRuntimeBundle
|
||||
} from './runtime-bundle-verifier'
|
||||
import { RuntimeAcpBackend } from './runtime-acp-backend'
|
||||
import type { ProtocolMethodContext } from './protocol-server'
|
||||
@@ -111,37 +115,90 @@ export async function createProductionRuntimeProtocol(
|
||||
}
|
||||
return registry
|
||||
}
|
||||
const currentVerificationOptions = async () => {
|
||||
const metadataOptions =
|
||||
options.filesystemPlatform === undefined
|
||||
? {}
|
||||
: { filesystemPlatform: options.filesystemPlatform }
|
||||
const [releaseKeyRegistry, runtimeLock] = await Promise.all([
|
||||
options.releaseKeyRegistry ??
|
||||
readManagedAgentReleaseKeyRegistry(
|
||||
join(runtimeRoot, 'release-keys.json'),
|
||||
metadataOptions
|
||||
),
|
||||
options.runtimeLock ??
|
||||
readRemoteRuntimeLock(
|
||||
join(runtimeRoot, 'remote-runtime-lock.json'),
|
||||
metadataOptions
|
||||
)
|
||||
])
|
||||
return {
|
||||
architecture: options.architecture,
|
||||
releaseKeyRegistry,
|
||||
runtimeLock,
|
||||
verificationEnvironment,
|
||||
...(options.filesystemPlatform === undefined
|
||||
? {}
|
||||
: { filesystemPlatform: options.filesystemPlatform })
|
||||
} as const
|
||||
let verificationOptionsPromise:
|
||||
| Promise<{
|
||||
architecture: AgentArchitecture
|
||||
releaseKeyRegistry: AgentReleaseKeyRegistry
|
||||
runtimeLock: RemoteRuntimeLock
|
||||
verificationEnvironment: 'production' | 'test'
|
||||
filesystemPlatform?: NodeJS.Platform
|
||||
}>
|
||||
| undefined
|
||||
const currentVerificationOptions = () => {
|
||||
verificationOptionsPromise ??= (async () => {
|
||||
const metadataOptions =
|
||||
options.filesystemPlatform === undefined
|
||||
? {}
|
||||
: {
|
||||
filesystemPlatform:
|
||||
options.filesystemPlatform
|
||||
}
|
||||
const [releaseKeyRegistry, runtimeLock] =
|
||||
await Promise.all([
|
||||
options.releaseKeyRegistry ??
|
||||
readManagedAgentReleaseKeyRegistry(
|
||||
join(runtimeRoot, 'release-keys.json'),
|
||||
metadataOptions
|
||||
),
|
||||
options.runtimeLock ??
|
||||
readRemoteRuntimeLock(
|
||||
join(
|
||||
runtimeRoot,
|
||||
'remote-runtime-lock.json'
|
||||
),
|
||||
metadataOptions
|
||||
)
|
||||
])
|
||||
return {
|
||||
architecture: options.architecture,
|
||||
releaseKeyRegistry,
|
||||
runtimeLock,
|
||||
verificationEnvironment,
|
||||
...(options.filesystemPlatform === undefined
|
||||
? {}
|
||||
: {
|
||||
filesystemPlatform:
|
||||
options.filesystemPlatform
|
||||
})
|
||||
}
|
||||
})()
|
||||
void verificationOptionsPromise.catch(() => {
|
||||
verificationOptionsPromise = undefined
|
||||
})
|
||||
return verificationOptionsPromise
|
||||
}
|
||||
const verifiedRuntimeCache =
|
||||
new Map<string, Promise<VerifiedRuntimeBundle>>()
|
||||
const loadRegistered = (
|
||||
entry: RuntimeRegistryEntry,
|
||||
bundleDirectory: string
|
||||
): Promise<VerifiedRuntimeBundle> => {
|
||||
const key = `${bundleDirectory}\0${JSON.stringify(entry)}`
|
||||
const cached = verifiedRuntimeCache.get(key)
|
||||
if (cached !== undefined) {
|
||||
return cached
|
||||
}
|
||||
const loading = (async () =>
|
||||
await loadRegisteredRuntimeBundle(
|
||||
bundleDirectory,
|
||||
{
|
||||
...(await currentVerificationOptions()),
|
||||
registered: entry
|
||||
}
|
||||
))()
|
||||
verifiedRuntimeCache.set(key, loading)
|
||||
void loading.catch(() => {
|
||||
if (verifiedRuntimeCache.get(key) === loading) {
|
||||
verifiedRuntimeCache.delete(key)
|
||||
}
|
||||
})
|
||||
return loading
|
||||
}
|
||||
const verifiedCapabilities = async () => {
|
||||
const source = createVerifiedRuntimeCapabilitySource({
|
||||
registry: currentRegistry(),
|
||||
...(await currentVerificationOptions()),
|
||||
loadRegistered,
|
||||
reportError: options.reportError
|
||||
})
|
||||
return await source()
|
||||
@@ -182,10 +239,10 @@ export async function createProductionRuntimeProtocol(
|
||||
bundleDigest,
|
||||
options.architecture
|
||||
),
|
||||
reverifyRuntimeBundle: async (resolved) => {
|
||||
const verified = await verifyRuntimeBundle(
|
||||
resolved.bundleDirectory,
|
||||
await currentVerificationOptions()
|
||||
loadRegisteredRuntimeBundle: async (resolved) => {
|
||||
const verified = await loadRegistered(
|
||||
resolved.entry,
|
||||
resolved.bundleDirectory
|
||||
)
|
||||
if (
|
||||
verified.manifestDigest !== resolved.entry.manifestDigest ||
|
||||
|
||||
Reference in New Issue
Block a user