mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
* test(vm): reproduce runtime store rollback poisoning * fix(vm): keep runtime sidecar rollback-readable * fix(vm): harden rollback-compatible runtime persistence * fix(vm): publish rollback lifecycle authority first * test(vm): harden rollback compatibility coverage
303 lines
10 KiB
TypeScript
303 lines
10 KiB
TypeScript
import { existsSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { JsonStringifyByteLimitError } from './node-bounded-json-stringify'
|
|
import { readNodeFileSyncWithinLimit } from './node-bounded-file-reader'
|
|
import { writeSecureJsonFileWithinLimit } from './bounded-secure-json-file'
|
|
import { hardenExistingSecureFile } from './secure-file'
|
|
import {
|
|
featureEntryFromRuntime,
|
|
featureIdentity,
|
|
readEphemeralVmRuntimeFeatureStore,
|
|
restoreRuntimeFeatures,
|
|
runtimeFeaturesEqual,
|
|
writeEphemeralVmRuntimeFeatureStore,
|
|
type EphemeralVmRuntimeFeatureStoreSnapshot
|
|
} from './ephemeral-vm-runtime-feature-store'
|
|
import {
|
|
mergeRuntimeFeatures,
|
|
projectRuntimeForRollback,
|
|
runtimeFeatureListsEqual
|
|
} from './ephemeral-vm-runtime-rollback-projection'
|
|
import {
|
|
EphemeralVmRuntimeRecordSchema,
|
|
EphemeralVmRuntimeStoreSchema,
|
|
RollbackEphemeralVmRuntimeStoreSchema,
|
|
type EphemeralVmCleanupStatus,
|
|
type EphemeralVmRuntimeRecord,
|
|
type EphemeralVmRuntimeStatus,
|
|
type EphemeralVmRuntimeStore
|
|
} from './ephemeral-vm-runtimes'
|
|
|
|
const EPHEMERAL_VM_RUNTIMES_FILE = 'orca-ephemeral-vm-runtimes.json'
|
|
export const MAX_EPHEMERAL_VM_RUNTIME_STORE_FILE_BYTES = 1024 * 1024
|
|
|
|
export type EphemeralVmRuntimeStoreErrorCode = 'invalid_argument' | 'runtime_error'
|
|
|
|
export class EphemeralVmRuntimeStoreError extends Error {
|
|
readonly code: EphemeralVmRuntimeStoreErrorCode
|
|
|
|
constructor(code: EphemeralVmRuntimeStoreErrorCode, message: string) {
|
|
super(message)
|
|
this.name = 'EphemeralVmRuntimeStoreError'
|
|
this.code = code
|
|
}
|
|
}
|
|
|
|
export function getEphemeralVmRuntimeStorePath(userDataPath: string): string {
|
|
return join(userDataPath, EPHEMERAL_VM_RUNTIMES_FILE)
|
|
}
|
|
|
|
export function listEphemeralVmRuntimes(userDataPath: string): EphemeralVmRuntimeRecord[] {
|
|
return readEphemeralVmRuntimeStore(userDataPath).store.runtimes
|
|
}
|
|
|
|
export function upsertEphemeralVmRuntime(
|
|
userDataPath: string,
|
|
record: EphemeralVmRuntimeRecord
|
|
): EphemeralVmRuntimeRecord {
|
|
const parsed = EphemeralVmRuntimeRecordSchema.parse(record)
|
|
const loaded = readEphemeralVmRuntimeStore(userDataPath)
|
|
const previous = loaded.store.runtimes.find((entry) => entry.id === parsed.id)
|
|
if (
|
|
previous &&
|
|
featureIdentity(previous) === featureIdentity(parsed) &&
|
|
!runtimeFeaturesEqual(previous, parsed)
|
|
) {
|
|
throw new EphemeralVmRuntimeStoreError(
|
|
'invalid_argument',
|
|
`Cannot change compatibility features for ephemeral VM runtime: ${parsed.id}`
|
|
)
|
|
}
|
|
writeEphemeralVmRuntimeStore(
|
|
userDataPath,
|
|
{
|
|
version: 1,
|
|
runtimes: [...loaded.store.runtimes.filter((entry) => entry.id !== parsed.id), parsed].sort(
|
|
compareRuntimeRecords
|
|
)
|
|
},
|
|
loaded.features
|
|
)
|
|
return parsed
|
|
}
|
|
|
|
export function upsertEphemeralVmRuntimeRollbackRecovery(
|
|
userDataPath: string,
|
|
record: EphemeralVmRuntimeRecord
|
|
): void {
|
|
const parsed = EphemeralVmRuntimeRecordSchema.parse(record)
|
|
const loaded = readEphemeralVmRuntimeStore(userDataPath)
|
|
const path = getEphemeralVmRuntimeStorePath(userDataPath)
|
|
try {
|
|
writeSecureJsonFileWithinLimit(
|
|
path,
|
|
RollbackEphemeralVmRuntimeStoreSchema.parse({
|
|
version: 1,
|
|
runtimes: [...loaded.store.runtimes.filter((entry) => entry.id !== parsed.id), parsed]
|
|
.sort(compareRuntimeRecords)
|
|
.map(projectRuntimeForRollback)
|
|
}),
|
|
MAX_EPHEMERAL_VM_RUNTIME_STORE_FILE_BYTES,
|
|
{ durable: true }
|
|
)
|
|
} catch (error) {
|
|
if (error instanceof JsonStringifyByteLimitError) {
|
|
throw new EphemeralVmRuntimeStoreError(
|
|
'runtime_error',
|
|
`Could not write Orca ephemeral VM runtimes at ${path}; the store exceeds its durable capacity.`
|
|
)
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
export function updateEphemeralVmRuntimeStatus(
|
|
userDataPath: string,
|
|
id: string,
|
|
args: {
|
|
status?: EphemeralVmRuntimeStatus
|
|
cleanupStatus?: EphemeralVmCleanupStatus
|
|
cleanupLastAttemptAt?: number
|
|
cleanupLastError?: string | null
|
|
workspaceId?: string
|
|
workspaceName?: string
|
|
connectionMode?: EphemeralVmRuntimeRecord['connectionMode'] | null
|
|
runtimeEnvironmentId?: string
|
|
sshTargetId?: string | null
|
|
recipeResult?: EphemeralVmRuntimeRecord['recipeResult']
|
|
updatedAt?: number
|
|
}
|
|
): EphemeralVmRuntimeRecord {
|
|
const loaded = readEphemeralVmRuntimeStore(userDataPath)
|
|
const existing = loaded.store.runtimes.find((entry) => entry.id === id)
|
|
if (!existing) {
|
|
throw new EphemeralVmRuntimeStoreError(
|
|
'invalid_argument',
|
|
`Unknown ephemeral VM runtime: ${id}`
|
|
)
|
|
}
|
|
const next = EphemeralVmRuntimeRecordSchema.parse({
|
|
...existing,
|
|
...(args.status ? { status: args.status } : {}),
|
|
...(args.cleanupStatus ? { cleanupStatus: args.cleanupStatus } : {}),
|
|
...(args.cleanupLastAttemptAt !== undefined
|
|
? { cleanupLastAttemptAt: args.cleanupLastAttemptAt }
|
|
: {}),
|
|
...(args.cleanupLastError === null
|
|
? { cleanupLastError: undefined }
|
|
: args.cleanupLastError
|
|
? { cleanupLastError: args.cleanupLastError }
|
|
: {}),
|
|
...(args.workspaceId ? { workspaceId: args.workspaceId } : {}),
|
|
...(args.workspaceName ? { workspaceName: args.workspaceName } : {}),
|
|
// null explicitly clears the field (e.g. terminal cleanup); undefined leaves it unchanged.
|
|
...(args.connectionMode === null
|
|
? { connectionMode: undefined }
|
|
: args.connectionMode
|
|
? { connectionMode: args.connectionMode }
|
|
: {}),
|
|
...(args.runtimeEnvironmentId ? { runtimeEnvironmentId: args.runtimeEnvironmentId } : {}),
|
|
...(args.sshTargetId === null
|
|
? { sshTargetId: undefined }
|
|
: args.sshTargetId
|
|
? { sshTargetId: args.sshTargetId }
|
|
: {}),
|
|
...(args.recipeResult ? { recipeResult: args.recipeResult } : {}),
|
|
updatedAt: args.updatedAt ?? Date.now()
|
|
})
|
|
writeEphemeralVmRuntimeStore(
|
|
userDataPath,
|
|
{
|
|
version: 1,
|
|
runtimes: loaded.store.runtimes
|
|
.map((entry) => (entry.id === id ? next : entry))
|
|
.sort(compareRuntimeRecords)
|
|
},
|
|
loaded.features
|
|
)
|
|
return next
|
|
}
|
|
|
|
export function removeEphemeralVmRuntime(
|
|
userDataPath: string,
|
|
id: string
|
|
): EphemeralVmRuntimeRecord {
|
|
const loaded = readEphemeralVmRuntimeStore(userDataPath)
|
|
const existing = loaded.store.runtimes.find((entry) => entry.id === id)
|
|
if (!existing) {
|
|
throw new EphemeralVmRuntimeStoreError(
|
|
'invalid_argument',
|
|
`Unknown ephemeral VM runtime: ${id}`
|
|
)
|
|
}
|
|
writeEphemeralVmRuntimeStore(
|
|
userDataPath,
|
|
{
|
|
version: 1,
|
|
runtimes: loaded.store.runtimes.filter((entry) => entry.id !== id)
|
|
},
|
|
loaded.features
|
|
)
|
|
return existing
|
|
}
|
|
|
|
type LoadedEphemeralVmRuntimeStore = {
|
|
store: EphemeralVmRuntimeStore
|
|
features: EphemeralVmRuntimeFeatureStoreSnapshot
|
|
}
|
|
|
|
function readEphemeralVmRuntimeStore(userDataPath: string): LoadedEphemeralVmRuntimeStore {
|
|
const path = getEphemeralVmRuntimeStorePath(userDataPath)
|
|
if (!existsSync(path)) {
|
|
return {
|
|
store: { version: 1, runtimes: [] },
|
|
features: readEphemeralVmRuntimeFeatureStore(userDataPath)
|
|
}
|
|
}
|
|
try {
|
|
hardenExistingSecureFile(path)
|
|
const persisted = JSON.parse(
|
|
readNodeFileSyncWithinLimit(path, MAX_EPHEMERAL_VM_RUNTIME_STORE_FILE_BYTES).buffer.toString(
|
|
'utf8'
|
|
)
|
|
)
|
|
const parsed = EphemeralVmRuntimeStoreSchema.parse(persisted)
|
|
const features = readEphemeralVmRuntimeFeatureStore(userDataPath)
|
|
const store: EphemeralVmRuntimeStore = {
|
|
version: 1,
|
|
runtimes: parsed.runtimes
|
|
.map((entry) => restoreRuntimeFeatures(entry, features.features))
|
|
.sort(compareRuntimeRecords)
|
|
}
|
|
if (features.writable && !RollbackEphemeralVmRuntimeStoreSchema.safeParse(persisted).success) {
|
|
try {
|
|
writeEphemeralVmRuntimeStore(userDataPath, store, features)
|
|
} catch {
|
|
// Why: a failed migration must not block cleanup through the still-readable current shape.
|
|
}
|
|
}
|
|
return { store, features }
|
|
} catch {
|
|
throw new EphemeralVmRuntimeStoreError(
|
|
'runtime_error',
|
|
`Could not read Orca ephemeral VM runtimes at ${path}; the file is invalid.`
|
|
)
|
|
}
|
|
}
|
|
|
|
function writeEphemeralVmRuntimeStore(
|
|
userDataPath: string,
|
|
store: EphemeralVmRuntimeStore,
|
|
features: EphemeralVmRuntimeFeatureStoreSnapshot
|
|
): void {
|
|
const path = getEphemeralVmRuntimeStorePath(userDataPath)
|
|
try {
|
|
const parsed = EphemeralVmRuntimeStoreSchema.parse(store)
|
|
const requiredFeatures = mergeRuntimeFeatures(
|
|
[],
|
|
parsed.runtimes.flatMap((entry) => {
|
|
const feature = featureEntryFromRuntime(entry)
|
|
return feature ? [feature] : []
|
|
})
|
|
)
|
|
const preparedFeatures = mergeRuntimeFeatures(features.features, requiredFeatures)
|
|
writeSecureJsonFileWithinLimit(
|
|
path,
|
|
RollbackEphemeralVmRuntimeStoreSchema.parse({
|
|
version: 1,
|
|
runtimes: parsed.runtimes.map(projectRuntimeForRollback)
|
|
}),
|
|
MAX_EPHEMERAL_VM_RUNTIME_STORE_FILE_BYTES,
|
|
{ durable: preparedFeatures.length > 0 || features.features.length > 0 }
|
|
)
|
|
if (!features.writable && requiredFeatures.length > 0) {
|
|
throw new EphemeralVmRuntimeStoreError(
|
|
'runtime_error',
|
|
'Could not preserve ephemeral VM runtime compatibility metadata.'
|
|
)
|
|
}
|
|
if (features.writable && !runtimeFeatureListsEqual(features.features, preparedFeatures)) {
|
|
writeEphemeralVmRuntimeFeatureStore(userDataPath, features, preparedFeatures)
|
|
}
|
|
if (features.writable && !runtimeFeatureListsEqual(preparedFeatures, requiredFeatures)) {
|
|
try {
|
|
writeEphemeralVmRuntimeFeatureStore(userDataPath, features, requiredFeatures)
|
|
} catch {
|
|
// Stale feature records do not match any persisted runtime identity.
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof JsonStringifyByteLimitError) {
|
|
throw new EphemeralVmRuntimeStoreError(
|
|
'runtime_error',
|
|
`Could not write Orca ephemeral VM runtimes at ${path}; the store exceeds its durable capacity.`
|
|
)
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
function compareRuntimeRecords(a: EphemeralVmRuntimeRecord, b: EphemeralVmRuntimeRecord): number {
|
|
return b.createdAt - a.createdAt || a.id.localeCompare(b.id)
|
|
}
|