Files
orca/src/main/agent-cli-version-probe.ts
T
Brennan Benson c0e2b73395 feat(native-chat): require Codex 0.136.0 for chats (#26684)
* Require Codex 0.136.0 for native chats

* Add host-owned Codex install and update actions

* Bind Codex checks and maintenance to the selected installation

* Resolve Windows updater cleanup through system helpers

* Use one child environment for Codex checks and launch

* Expire Codex readiness and reconcile current host jobs

* Resolve Codex checks against the child command search path

* Restrict global npm repair to the selected Codex install layout

* Pin the host updater directory through Windows supervision

* Synchronize Codex maintenance fixtures on readiness

* Unify Codex notices and recover version-refused chats

* Route Codex recovery to the operation that remains

* Preserve Codex setup execution errors in the job log

* Preserve UTF-8 in Codex setup logs

* Remove unused Codex host installation detector

* Require host administration for Codex maintenance

* Show a missing Codex explanation once

* test(native-chat): give scripted Codex runtimes their installation fact; keep the registry out of the relay

Runtime tests that drive a scripted Codex app-server still ran the new
version check against whatever codex was on the machine's PATH: refused
on CI (no codex), and on a dev machine it ran the real codex --version.
A scripted transport now must state the Codex it stands for
(readCodexInstallation), asserted at install, and the tests get it from
one scriptedCodexTransport fixture.

The relay's Codex maintenance path reached @orca/windows-registry through
resolvePathEnvKey's re-export in windows-environment-path; import it from
its registry-free module instead.

Also update the catalog-probe test for the shared child environment.

* refactor(native-chat): drop the Update Codex action; keep Install for a missing Codex

A too-old Codex now gets only the plain message ("Codex X is too old for
chats. Update to 0.136.0 or newer.") with no button and no job. The
existing rechecks (window focus, the <=30 s expiry of a saved result)
retry the refused chat once the user has updated Codex themselves.

A missing Codex keeps the Install Codex button: `npm install -g
@openai/codex` with the npm on the chat's PATH, as a host-run job with
its log, then a recheck and recovery. A configured Command that is
missing gets the message only, since a global npm install would not
land at that path.

Removed: the `codex update` command and its 0.126 floor, manual
instructions, npm layout and launcher identity detection, the action
descriptor (install|update) and its titles, the legacy capability, and
the SSH relay maintenance path (structured chat never runs over SSH or
WSL), so the relay preflight handler is back to main's.

* refactor(native-chat): keep Install Codex in Settings only

When Codex isn't installed, main already hides Codex from the New tab menu,
so the in-chat Install button was almost never reachable. Settings → Agents
already shows "Codex isn't installed." with Install Codex.

- Chats and the new-workspace composer now only state "Codex isn't
  installed." (no button, no install job from the chat).
- The Install action, its busy spinner and the host-busy subscription move
  into the Settings Codex row, the only place that starts the job.
- The store no longer re-checks every other workspace context when a job
  finishes. A refused chat recovers through the same re-check a too-old
  Codex uses (window focus, or the <=30 s expiry of a saved result).
- The chat recovery key no longer includes the install job id.
- Tests: Settings drives the install; a new test proves a chat refused for
  a missing Codex recovers through focus after a Settings install, not
  through the job; the expiry recovery test covers missing and too old.

* refactor(codex): check chat compatibility when acquiring the child

* Restore workspace submit guard and simplify Codex maintenance

* Keep only the Codex chat minimum version
2026-10-12 14:30:27 -07:00

80 lines
2.8 KiB
TypeScript

import {
hasReachedAppVersion,
isPrereleaseAppVersion,
parseCliVersion
} from '../shared/app-version'
import { runProcess } from '@orca/process-host'
import type { ProcessSpec } from '@orca/process-host/process-spec'
/** One line naming why a probe said no; the binary's own words, bounded. */
function warnRefused(program: string, why: string, output = ''): void {
const said = output.trim().replace(/\s+/g, ' ').slice(0, 200)
console.warn(`[agent-cli-version] ${program} --version: ${why}${said ? `: ${said}` : ''}`)
}
/** Runs `<program> --version` on this host and asks `supports` about the version it prints. False
* when it cannot tell: a failed spawn, a non-zero exit, a timeout, or output with no version. */
export async function probeAgentCliVersion(
input: Pick<ProcessSpec, 'program' | 'cwd' | 'env'>,
supports: (version: string) => boolean
): Promise<boolean> {
const result = await readAgentCliVersion(input)
if (result.version === null) {
return false
}
if (!supports(result.version)) {
warnRefused(input.program, `${result.version} is not a supported release`)
return false
}
console.info(`[agent-cli-version] ${input.program} --version: ${result.version} is supported`)
return true
}
export async function readAgentCliVersion(
input: Pick<ProcessSpec, 'program' | 'cwd' | 'env'>,
parseVersion: typeof parseCliVersion = parseCliVersion
): Promise<{ version: string | null }> {
let result
try {
result = await runProcess({
...input,
args: ['--version'],
timeoutMs: 5_000,
maxOutputBytes: 4_096,
killOnOutputLimit: true
})
} catch (error) {
warnRefused(input.program, `did not start (${String(error)})`)
return { version: null }
}
const output = `${result.stdout}\n${result.stderr}`
if (result.timedOut || result.outputTruncated || result.code !== 0) {
const why = result.timedOut
? 'timed out'
: result.outputTruncated
? 'printed too much'
: `exited ${result.code ?? result.signal}`
warnRefused(input.program, why, output)
return { version: null }
}
const version = parseVersion(parseVersion === parseCliVersion ? result.stdout : output)
if (version === null) {
warnRefused(input.program, 'printed no version', output)
return { version: null }
}
return { version }
}
/** A stable release at or after `floor`, on any later major line too. */
export function isStableCliVersionFrom(version: string, floor: string): boolean {
return !isPrereleaseAppVersion(version) && hasReachedAppVersion(version, floor)
}
/** A stable release on `major`'s line, at or after `floor`. */
export function isStableCliVersionOnLine(
version: string,
line: { major: number; floor: string }
): boolean {
return version.startsWith(`${line.major}.`) && isStableCliVersionFrom(version, line.floor)
}