diff --git a/src/main/codex/codex-app-server-client.ts b/src/main/codex/codex-app-server-client.ts index 8c95562e66c..efdee5de8bf 100644 --- a/src/main/codex/codex-app-server-client.ts +++ b/src/main/codex/codex-app-server-client.ts @@ -1,4 +1,5 @@ -import { spawn } from 'node:child_process' +import type { ChildProcessHandle, ProcessSpec } from '../../shared/child-process/process-spec' +import { spawnProcess } from '../../shared/child-process/run-process' import { normalizeHookTrustKeyForLookup } from './config-toml-trust' import { runCodexAppServerSession, type CodexAppServerInvocation } from './codex-app-server-session' @@ -105,7 +106,12 @@ function collectHookListings(result: unknown): CodexHookListing[] { */ export async function runCodexHookTrustGrantSession( request: CodexHookTrustGrantRequest, - spawnImpl: typeof spawn = spawn + spawnImpl: ( + program: string, + args: string[], + options: Record + ) => ChildProcessHandle = (program, args, options) => + spawnProcess({ program, args, ...options } as ProcessSpec) ): Promise { return runCodexAppServerSession( request.invocation, diff --git a/src/main/codex/codex-app-server-session.ts b/src/main/codex/codex-app-server-session.ts index 2e176e13ae2..b205f388e93 100644 --- a/src/main/codex/codex-app-server-session.ts +++ b/src/main/codex/codex-app-server-session.ts @@ -1,4 +1,6 @@ -import { spawn, type ChildProcess, type ChildProcessWithoutNullStreams } from 'node:child_process' +import { spawnProcess } from '../../shared/child-process/run-process' +import type { ChildProcessHandle, ProcessSpec } from '../../shared/child-process/process-spec' +import type { ChildProcessWithoutNullStreams } from 'node:child_process' import { waitForProcessExitUntil } from './codex-process-exit-deadline' import { stderrIndicatesMissingAppServer } from './codex-app-server-capability-signal' import { withCliRuntimeOnPath } from '../../shared/node-cli-command-resolution' @@ -68,11 +70,17 @@ const STDERR_TAIL_MAX_BYTES = 8192 const STDOUT_LINE_MAX_BYTES = 1024 * 1024 export function killCodexAppServerProcessTree( - child: Pick, - options: { platform?: NodeJS.Platform; spawnImpl?: typeof spawn } = {} + child: Pick, + options: { + platform?: NodeJS.Platform + spawnImpl?: (program: string, args: string[], options: Record) => ChildProcessHandle + } = {} ): void { const platform = options.platform ?? process.platform - const spawnImpl = options.spawnImpl ?? spawn + const spawnImpl = + options.spawnImpl ?? + ((program: string, args: string[], spawnOptions: Record) => + spawnProcess({ program, args, ...spawnOptions } as ProcessSpec)) if (platform === 'win32' && child.pid) { try { // Why: npm-installed Codex runs behind cmd.exe; killing only that wrapper @@ -139,7 +147,8 @@ export function isCodexMethodNotFoundError(error: unknown): boolean { export async function runCodexAppServerSession( invocation: CodexAppServerInvocation, body: (rpc: CodexAppServerRpc) => Promise, - spawnImpl: typeof spawn = spawn + spawnImpl: (program: string, args: string[], options: Record) => ChildProcessHandle = + (program, args, options) => spawnProcess({ program, args, ...options } as ProcessSpec) ): Promise { // Why: a default-home grant must run against the real ~/.codex, so strip an // inherited CODEX_HOME (envToDelete) after applying the overlay, not before. diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index c8a9c7eb376..d46a845c421 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -724,6 +724,7 @@ import { RUNTIME_CAPABILITIES, RUNTIME_PROTOCOL_VERSION, SESSION_TABS_AUTHORITATIVE_INVENTORY_RUNTIME_CAPABILITY, + STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY, TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY, type RuntimeCapability } from '../../shared/protocol-version' @@ -732,6 +733,7 @@ import { listAiVaultSessions } from '../ai-vault/cached-session-list' import { configureHostReadableTranscriptPathSources } from '../native-chat/host-readable-transcript-path' +import { isWindowsProcessStartTimeAvailable } from '../windows/windows-process-table' import { resolveLocalAiVaultSessionTitles } from '../ai-vault/session-title-resolver' import type { AiVaultListArgs, AiVaultListResult } from '../../shared/ai-vault-types' import type { @@ -6619,7 +6621,13 @@ export class OrcaRuntimeService { (process.env.ORCA_E2E_DISABLE_PAIRED_TERMINAL_PARKING !== '1' || capability !== TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY) && (process.env.ORCA_E2E_DISABLE_AUTHORITATIVE_SESSION_TABS_INVENTORY !== '1' || - capability !== SESSION_TABS_AUTHORITATIVE_INVENTORY_RUNTIME_CAPABILITY) + capability !== SESSION_TABS_AUTHORITATIVE_INVENTORY_RUNTIME_CAPABILITY) && + // Structured ownership on Windows requires the native process-table + // creation-time field; an older host must honestly fall back to the + // terminal bridge rather than advertise an unsafe capability. + (capability !== STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY || + process.platform !== 'win32' || + isWindowsProcessStartTimeAvailable()) ) if (hasOffscreen || hasHeadlessCommands) { capabilities.push(BROWSER_HEADLESS_RUNTIME_CAPABILITY) diff --git a/src/renderer/src/lib/structured-native-chat-availability.test.ts b/src/renderer/src/lib/structured-native-chat-availability.test.ts index 770413208cc..cb6105b3357 100644 --- a/src/renderer/src/lib/structured-native-chat-availability.test.ts +++ b/src/renderer/src/lib/structured-native-chat-availability.test.ts @@ -152,11 +152,9 @@ describe('canUseStructuredNativeChat', () => { expect(canUseStructuredNativeChat(stateFor({ windowsRuntime: 'wsl' }), 'wt-1')).toBe(false) }) - it('keeps Windows-host projects on the terminal path until native start-time proof is advertised', () => { + it('allows Windows-host projects when structured chat is enabled', () => { mockGetRendererAppPlatform.mockReturnValue('win32') - expect(canUseStructuredNativeChat(stateFor({ windowsRuntime: 'windows-host' }), 'wt-1')).toBe( - false - ) + expect(canUseStructuredNativeChat(stateFor({ windowsRuntime: 'windows-host' }), 'wt-1')).toBe(true) }) it('refuses a Windows folder workspace even though its key resolves no project runtime', () => { @@ -166,7 +164,7 @@ describe('canUseStructuredNativeChat', () => { activeRepoId: null, activeWorktreeId: null } as unknown as AppState - expect(canUseStructuredNativeChat(state, 'folder:folder-1')).toBe(false) + expect(canUseStructuredNativeChat(state, 'folder:folder-1')).toBe(true) }) it('allows a folder workspace on a non-Windows platform', () => { diff --git a/src/renderer/src/lib/structured-native-chat-availability.ts b/src/renderer/src/lib/structured-native-chat-availability.ts index bc14ccfd4f0..ff5e5df0177 100644 --- a/src/renderer/src/lib/structured-native-chat-availability.ts +++ b/src/renderer/src/lib/structured-native-chat-availability.ts @@ -1,7 +1,6 @@ import type { AppState } from '@/store/types' import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context' import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner' -import { getRendererAppPlatform } from '@/lib/renderer-app-platform' export function canUseStructuredNativeChat(state: AppState, worktreeId: string): boolean { if (state.settings?.experimentalStructuredNativeChat !== true) { @@ -16,15 +15,8 @@ export function canUseStructuredNativeChat(state: AppState, worktreeId: string): if (getExecutionHostIdForWorktree(state, worktreeId) !== 'local') { return false } - // The shipped Windows process-tree addon may not expose creation time. Until - // the host advertises that proof, refuse every local Windows execution path — - // windows-host, WSL, and keys that resolve no project runtime (folder - // workspaces, floating terminal) — so create cannot fail after the click. - if (getRendererAppPlatform() === 'win32') { - return false - } - // Refuse WSL and repair-required runtimes even if resolution ever runs - // off-win32; the gate must not depend on the resolver's platform guard. + // Refuse WSL and repair-required runtimes; Windows native execution is + // supported when the host advertises the process identity capability. const projectRuntime = getLocalProjectExecutionRuntimeContext(state, worktreeId) return !(projectRuntime?.status === 'repair-required' || projectRuntime?.runtime.kind === 'wsl') } diff --git a/src/shared/child-process/__fixtures__/child-process-import-allowlist.txt b/src/shared/child-process/__fixtures__/child-process-import-allowlist.txt index 988adc08755..86636a69ae2 100644 --- a/src/shared/child-process/__fixtures__/child-process-import-allowlist.txt +++ b/src/shared/child-process/__fixtures__/child-process-import-allowlist.txt @@ -50,7 +50,6 @@ src/main/codex-accounts/legacy-wsl-runtime-auth-drain-recovery-script-harness.ts src/main/codex-accounts/legacy-wsl-runtime-auth-drain-script-harness.ts src/main/codex-accounts/legacy-wsl-runtime-auth-drain-script-interference-shims.ts src/main/codex-accounts/service.ts -src/main/codex/codex-app-server-client.ts src/main/codex/codex-app-server-posix-supervisor.ts src/main/codex/codex-app-server-session.ts src/main/codex/codex-state-db-backfill-recovery.ts