mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 01:27:35 +00:00
* [comment audit] Align legacy failure-read counts with cached display * [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci] * [comment audit] Condense shared generation, path and preview comments [skip ci] * [comment audit] Condense host, file and Git contract rationale [skip ci] * [comment audit] Tighten Git lifecycle and provider contract comments [skip ci] * [comment audit] Clarify shared host authority and launch isolation [skip ci] * [comment audit] simplify local access mobile and native chat contracts [skip ci] * [comment audit] simplify native chat presentation and state contracts [skip ci] * [comment audit] simplify native runtime and provider contracts [skip ci] * [comment audit] simplify migration and orchestration contracts [skip ci] * [comment audit] simplify plugin trust and process capture rationale [skip ci] * [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci] * [comment audit] clarify remote recovery and launch wire rationale [skip ci] * [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci] * [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci] * [comment audit] clarify sidebar, skill and SSH invariants [skip ci] * [comment audit] condense structured chat delivery and timing rationale [skip ci] * [comment audit] condense terminal replay and telemetry invariants [skip ci] * [comment audit] clarify launch permission and terminal delivery rationale [skip ci] * [comment audit] clarify workspace ownership and layout contracts [skip ci] * [comment audit] wait for fitted and painted SSH terminal links * [comment audit] clarify layout persistence and session ownership [skip ci] * [comment audit] clarify host routing and workspace lifecycle contracts [skip ci] * [comment audit] clarify startup persistence and app lifetime comments [skip ci] * [comment audit] clarify recovery and activity boundaries [skip ci] * [comment audit] condense activity and artifact invariants [skip ci] * [comment audit] condense automation authority contracts [skip ci] * [comment audit] simplify automation scheduling and history contracts [skip ci] * [comment audit] clarify browser chrome and annotation lifecycles [skip ci] * [comment audit] clarify browser routing and retained guest ownership [skip ci] * [comment audit] clarify browser recovery and document ownership comments [skip ci] * [comment audit] restore hosted runners required by native IME fixtures * [comment audit] shorten preview, palette and dashboard invariants [skip ci] * [comment audit] clarify terminal preview, dashboard and retention comments [skip ci] * [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci] * [comment audit] shorten rich-text and combined-diff ownership comments [skip ci] * [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci] * [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci] * [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci] * [comment audit] clarify rich Markdown source and selection safeguards [skip ci] * [comment audit] clarify editor loading and emulator stream lifetimes [skip ci] * [comment audit] trim emulator and feature tour narration [skip ci] * [comment audit] clarify floating workspace and GitHub cache ownership [skip ci] * [comment audit] trim provider controls and Linear filter explanations [skip ci] * [comment audit] clarify pairing and chat resume lifetimes [skip ci] * [comment audit] clarify chat delivery and transcript lifetimes [skip ci] * [comment audit] clarify session options and host-owned status [skip ci] * [comment audit] clarify draft durability and live-status precedence [skip ci] * [comment audit] Trim chat send and model ownership explanations [skip ci] * [comment audit] Shorten transcript ownership and send recovery comments [skip ci] * [comment audit] Simplify native chat composer and transcript hook comments [skip ci] * [comment audit] Shorten native chat scroll and structured session explanations [skip ci] * [comment audit] Trim workspace picker and onboarding commentary [skip ci] * [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci] * [comment audit] Trim sidebar and session history explanations [skip ci] * [comment audit] Condense session resume and review checks comments [skip ci] * [comment audit] Shorten checks state and file explorer refresh comments [skip ci] * [comment audit] Trim plugin bridge and source-control action rationale [skip ci] * [comment audit] Trim source-control listing and review rationale [skip ci] * [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci] * [comment audit] Condense Settings account and appearance rationale [skip ci] * [comment audit] Trim pairing and repository Settings explanations [skip ci] * [comment audit] Condense Settings search SSH and terminal safeguards [skip ci] * [comment audit] Shorten Settings voice and provider ownership comments [skip ci] * [comment audit] Condense Settings draft persistence and host-state rationale [skip ci] * [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci] * [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci] * [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci] * [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci] * [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci] * [comment audit] Prune focus-read baseline after comment cleanup * [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci] * [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci] * [comment audit] Clarify viewport geometry and skill update safeguards [skip ci] * [comment audit] Tighten skill inventory, usage and host status comments [skip ci] * [comment audit] Clarify usage ownership and resource kill safeguards [skip ci] * [comment audit] Tighten tab selection, rename and search contracts [skip ci] * [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci] * [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci] * [comment audit] Tighten provider loading and terminal recovery comments [skip ci] * [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci] * [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci] * [comment audit] Tighten PTY startup and replay explanations [skip ci] * [comment audit] Clarify PTY identity and delivery ownership [skip ci] * [comment audit] Shorten remote reconnect and input replay rationale [skip ci] * [comment audit] Condense terminal parking clipboard and file link comments [skip ci] * [comment audit] Clarify IME ownership and trim trace history [skip ci] * [comment audit] Shorten pane recovery and parked watcher explanations [skip ci] * [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci] * [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci] * [comment audit] Trim renderer lifecycle and menu rationale [skip ci] * [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci] * [comment audit] Clarify renderer hook evidence and source ownership [skip ci] * [comment audit] Tighten IPC ownership and reconnect explanations [skip ci] * [comment audit] Trim renderer focus, paste and attention comments [skip ci] * [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci] * [comment audit] Trim agent launch, resume and ownership commentary [skip ci] * [comment audit] Shorten account, browser and editor comments [skip ci] * [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci] * [comment audit] Condense launch recovery and editor tokenization comments [skip ci] * [comment audit] Condense workspace selection and terminal pane rationale [skip ci] * [comment audit] Condense terminal fitting and rendering recovery comments [skip ci] * [comment audit] Condense terminal input and host routing explanations [skip ci] * [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci] * [comment audit] Condense structured launch and terminal link explanations [skip ci] * [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci] * [comment audit] Condense session persistence and workspace activation rationale [skip ci] * [comment audit] Condense workspace routing and browser ownership rationale [skip ci] * [comment audit] Condense mirror recovery and layout authority rationale [skip ci] * [comment audit] Condense runtime transport and file safety rationale [skip ci] * [comment audit] Condense runtime graph and structured session rationale [skip ci] * [comment audit] Condense browser creation and remote tab recovery rationale [skip ci] * [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci] * [comment audit] Condense frame, startup and provider cache rationale [skip ci] * [comment audit] Condense host twin and agent lifecycle rationale [skip ci] * [comment audit] Condense browser authority and editor cleanup rationale [skip ci] * [comment audit] Condense editor buffer identity and review cache rationale [skip ci] * [comment audit] Condense provider lifetime and repository ownership rationale [skip ci] * [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci] * [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci] * [comment audit] Condense cleanup ownership and listing admission rationale [skip ci] * [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci] * [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci] * [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci] * [comment audit] Condense cloud rollout and assignment safety rationale [skip ci] * [comment audit] Condense relay session and database recovery rationale [skip ci] * [comment audit] Condense relay placement and protocol compatibility rationale [skip ci] * [comment audit] Condense build and packaging constraints [skip ci] * [comment audit] Condense CI gates and validation isolation rationale [skip ci] * [comment audit] Condense catalog identity and browser validation rationale [skip ci] * [comment audit] Condense mobile browser and frame-budget rationale [skip ci] * [comment audit] Shorten mobile route and browser test explanations [skip ci] * [comment audit] Condense editor and session seam test rationale [skip ci] * [comment audit] Trim session and terminal closure history [skip ci] * [comment audit] Shorten terminal browser and census explanations [skip ci] * [comment audit] Preserve root-only E2E diagnostic baseline * [comment audit] Condense mobile seam and HTML preview test rationale [skip ci] * [comment audit] Condense preview instrumentation and render harness rationale [skip ci] * [comment audit] Condense bundle contracts and native-build constraints [skip ci] * [comment audit] Shorten native packaging and CI routing rationale [skip ci] * [comment audit] Condense release tooling and benchmark constraints [skip ci] * [comment audit] Condense packaging and platform gate rationale [skip ci] * [comment audit] Shorten mobile route and history rationale [skip ci] * [comment audit] Condense mobile browser and component rationale [skip ci] * [comment audit] Shorten drawer and workspace-form rationale [skip ci] * [comment audit] Trim mobile review and editor-document explanations [skip ci] * [comment audit] Condense editor lifecycle and Markdown explanations [skip ci] * [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci] * [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci] * [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci] * [comment audit] Condense bridge stream, native request and audio rationale [skip ci] * [comment audit] Reduce bridge schema and page-client commentary [skip ci] * [comment audit] Tighten mobile page bridge contracts [skip ci] * [comment audit] Clarify mobile cache and session constraints [skip ci] * [comment audit] Condense mobile route and lifecycle rationale [skip ci] * [comment audit] Tighten mobile shell hook timing rationale [skip ci] * [comment audit] Clarify route handoff and notification compatibility [skip ci] * [comment audit] Clarify push registration and dictation cleanup constraints [skip ci] * [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci] * [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci] * [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci] * [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci] * [comment audit] Tighten stream replay and queued session ownership rationale [skip ci] * [comment audit] Condense mobile composer and reply-schema constraints [skip ci] * [comment audit] Clarify mobile transcript recovery and session ownership [skip ci] * [comment audit] Clarify mobile queue and source-control compatibility [skip ci] * [comment audit] Condense storage and task launch constraints [skip ci] * [comment audit] Clarify task schemas and terminal document ownership [skip ci] * [comment audit] Tighten terminal input and viewport rationale [skip ci] * [comment audit] Shorten terminal and RPC recorder contracts [skip ci] * [comment audit] Condense recorder engine and connection contracts [skip ci] * [comment audit] Shorten host credential and relay recovery comments [skip ci] * [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci] * [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci] * [comment audit] Condense catalog and relay entry contracts [skip ci] * [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci] * [comment audit] Condense WSL relay and E2E regression rationale [skip ci] * [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci] * [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci] * [comment audit] Shorten layout oracle and regression explanations [skip ci] * [comment audit] Condense paired recovery and ownership regression contracts [skip ci] * [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci] * [comment audit] Condense terminal rendering and IME regression rationale [skip ci] * [comment audit] Shorten terminal recovery and benchmark contracts [skip ci] * [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci] * [comment audit] Condense workflow routing and release rationale [skip ci] * [comment audit] Condense mobile security and installer contracts [skip ci] * [comment audit] Shorten native rationale and stylesheet workarounds [skip ci] * [comment audit] Shorten Windows visibility probe rationale [skip ci] * [comment audit] Condense embedded release and signing rationale [skip ci] * [comment audit] Retain current main test behavior in final reconciliation [skip ci] * [comment audit] Complete final comment review across current source inventory * [comment audit] Handle consolidated audit reports before final CI * [comment audit] Preserve credential storage census detail [skip ci] * [comment audit] Scale broad changed E2E selections with verified timing shards * [comment audit] Align E2E assertions with browser close and settled session metadata * [comment audit] Match existing E2Es to current resource and windowing contracts * [comment audit] Observe the shared server outage notice in reconnect coverage * [comment audit] Make existing renderer probes measure settled tab and PTY work * [comment audit] Align palette E2E selector with task search * [comment audit] Refresh remaining palette search selectors * [comment audit] Keep selector correction within selected audit specs * [comment audit] Await emoji fixture settings inside renderer evaluation * [comment audit] Cancel fixture fit retries between terminal tests --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
611 lines
21 KiB
JavaScript
611 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
||
import { _electron as electron } from '@stablyai/playwright-test'
|
||
import { execFileSync } from 'node:child_process'
|
||
import { createHash, randomUUID } from 'node:crypto'
|
||
import { existsSync } from 'node:fs'
|
||
import {
|
||
copyFile,
|
||
lstat,
|
||
mkdir,
|
||
mkdtemp,
|
||
readFile,
|
||
readdir,
|
||
realpath,
|
||
rm,
|
||
writeFile
|
||
} from 'node:fs/promises'
|
||
import os from 'node:os'
|
||
import path from 'node:path'
|
||
import process from 'node:process'
|
||
import readline from 'node:readline/promises'
|
||
import { pathToFileURL } from 'node:url'
|
||
import {
|
||
classifyCodexHomeTripwireEvent,
|
||
startCodexPrimaryHomeTripwire
|
||
} from './codex-primary-home-tripwire.mjs'
|
||
import {
|
||
cleanupValidationDaemons,
|
||
closeValidationElectronApp
|
||
} from './codex-validation-process-shutdown.mjs'
|
||
|
||
const RESTRICTED_ENV_KEYS = [
|
||
'HOME',
|
||
'USERPROFILE',
|
||
'HOMEDRIVE',
|
||
'HOMEPATH',
|
||
'CODEX_HOME',
|
||
'ORCA_CODEX_HOME',
|
||
'ORCA_E2E_HOME_DIR',
|
||
'ORCA_E2E_USER_DATA_DIR',
|
||
'ORCA_USER_DATA_PATH',
|
||
'ZDOTDIR',
|
||
'ORCA_ORIG_ZDOTDIR',
|
||
'BASH_ENV',
|
||
'ENV',
|
||
'ELECTRON_RUN_AS_NODE'
|
||
]
|
||
const VALID_SCENARIOS = new Set(['mixed', 'managed-only', 'codex-lb'])
|
||
|
||
function samePath(left, right) {
|
||
const normalizedLeft = path.resolve(left)
|
||
const normalizedRight = path.resolve(right)
|
||
return process.platform === 'win32'
|
||
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
||
: normalizedLeft === normalizedRight
|
||
}
|
||
|
||
function isWithin(candidate, parent) {
|
||
const relative = path.relative(path.resolve(parent), path.resolve(candidate))
|
||
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
||
}
|
||
|
||
async function resolveRealPath(candidate) {
|
||
try {
|
||
return await realpath(candidate)
|
||
} catch {
|
||
// Why: containment guards must still run for paths that do not exist yet.
|
||
return candidate
|
||
}
|
||
}
|
||
|
||
export function createValidationEnv(inheritedEnv, layout) {
|
||
const env = { ...inheritedEnv }
|
||
for (const key of RESTRICTED_ENV_KEYS) {
|
||
delete env[key]
|
||
}
|
||
return {
|
||
...env,
|
||
HOME: layout.homeDir,
|
||
USERPROFILE: layout.homeDir,
|
||
NODE_ENV: 'development',
|
||
ORCA_E2E_HOME_DIR: layout.homeDir,
|
||
ORCA_E2E_USER_DATA_DIR: layout.userDataDir,
|
||
ORCA_USER_DATA_PATH: layout.userDataDir
|
||
}
|
||
}
|
||
|
||
export async function createValidationLayout(options = {}) {
|
||
const primaryHome = path.resolve(options.primaryHome ?? os.homedir())
|
||
const envTempParent = process.env.ORCA_CODEX_VALIDATION_TEMP_PARENT?.trim()
|
||
const tempParent = path.resolve(options.tempParent ?? (envTempParent || os.tmpdir()))
|
||
// Canonicalize containment paths so symlinked temp parent cannot place disposable root inside primary home.
|
||
const [primaryHomeReal, tempParentReal] = await Promise.all([
|
||
resolveRealPath(primaryHome),
|
||
resolveRealPath(tempParent)
|
||
])
|
||
// Windows TEMP usually lies inside USERPROFILE, violating disposable-home containment; reject before creation with override guidance.
|
||
if (samePath(tempParentReal, primaryHomeReal) || isWithin(tempParentReal, primaryHomeReal)) {
|
||
throw new Error(
|
||
`Refusing to place the disposable validation root inside the primary home (${primaryHome}). ` +
|
||
'Pass --temp-parent <dir> or set ORCA_CODEX_VALIDATION_TEMP_PARENT to a directory outside it.'
|
||
)
|
||
}
|
||
const tempRoot = await mkdtemp(path.join(tempParent, 'orca-codex-real-'))
|
||
const homeDir = path.join(tempRoot, 'home')
|
||
const userDataDir = path.join(tempRoot, 'user-data')
|
||
await Promise.all([
|
||
mkdir(homeDir, { recursive: true, mode: 0o700 }),
|
||
mkdir(userDataDir, { recursive: true, mode: 0o700 })
|
||
])
|
||
const homeDirReal = await resolveRealPath(homeDir)
|
||
if (samePath(primaryHomeReal, homeDirReal) || isWithin(homeDirReal, primaryHomeReal)) {
|
||
throw new Error('Refusing to place the disposable validation home inside the primary home')
|
||
}
|
||
return { primaryHome, tempRoot, homeDir, userDataDir }
|
||
}
|
||
|
||
async function seedCompletedProfile(layout) {
|
||
// Suppress first-run education/telemetry overlays so account controls remain testable.
|
||
const profile = {
|
||
settings: {
|
||
telemetry: {
|
||
optedIn: true,
|
||
installId: '00000000-0000-4000-8000-000000000000',
|
||
existedBeforeTelemetryRelease: false
|
||
}
|
||
},
|
||
onboarding: { flowVersion: 2, closedAt: 1, outcome: 'completed', lastCompletedStep: 3 },
|
||
ui: { contextualToursAutoEligible: false, projectOrderManualDefaultNoticeDismissed: true }
|
||
}
|
||
await writeFile(
|
||
path.join(layout.userDataDir, 'orca-data.json'),
|
||
`${JSON.stringify(profile, null, 2)}\n`
|
||
)
|
||
}
|
||
|
||
async function installCodexConfigTemplate(layout, templatePath) {
|
||
if (!templatePath) {
|
||
return
|
||
}
|
||
const resolvedTemplate = await realpath(path.resolve(templatePath))
|
||
const primaryCodexHome = path.join(layout.primaryHome, '.codex')
|
||
// Never bootstrap validation from user’s live Codex config, even when supplied accidentally.
|
||
if (isWithin(resolvedTemplate, primaryCodexHome)) {
|
||
throw new Error('Refusing to copy a config template from the primary ~/.codex')
|
||
}
|
||
await mkdir(path.join(layout.homeDir, '.codex'), { recursive: true, mode: 0o700 })
|
||
await copyFile(resolvedTemplate, path.join(layout.homeDir, '.codex', 'config.toml'))
|
||
}
|
||
|
||
async function fingerprintFile(filePath) {
|
||
try {
|
||
const stat = await lstat(filePath)
|
||
if (!stat.isFile()) {
|
||
return { exists: true, type: stat.isSymbolicLink() ? 'symlink' : 'other' }
|
||
}
|
||
const contents = await readFile(filePath)
|
||
return {
|
||
exists: true,
|
||
type: 'file',
|
||
size: stat.size,
|
||
mtimeMs: stat.mtimeMs,
|
||
sha256: createHash('sha256').update(contents).digest('hex')
|
||
}
|
||
} catch (error) {
|
||
if (error?.code === 'ENOENT') {
|
||
return { exists: false }
|
||
}
|
||
throw error
|
||
}
|
||
}
|
||
|
||
async function inventoryTree(rootPath) {
|
||
const entries = []
|
||
async function visit(absolutePath, relativePath) {
|
||
const stat = await lstat(absolutePath)
|
||
const type = stat.isDirectory()
|
||
? 'directory'
|
||
: stat.isFile()
|
||
? 'file'
|
||
: stat.isSymbolicLink()
|
||
? 'symlink'
|
||
: 'other'
|
||
entries.push({ path: relativePath || '.', type, size: stat.size, mtimeMs: stat.mtimeMs })
|
||
if (type !== 'directory') {
|
||
return
|
||
}
|
||
const children = await readdir(absolutePath)
|
||
children.sort((left, right) => left.localeCompare(right))
|
||
for (const child of children) {
|
||
await visit(
|
||
path.join(absolutePath, child),
|
||
relativePath ? path.join(relativePath, child) : child
|
||
)
|
||
}
|
||
}
|
||
try {
|
||
await visit(rootPath, '')
|
||
} catch (error) {
|
||
if (error?.code === 'ENOENT') {
|
||
return []
|
||
}
|
||
throw error
|
||
}
|
||
return entries
|
||
}
|
||
|
||
async function snapshotManagedHomes(userDataDir) {
|
||
const accountsRoot = path.join(userDataDir, 'codex-accounts')
|
||
let accountNames = []
|
||
try {
|
||
accountNames = (await readdir(accountsRoot, { withFileTypes: true }))
|
||
.filter((entry) => entry.isDirectory())
|
||
.map((entry) => entry.name)
|
||
.sort((left, right) => left.localeCompare(right))
|
||
} catch (error) {
|
||
if (error?.code !== 'ENOENT') {
|
||
throw error
|
||
}
|
||
}
|
||
return Promise.all(
|
||
accountNames.map(async (accountId) => {
|
||
const homePath = path.join(accountsRoot, accountId, 'home')
|
||
return {
|
||
accountId,
|
||
homePath,
|
||
inventory: await inventoryTree(homePath),
|
||
auth: await fingerprintFile(path.join(homePath, 'auth.json'))
|
||
}
|
||
})
|
||
)
|
||
}
|
||
|
||
export async function snapshotValidationState(layout) {
|
||
const throwawayCodexHome = path.join(layout.homeDir, '.codex')
|
||
return {
|
||
capturedAt: new Date().toISOString(),
|
||
throwawayCodex: {
|
||
auth: await fingerprintFile(path.join(throwawayCodexHome, 'auth.json')),
|
||
config: await fingerprintFile(path.join(throwawayCodexHome, 'config.toml')),
|
||
hooks: await fingerprintFile(path.join(throwawayCodexHome, 'hooks.json'))
|
||
},
|
||
managedHomes: await snapshotManagedHomes(layout.userDataDir),
|
||
sharedRuntimeAuth: await fingerprintFile(
|
||
path.join(layout.userDataDir, 'codex-runtime-home', 'home', 'auth.json')
|
||
)
|
||
}
|
||
}
|
||
|
||
function parseArgs(argv) {
|
||
const options = {
|
||
scenario: 'mixed',
|
||
dryRun: false,
|
||
closeAfterLaunch: false,
|
||
skipBuild: false,
|
||
keep: false,
|
||
reportPath: null,
|
||
primaryHome: os.homedir(),
|
||
configTemplate: null,
|
||
tempParent: null,
|
||
laneAwareContainment: false
|
||
}
|
||
for (let index = 0; index < argv.length; index += 1) {
|
||
const arg = argv[index]
|
||
const readValue = () => {
|
||
const value = argv[index + 1]
|
||
if (!value || value.startsWith('--')) {
|
||
throw new Error(`Missing value for ${arg}`)
|
||
}
|
||
index += 1
|
||
return value
|
||
}
|
||
if (arg === '--scenario') {
|
||
options.scenario = readValue()
|
||
} else if (arg === '--report') {
|
||
options.reportPath = path.resolve(readValue())
|
||
} else if (arg === '--primary-home') {
|
||
options.primaryHome = path.resolve(readValue())
|
||
} else if (arg === '--config-template') {
|
||
options.configTemplate = path.resolve(readValue())
|
||
} else if (arg === '--temp-parent') {
|
||
options.tempParent = path.resolve(readValue())
|
||
} else if (arg === '--dry-run') {
|
||
options.dryRun = true
|
||
} else if (arg === '--close-after-launch') {
|
||
options.closeAfterLaunch = true
|
||
} else if (arg === '--skip-build') {
|
||
options.skipBuild = true
|
||
} else if (arg === '--keep') {
|
||
options.keep = true
|
||
} else if (arg === '--lane-aware-containment') {
|
||
// Windows native Codex ignores USERPROFILE, preventing zero-event real-home isolation; allow only documented volatile churn, fail other writes. macOS proves zero events.
|
||
options.laneAwareContainment = true
|
||
} else if (arg === '--help') {
|
||
console.log(
|
||
'Usage: node config/scripts/run-codex-real-account-validation.mjs [--scenario mixed|managed-only|codex-lb] [--config-template <path>] [--temp-parent <dir>] [--skip-build] [--dry-run] [--close-after-launch] [--keep] [--lane-aware-containment] [--report <path>]'
|
||
)
|
||
process.exit(0)
|
||
} else {
|
||
throw new Error(`Unknown argument: ${arg}`)
|
||
}
|
||
}
|
||
if (!VALID_SCENARIOS.has(options.scenario)) {
|
||
throw new Error(`Invalid scenario: ${options.scenario}`)
|
||
}
|
||
if (options.scenario === 'codex-lb' && !options.configTemplate) {
|
||
throw new Error('The codex-lb scenario requires --config-template outside primary ~/.codex')
|
||
}
|
||
return options
|
||
}
|
||
|
||
// Run local electron-vite JS with current Node; Windows npx.cmd cannot launch through execFileSync without shell.
|
||
export function resolveElectronViteBuildCommand(repoRoot) {
|
||
const electronViteEntry = path.join(
|
||
repoRoot,
|
||
'node_modules',
|
||
'electron-vite',
|
||
'bin',
|
||
'electron-vite.js'
|
||
)
|
||
if (!existsSync(electronViteEntry)) {
|
||
throw new Error(
|
||
`Cannot build the validation app: electron-vite entry not found at ${electronViteEntry}. ` +
|
||
'Install dependencies (pnpm install) or pass --skip-build with a prebuilt out/main/index.js.'
|
||
)
|
||
}
|
||
return { command: process.execPath, args: [electronViteEntry, 'build', '--mode', 'e2e'] }
|
||
}
|
||
|
||
function buildAppIfNeeded(repoRoot, skipBuild) {
|
||
const mainPath = path.join(repoRoot, 'out', 'main', 'index.js')
|
||
if (skipBuild) {
|
||
if (!existsSync(mainPath)) {
|
||
throw new Error(`--skip-build requested, but ${mainPath} does not exist`)
|
||
}
|
||
return mainPath
|
||
}
|
||
const { command, args } = resolveElectronViteBuildCommand(repoRoot)
|
||
execFileSync(command, args, {
|
||
cwd: repoRoot,
|
||
stdio: 'inherit',
|
||
env: { ...process.env, VITE_EXPOSE_STORE: 'true' }
|
||
})
|
||
return mainPath
|
||
}
|
||
|
||
function validationCliCommand() {
|
||
if (process.env.ORCA_VALIDATION_CLI) {
|
||
return process.env.ORCA_VALIDATION_CLI
|
||
}
|
||
if (process.env.ORCA_CLI_COMMAND) {
|
||
return process.env.ORCA_CLI_COMMAND
|
||
}
|
||
return process.platform === 'linux' ? 'orca-ide' : 'orca'
|
||
}
|
||
|
||
async function probeTerminalEnvironment(terminalHandle, launchEnv) {
|
||
const marker = `__ORCA_CODEX_VALIDATION_${randomUUID()}__`
|
||
const command = [
|
||
'node -e',
|
||
`"console.log('${marker}:' + JSON.stringify({home: require('node:os').homedir(), codexHome: process.env.CODEX_HOME || null, orcaCodexHome: process.env.ORCA_CODEX_HOME || null}))"`
|
||
].join(' ')
|
||
const cli = validationCliCommand()
|
||
execFileSync(
|
||
cli,
|
||
['terminal', 'send', '--terminal', terminalHandle, '--text', command, '--enter', '--json'],
|
||
{ env: launchEnv, stdio: 'pipe' }
|
||
)
|
||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||
const output = execFileSync(cli, ['terminal', 'read', '--terminal', terminalHandle, '--json'], {
|
||
env: launchEnv,
|
||
encoding: 'utf8'
|
||
})
|
||
const match = new RegExp(`${marker}:(\\{[^\\r\\n]+\\})`).exec(output)
|
||
if (match?.[1]) {
|
||
return JSON.parse(match[1])
|
||
}
|
||
await new Promise((resolve) => setTimeout(resolve, 250))
|
||
}
|
||
throw new Error(`Timed out waiting for environment probe in terminal ${terminalHandle}`)
|
||
}
|
||
|
||
async function writeReport(reportPath, report) {
|
||
await mkdir(path.dirname(reportPath), { recursive: true })
|
||
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600 })
|
||
}
|
||
|
||
async function runInteractiveSession(context) {
|
||
if (process.stdin.readableEnded) {
|
||
return
|
||
}
|
||
const prompt = readline.createInterface({ input: process.stdin, output: process.stdout })
|
||
const closePrompt = () => prompt.close()
|
||
// Race question against stdin EOF so redirected input still unwinds app/credential cleanup.
|
||
const inputClosed = new Promise((resolve) => prompt.once('close', () => resolve(null)))
|
||
context.signal.addEventListener('abort', closePrompt, { once: true })
|
||
console.log('Commands: checkpoint <label>, probe <terminal-handle>, status, done')
|
||
try {
|
||
while (!context.signal.aborted) {
|
||
const answer = await Promise.race([prompt.question('codex-validation> '), inputClosed])
|
||
if (answer === null) {
|
||
return
|
||
}
|
||
const line = answer.trim()
|
||
const [command, ...rest] = line.split(/\s+/)
|
||
if (command === 'checkpoint') {
|
||
const label = rest.join(' ') || `checkpoint-${context.report.checkpoints.length + 1}`
|
||
context.report.checkpoints.push({
|
||
label,
|
||
...(await snapshotValidationState(context.layout)),
|
||
primaryTripwire: context.tripwire.getStatus()
|
||
})
|
||
await writeReport(context.reportPath, context.report)
|
||
console.log(`Recorded ${label}`)
|
||
} else if (command === 'probe') {
|
||
const terminalHandle = rest[0]
|
||
if (!terminalHandle) {
|
||
console.log('Usage: probe <terminal-handle>')
|
||
continue
|
||
}
|
||
const environment = await probeTerminalEnvironment(terminalHandle, context.launchEnv)
|
||
context.report.terminalProbes.push({
|
||
capturedAt: new Date().toISOString(),
|
||
terminalHandle,
|
||
environment
|
||
})
|
||
await writeReport(context.reportPath, context.report)
|
||
console.log(JSON.stringify(environment, null, 2))
|
||
} else if (command === 'status') {
|
||
console.log(JSON.stringify(context.tripwire.getStatus(), null, 2))
|
||
} else if (command === 'done') {
|
||
return
|
||
} else if (command) {
|
||
console.log('Commands: checkpoint <label>, probe <terminal-handle>, status, done')
|
||
}
|
||
}
|
||
} catch (error) {
|
||
if (!context.signal.aborted) {
|
||
throw error
|
||
}
|
||
} finally {
|
||
context.signal.removeEventListener('abort', closePrompt)
|
||
prompt.close()
|
||
}
|
||
}
|
||
|
||
async function main() {
|
||
const options = parseArgs(process.argv.slice(2))
|
||
const repoRoot = process.cwd()
|
||
const layout = await createValidationLayout({
|
||
primaryHome: options.primaryHome,
|
||
tempParent: options.tempParent ?? undefined
|
||
})
|
||
const reportPath =
|
||
options.reportPath ??
|
||
path.join(os.tmpdir(), `orca-codex-real-account-${options.scenario}-${Date.now()}.json`)
|
||
const launchEnv = createValidationEnv(process.env, layout)
|
||
let app = null
|
||
let tripwire = null
|
||
const abortController = new AbortController()
|
||
const abortForSignal = () => abortController.abort()
|
||
process.once('SIGINT', abortForSignal)
|
||
process.once('SIGTERM', abortForSignal)
|
||
const report = {
|
||
scenario: options.scenario,
|
||
startedAt: new Date().toISOString(),
|
||
reportVersion: 1,
|
||
disposableHome: layout.homeDir,
|
||
userDataDir: layout.userDataDir,
|
||
primaryCodexHome: path.join(layout.primaryHome, '.codex'),
|
||
electronPaths: null,
|
||
checkpoints: [],
|
||
terminalProbes: [],
|
||
tripwire: null
|
||
}
|
||
|
||
try {
|
||
await seedCompletedProfile(layout)
|
||
await installCodexConfigTemplate(layout, options.configTemplate)
|
||
tripwire = await startCodexPrimaryHomeTripwire({
|
||
primaryHome: layout.primaryHome,
|
||
onChange: (event) => {
|
||
if (
|
||
options.laneAwareContainment &&
|
||
classifyCodexHomeTripwireEvent(event) === 'designed-system-default'
|
||
) {
|
||
console.warn(
|
||
'[LANE-DESIGNED] Real ~/.codex volatile churn from the system-default codex lane (recorded, not a violation)'
|
||
)
|
||
console.warn(JSON.stringify(event, null, 2))
|
||
return
|
||
}
|
||
console.error('\u001b[31;1m[VALIDATION ABORTED] Primary ~/.codex changed\u001b[0m')
|
||
console.error(JSON.stringify(event, null, 2))
|
||
abortController.abort()
|
||
}
|
||
})
|
||
report.checkpoints.push({
|
||
label: 'prepared',
|
||
...(await snapshotValidationState(layout)),
|
||
primaryTripwire: tripwire.getStatus()
|
||
})
|
||
await writeReport(reportPath, report)
|
||
console.log(`Disposable HOME: ${layout.homeDir}`)
|
||
console.log(`Disposable userData: ${layout.userDataDir}`)
|
||
console.log(`Sanitized report: ${reportPath}`)
|
||
|
||
if (!options.dryRun) {
|
||
const mainPath = buildAppIfNeeded(repoRoot, options.skipBuild)
|
||
app = await electron.launch({
|
||
args: [mainPath],
|
||
// Why: a validation run must not pull the window over the developer's work.
|
||
env: { ...launchEnv, ORCA_BACKGROUND_LAUNCH: '1' }
|
||
})
|
||
report.electronPaths = await app.evaluate(({ app: electronApp }) => ({
|
||
home: electronApp.getPath('home'),
|
||
userData: electronApp.getPath('userData'),
|
||
nodeHome: process.getBuiltinModule('node:os').homedir()
|
||
}))
|
||
if (
|
||
!samePath(report.electronPaths.home, layout.homeDir) ||
|
||
!samePath(report.electronPaths.nodeHome, layout.homeDir) ||
|
||
!samePath(report.electronPaths.userData, layout.userDataDir)
|
||
) {
|
||
throw new Error('Electron escaped the disposable validation boundary')
|
||
}
|
||
app.process().once('exit', () => abortController.abort())
|
||
await writeReport(reportPath, report)
|
||
if (!options.closeAfterLaunch) {
|
||
await runInteractiveSession({
|
||
layout,
|
||
launchEnv,
|
||
report,
|
||
reportPath,
|
||
tripwire,
|
||
signal: abortController.signal
|
||
})
|
||
}
|
||
}
|
||
} finally {
|
||
abortController.abort()
|
||
try {
|
||
await closeValidationElectronApp(app)
|
||
await cleanupValidationDaemons(layout.userDataDir)
|
||
if (tripwire) {
|
||
const tripwireStatus = await tripwire.stop()
|
||
report.tripwire = options.laneAwareContainment
|
||
? { ...tripwireStatus, laneAware: summarizeLaneAwareTripwire(tripwireStatus.events) }
|
||
: tripwireStatus
|
||
}
|
||
report.checkpoints.push({ label: 'shutdown', ...(await snapshotValidationState(layout)) })
|
||
report.completedAt = new Date().toISOString()
|
||
await writeReport(reportPath, report)
|
||
} finally {
|
||
try {
|
||
if (options.keep) {
|
||
console.warn(`Credential-bearing disposable root kept at ${layout.tempRoot}`)
|
||
} else {
|
||
// Retry removal: detached Codex descendant can briefly lock disposable home on Windows; never strand credentials.
|
||
await rm(layout.tempRoot, {
|
||
recursive: true,
|
||
force: true,
|
||
maxRetries: 10,
|
||
retryDelay: 250
|
||
})
|
||
console.log('Removed the credential-bearing disposable root.')
|
||
}
|
||
} finally {
|
||
process.removeListener('SIGINT', abortForSignal)
|
||
process.removeListener('SIGTERM', abortForSignal)
|
||
}
|
||
}
|
||
}
|
||
|
||
const tripwireFailed = report.tripwire
|
||
? options.laneAwareContainment
|
||
? report.tripwire.laneAware.violations.length > 0
|
||
: !report.tripwire.clean
|
||
: false
|
||
if (tripwireFailed) {
|
||
process.exitCode = 2
|
||
} else {
|
||
if (options.laneAwareContainment && report.tripwire?.events.length) {
|
||
console.warn(
|
||
`Lane-aware containment: ${report.tripwire.laneAware.designedLaneEvents.length} designed system-default event(s) recorded, 0 violations. Review them in the report.`
|
||
)
|
||
}
|
||
console.log(`Validation harness complete. Report: ${reportPath}`)
|
||
}
|
||
}
|
||
|
||
function summarizeLaneAwareTripwire(events) {
|
||
const designedLaneEvents = []
|
||
const violations = []
|
||
for (const event of events) {
|
||
if (classifyCodexHomeTripwireEvent(event) === 'designed-system-default') {
|
||
designedLaneEvents.push(event)
|
||
} else {
|
||
violations.push(event)
|
||
}
|
||
}
|
||
return { designedLaneEvents, violations }
|
||
}
|
||
|
||
const invokedPath = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : null
|
||
if (invokedPath === import.meta.url) {
|
||
main().catch((error) => {
|
||
console.error(error)
|
||
process.exit(1)
|
||
})
|
||
}
|