Files
orca/src/shared/runtime-environment-store-records.ts
T
Neil a3e97184f4 fix(runtime): keep remote servers usable when one saved record can't be read (#27685)
* fix(runtime-environments): tolerate unreadable server records without dropping them

One corrupt or newer-format record in orca-environments.json or the
environment sidecar made every server unlistable. Each file now parses
per record: readable records load, unreadable ones are rewritten
verbatim on every write. A server whose sidecar entry cannot be read is
hidden and refused for edits, so it never connects or is rewritten
without its SSH access or deployment link.

* test(runtime-environments): fuzz interleaved writers against unreadable records
2026-10-11 19:15:43 -07:00

108 lines
3.5 KiB
TypeScript

import { existsSync } from 'node:fs'
import { join } from 'node:path'
import { z } from 'zod'
import { readNodeFileSyncWithinLimit } from './node-bounded-file-reader'
import {
PersistedRuntimeEnvironmentSchema,
type PersistedRuntimeEnvironment
} from './runtime-environments'
import { hardenExistingSecureFile } from './secure-file'
const ENVIRONMENTS_FILE = 'orca-environments.json'
export const MAX_RUNTIME_ENVIRONMENT_STORE_FILE_BYTES = 1024 * 1024
export type RuntimeEnvironmentStoreErrorCode = 'invalid_argument' | 'runtime_error'
export class RuntimeEnvironmentStoreError extends Error {
readonly code: RuntimeEnvironmentStoreErrorCode
constructor(code: RuntimeEnvironmentStoreErrorCode, message: string) {
super(message)
this.name = 'RuntimeEnvironmentStoreError'
this.code = code
}
}
export function getEnvironmentStorePath(userDataPath: string): string {
return join(userDataPath, ENVIRONMENTS_FILE)
}
// Records stay opaque so one bad or newer-format entry cannot hide the rest.
const StoreEnvelopeSchema = z.object({
version: z.literal(1),
environments: z.array(z.unknown())
})
/** A record this build cannot read; it is rewritten verbatim so a newer build still finds it. */
export type UnreadableEnvironmentRecord = { id: string | undefined; raw: unknown }
export type EnvironmentStoreRecords = {
environments: PersistedRuntimeEnvironment[]
unreadable: UnreadableEnvironmentRecord[]
}
function recordId(raw: unknown): string | undefined {
if (typeof raw !== 'object' || raw === null || !('id' in raw)) {
return undefined
}
return typeof raw.id === 'string' && raw.id.length > 0 ? raw.id : undefined
}
function parseRecord(raw: unknown): PersistedRuntimeEnvironment | undefined {
// Why: a `kind` marks a record type this build predates; reading it as a plain pairing would
// strip the kind and rewrite it as one.
if (typeof raw === 'object' && raw !== null && 'kind' in raw) {
return undefined
}
const parsed = PersistedRuntimeEnvironmentSchema.safeParse(raw)
return parsed.success ? parsed.data : undefined
}
/** The envelope must be valid; individual records that are not are set aside, not rejected. */
export function readEnvironmentStoreRecords(
userDataPath: string,
options: { requireStoreFile?: boolean } = {}
): EnvironmentStoreRecords {
const path = getEnvironmentStorePath(userDataPath)
if (!options.requireStoreFile && !existsSync(path)) {
return { environments: [], unreadable: [] }
}
let records: unknown[]
try {
hardenExistingSecureFile(path)
records = StoreEnvelopeSchema.parse(
JSON.parse(
readNodeFileSyncWithinLimit(path, MAX_RUNTIME_ENVIRONMENT_STORE_FILE_BYTES).buffer.toString(
'utf8'
)
)
).environments
} catch {
throw new RuntimeEnvironmentStoreError(
'runtime_error',
`Could not read Orca environments at ${path}; the file is invalid.`
)
}
const result: EnvironmentStoreRecords = { environments: [], unreadable: [] }
for (const raw of records) {
const environment = parseRecord(raw)
if (environment) {
result.environments.push(environment)
} else {
result.unreadable.push({ id: recordId(raw), raw })
}
}
return result
}
const warned = new Set<string>()
/** Logs once per distinct message so per-round-trip reads do not flood the log. */
export function warnUnreadableEnvironmentState(message: string): void {
if (warned.has(message)) {
return
}
warned.add(message)
console.warn(`[runtime-environments] ${message}`)
}