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:
@@ -33,6 +33,10 @@ import {
|
||||
type RemoteRuntimeBundleManifest,
|
||||
type RemoteRuntimeLock
|
||||
} from '../remote-runtime-launch-contracts'
|
||||
import {
|
||||
runtimeRegistryEntrySchema,
|
||||
type RuntimeRegistryEntry
|
||||
} from '../remote-environment-registry-contracts'
|
||||
|
||||
export type InstalledBundleVerificationEnvironment =
|
||||
| 'production'
|
||||
@@ -63,6 +67,11 @@ export type VerifyRuntimeBundleOptions = {
|
||||
uid?: number
|
||||
}
|
||||
|
||||
export type LoadRegisteredRuntimeBundleOptions =
|
||||
VerifyRuntimeBundleOptions & {
|
||||
registered: RuntimeRegistryEntry
|
||||
}
|
||||
|
||||
export async function verifyRuntimeBundle(
|
||||
bundleDirectoryInput: string,
|
||||
options: VerifyRuntimeBundleOptions
|
||||
@@ -179,6 +188,121 @@ export async function verifyRuntimeBundle(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs a Runtime identity that was fully verified when it was
|
||||
* registered. Normal capabilities and prompt launches use this bounded
|
||||
* metadata path instead of hashing the complete Runtime payload again.
|
||||
*/
|
||||
export async function loadRegisteredRuntimeBundle(
|
||||
bundleDirectoryInput: string,
|
||||
options: LoadRegisteredRuntimeBundleOptions
|
||||
): Promise<VerifiedRuntimeBundle> {
|
||||
const bundleDirectory = assertAbsoluteManagedPath(
|
||||
resolve(bundleDirectoryInput)
|
||||
)
|
||||
const registered = runtimeRegistryEntrySchema.parse(
|
||||
options.registered
|
||||
)
|
||||
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'
|
||||
)
|
||||
}
|
||||
if (
|
||||
basename(bundleDirectory) !==
|
||||
registered.bundleDigest.slice('sha256:'.length)
|
||||
) {
|
||||
throw new Error(
|
||||
'Registered Runtime digest does not match its managed directory'
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
options.releaseKeyRegistry,
|
||||
options.verificationEnvironment ?? 'production'
|
||||
)
|
||||
assertRuntimeManifestMatchesLock(
|
||||
manifest,
|
||||
remoteRuntimeLockSchema.parse(options.runtimeLock),
|
||||
options.architecture
|
||||
)
|
||||
const manifestDigest =
|
||||
await digestRemoteRuntimeBundleManifest(manifest)
|
||||
if (
|
||||
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'
|
||||
)
|
||||
}
|
||||
|
||||
const executableRecord = manifest.files.find(
|
||||
(file) => file.path === manifest.entrypoint.path
|
||||
)
|
||||
if (executableRecord === undefined) {
|
||||
throw new Error(
|
||||
'Registered 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(
|
||||
'Registered Runtime entrypoint metadata changed'
|
||||
)
|
||||
}
|
||||
return {
|
||||
bundleDirectory,
|
||||
executablePath,
|
||||
manifest,
|
||||
manifestDigest
|
||||
}
|
||||
}
|
||||
|
||||
export function canonicalRuntimeManifestBytes(
|
||||
manifest: RemoteRuntimeBundleManifest
|
||||
): Buffer {
|
||||
|
||||
@@ -16,7 +16,14 @@ export const installationRegistryEntrySchema = z
|
||||
installationId: installationRegistryIdSchema,
|
||||
agentVersion: componentVersionSchema,
|
||||
manifestSha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
||||
arch: z.enum(['x64', 'arm64'])
|
||||
arch: z.enum(['x64', 'arm64']),
|
||||
protocol: z
|
||||
.object({
|
||||
major: z.number().int().min(0).max(65_535),
|
||||
minor: z.number().int().min(0).max(65_535)
|
||||
})
|
||||
.strict()
|
||||
.optional()
|
||||
})
|
||||
.strict()
|
||||
|
||||
@@ -137,6 +144,7 @@ export const runtimeRegistryEntrySchema = z
|
||||
architecture: z.enum(['x64', 'arm64']),
|
||||
bundleDigest: sha256DigestSchema,
|
||||
manifestDigest: sha256DigestSchema,
|
||||
runtimeAdapterDigest: sha256DigestSchema.optional(),
|
||||
acpCapabilitiesDigest: sha256DigestSchema
|
||||
})
|
||||
.strict()
|
||||
@@ -219,7 +227,10 @@ function canonicalInstallationEntry(
|
||||
installationId: entry.installationId,
|
||||
agentVersion: entry.agentVersion,
|
||||
manifestSha256: entry.manifestSha256,
|
||||
arch: entry.arch
|
||||
arch: entry.arch,
|
||||
...(entry.protocol === undefined
|
||||
? {}
|
||||
: { protocol: entry.protocol })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -233,6 +244,12 @@ function canonicalRuntimeEntry(
|
||||
architecture: entry.architecture,
|
||||
bundleDigest: entry.bundleDigest,
|
||||
manifestDigest: entry.manifestDigest,
|
||||
...(entry.runtimeAdapterDigest === undefined
|
||||
? {}
|
||||
: {
|
||||
runtimeAdapterDigest:
|
||||
entry.runtimeAdapterDigest
|
||||
}),
|
||||
acpCapabilitiesDigest: entry.acpCapabilitiesDigest
|
||||
})
|
||||
}
|
||||
@@ -245,6 +262,7 @@ function installationEntryEquals(
|
||||
left.installationId === right.installationId &&
|
||||
left.agentVersion === right.agentVersion &&
|
||||
left.manifestSha256 === right.manifestSha256 &&
|
||||
left.arch === right.arch
|
||||
left.arch === right.arch &&
|
||||
JSON.stringify(left.protocol) === JSON.stringify(right.protocol)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user