Files
orca/src/relay/ai-vault-opencode-reader.ts
T
8afa1db50c feat(ssh): rung B glibc 2.17 compat runtime; gate remote vault on host node:sqlite (#24148)
* feat(ssh): wire rung B to the glibc 2.17 compat runtime; gate rung C vault on full node:sqlite

- COMPAT_RELAY_RUNTIMES lists linux-x64-glibc217; rung B plans the compat slot and compat
  pinned Node when glibc is below 2.28 or rung A refused with libc_floor/missing_lib.
- The relay version folds the compat runtime's executable hash; refusals are cached per runtime.
- The orcad template stages an optional linux-x64-glibc217 target (base package + compat
  node-pty slot + compat runtime marker); the verifier and materializer accept it.
- node-pty slot loader falls back to the compat slot when the default slot is missing or
  needs a newer glibc.
- Runtime store GC keeps the compat pin beside the default one on every relay connect.
- hasNodeSqliteReaderApi (DatabaseSync + backup) gates relay session search and the relay
  OpenCode reader, which now names the host Node version in its unavailable reason; the SSH
  vault reader installs the compat Node on old-glibc hosts and uploads nothing when no
  pinned Node can run.
- Rung D: a remembered noexec reports home_noexec and never advises installing Node.

* fix(ssh): re-prove a replayed noexec after rung D so allowing exec recovers the host

* fix(ssh): keep the rung B compat runtime pinned in the relay-connect store GC

* test(ssh): mock deployment-target facts in the Windows OpenCode runtime tests

* ci(ssh): build the glibc 2.17 compat slot for the hostile-host matrix; CentOS 7 lands on rung B

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-01 05:32:05 -07:00

172 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { open } from 'node:fs/promises'
import { homedir } from 'node:os'
import { isAbsolute, join } from 'node:path'
import type { RemoteOpenCodeSessionReader } from '../main/ai-vault/remote-session-scanner-types'
import { throwIfAiVaultScanCancelled } from '../main/ai-vault/ai-vault-scan-cancellation'
import { isMissingRemoteSessionPathError } from '../main/ai-vault/remote-session-file-stat'
import { parseOpenCodeSessionFile } from '../main/ai-vault/session-scanner-opencode-parser'
import { createOpenCodeSqliteProcessClient } from '../main/ai-vault/session-scanner-opencode-sqlite-process-client'
import { buildRelayAiVaultServiceEnv } from '../main/ai-vault/session-scanner-service-env'
import { resolveOpenCodeDataDirectory } from '../main/opencode/opencode-data-directory'
import SyncDatabase, { isSqliteAvailable } from '../main/sqlite/sync-database'
type Reader = Pick<RemoteOpenCodeSessionReader, 'list' | 'parse'> & { dispose(): void }
export type RelayOpenCodeReaderOptions = {
baseDir?: string
environment?: NodeJS.ProcessEnv
homeDirectory?: string
currentExecutable?: string
readerFactory?: (options: Parameters<typeof createOpenCodeSqliteProcessClient>[0]) => Reader
canReadSqlite?: () => boolean
}
export function createRelayOpenCodeReader(
options: RelayOpenCodeReaderOptions = {}
): RemoteOpenCodeSessionReader & { dispose(): void } {
const environment = options.environment ?? process.env
const dataDirectory = resolveOpenCodeDataDirectory(
environment,
options.homeDirectory ?? homedir()
)
const override = environment.OPENCODE_DB?.trim()
const baseDir = options.baseDir ?? __dirname
let reader: Reader | undefined
let executable: string | undefined
let pending: Promise<Reader> | undefined
let disposed = false
const getReader = (refreshRuntime = false): Promise<Reader> => {
if (disposed) {
return Promise.reject(new Error('OpenCode database reader was disposed.'))
}
if (reader && !refreshRuntime) {
return Promise.resolve(reader)
}
pending ??= (async () => {
const configured = await readRuntimeExecutable(join(baseDir, 'opencode-sqlite-runtime.json'))
if (!configured && !(options.canReadSqlite ?? canCurrentRuntimeReadSqlite)()) {
throw new Error(openCodeReaderUnavailableMessage(process.version))
}
if (disposed) {
throw new Error('OpenCode database reader was disposed.')
}
const nextExecutable = configured ?? options.currentExecutable ?? process.execPath
if (!reader || executable !== nextExecutable) {
reader?.dispose()
reader = (options.readerFactory ?? createOpenCodeSqliteProcessClient)({
executable: nextExecutable,
args: [join(baseDir, 'opencode-sqlite-reader.cjs')],
env: buildRelayAiVaultServiceEnv(environment)
})
executable = nextExecutable
}
return reader
})().finally(() => {
pending = undefined
})
return pending
}
return {
dataDirectory,
...(override
? {
databasePath:
override === ':memory:'
? null
: isAbsolute(override)
? override
: join(dataDirectory, override)
}
: {}),
async list(args) {
throwIfAiVaultScanCancelled(args.signal)
if (args.dbPaths.length === 0) {
return []
}
try {
return await (await getReader(true)).list(args)
} catch (error) {
throwIfAiVaultScanCancelled(args.signal)
args.issues.push({
agent: args.agent ?? 'opencode',
kind: 'scope',
path: args.dbPaths[0] ?? dataDirectory,
message: error instanceof Error ? error.message : String(error)
})
return []
}
},
async parse(args) {
throwIfAiVaultScanCancelled(args.signal)
return (await getReader()).parse(args)
},
parseLegacy: parseOpenCodeSessionFile,
dispose() {
if (disposed) {
return
}
disposed = true
reader?.dispose()
}
}
}
async function readRuntimeExecutable(path: string): Promise<string | undefined> {
let file
try {
file = await open(path, 'r')
} catch (error) {
if (isMissingRemoteSessionPathError(error)) {
return undefined
}
throw error
}
try {
const bytes = Buffer.alloc(16_385)
const { bytesRead } = await file.read(bytes, 0, bytes.length, 0)
if (bytesRead > 16_384) {
throw new Error('OpenCode database runtime reference exceeds its size limit.')
}
const reference: unknown = JSON.parse(bytes.subarray(0, bytesRead).toString('utf8'))
if (
typeof reference !== 'object' ||
reference === null ||
!('protocol' in reference) ||
reference.protocol !== 1 ||
!('executable' in reference) ||
typeof reference.executable !== 'string' ||
!isAbsolute(reference.executable) ||
reference.executable.includes('\0')
) {
throw new Error('OpenCode database runtime reference is invalid.')
}
return reference.executable
} finally {
await file.close()
}
}
/** Rung C runs the relay on the host's Node, which may predate the full node:sqlite surface. */
export function openCodeReaderUnavailableMessage(nodeVersion: string): string {
return (
`OpenCode history is unavailable on this host for now: its Node.js ${nodeVersion} has no ` +
'complete node:sqlite (22.16 or newer), and Orca has not installed its own Node reader here yet.'
)
}
function canCurrentRuntimeReadSqlite(): boolean {
// Why: same admission as the host probes, so Node 22.13–22.15 waits for the pinned runtime.
if (!isSqliteAvailable()) {
return false
}
let db: SyncDatabase | undefined
try {
db = new SyncDatabase(':memory:')
return db.prepare('SELECT 1 AS ready').get()?.ready === 1
} catch {
return false
} finally {
db?.close()
}
}