From 10173951192de6a714c32769839d9101932d8ba0 Mon Sep 17 00:00:00 2001 From: Daniil Shushakov <4shushakov@gmail.com> Date: Fri, 28 Aug 2026 16:58:16 +0300 Subject: [PATCH 1/6] Reproduce managed Windows Codex runtime failure --- .github/workflows/ci.yml | 16 ++++ scripts/smoke-managed-codex-acp.mjs | 113 ++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 scripts/smoke-managed-codex-acp.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2518fb49..d7770b02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,22 @@ jobs: - run: npm run build:typescript-deps - run: npm run test --workspaces --if-present + windows-codex-runtime: + name: Managed Windows Codex runtime + runs-on: windows-2025 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version-file: .node-version + - name: Install the pinned managed runtime + working-directory: openaide-rs/app-server/assets/codex-acp-runtime + run: npm ci --ignore-scripts --omit=dev --no-audit --no-fund + - name: Smoke-test adapter and native Codex process + run: >- + node scripts/smoke-managed-codex-acp.mjs + openaide-rs/app-server/assets/codex-acp-runtime + smoke-tests: name: Task Chat smoke tests runs-on: ubuntu-24.04 diff --git a/scripts/smoke-managed-codex-acp.mjs b/scripts/smoke-managed-codex-acp.mjs new file mode 100644 index 00000000..955cccda --- /dev/null +++ b/scripts/smoke-managed-codex-acp.mjs @@ -0,0 +1,113 @@ +import { spawn } from "node:child_process"; +import { access } from "node:fs/promises"; +import path from "node:path"; + +const [runtimeRoot] = process.argv.slice(2); +if (!runtimeRoot) { + throw new Error("Usage: node scripts/smoke-managed-codex-acp.mjs "); +} +if (process.platform !== "win32" || process.arch !== "x64") { + throw new Error("The managed Windows Codex smoke requires Windows x64"); +} + +const adapterPath = path.resolve( + runtimeRoot, + "node_modules/@openaide/codex-acp/dist/index.js", +); +const codexPath = path.resolve( + runtimeRoot, + "node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc/bin/codex.exe", +); +await Promise.all([access(adapterPath), access(codexPath)]); + +const child = spawn(process.execPath, [adapterPath], { + env: { + ...process.env, + CODEX_PATH: codexPath, + NO_BROWSER: "1", + }, + stdio: "pipe", + windowsHide: true, +}); + +let stderr = ""; +let stdoutBuffer = ""; +let exitDescription; +const pending = new Map(); +child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; }); +child.stdout.setEncoding("utf8").on("data", (chunk) => { + stdoutBuffer += chunk; + for (;;) { + const newline = stdoutBuffer.indexOf("\n"); + if (newline < 0) break; + const line = stdoutBuffer.slice(0, newline).trim(); + stdoutBuffer = stdoutBuffer.slice(newline + 1); + if (!line) continue; + let message; + try { message = JSON.parse(line); } catch { continue; } + const waiter = pending.get(String(message.id)); + if (!waiter) continue; + pending.delete(String(message.id)); + waiter.resolve(message); + } +}); + +const closed = new Promise((resolve) => { + child.once("close", (code, signal) => { + exitDescription = `code=${code}, signal=${signal}`; + for (const waiter of pending.values()) { + waiter.reject(new Error(`Codex ACP exited before responding (${exitDescription})`)); + } + pending.clear(); + resolve(); + }); +}); +child.once("error", (error) => { + for (const waiter of pending.values()) waiter.reject(error); + pending.clear(); +}); +child.stdin.on("error", () => {}); + +let nextRequestId = 1; +function request(method, params, timeoutMs = 20_000) { + const id = String(nextRequestId++); + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + pending.delete(id); + reject(new Error(`${method} timed out after ${timeoutMs}ms`)); + }, timeoutMs); + pending.set(id, { + resolve: (value) => { clearTimeout(timeout); resolve(value); }, + reject: (error) => { clearTimeout(timeout); reject(error); }, + }); + child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`); + }); +} + +try { + const initialized = await request("initialize", { + protocolVersion: 1, + clientCapabilities: {}, + }); + if (initialized.error || initialized.result?.protocolVersion !== 1) { + throw new Error(`Codex ACP initialization failed: ${JSON.stringify(initialized.error)}`); + } + + // Authentication may legitimately reject listing on a clean runner. The + // release invariant is that the managed native Codex process stays alive and + // returns an ACP response instead of disappearing at this boundary. + await request("session/list", { cwd: process.cwd() }); + if (exitDescription) { + throw new Error(`Codex ACP exited during session listing (${exitDescription})`); + } + console.log("Verified the managed Windows Codex ACP process through session listing."); +} catch (error) { + throw new Error(`${error.message}; Codex ACP stderr: ${stderr.slice(0, 4_000)}`); +} finally { + if (!child.stdin.destroyed) child.stdin.end(); + await Promise.race([ + closed, + new Promise((resolve) => setTimeout(resolve, 2_000)), + ]); + if (child.exitCode === null) child.kill(); +} From 69f9714214038c29b6df45dd5ca350a71878ed02 Mon Sep 17 00:00:00 2001 From: Daniil Shushakov <4shushakov@gmail.com> Date: Fri, 28 Aug 2026 17:00:20 +0300 Subject: [PATCH 2/6] Exercise App Server Windows Codex launch --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7770b02..b7eaa4f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,10 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version-file: .node-version + - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c + with: + toolchain: 1.97.1 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Install the pinned managed runtime working-directory: openaide-rs/app-server/assets/codex-acp-runtime run: npm ci --ignore-scripts --omit=dev --no-audit --no-fund @@ -74,6 +78,13 @@ jobs: run: >- node scripts/smoke-managed-codex-acp.mjs openaide-rs/app-server/assets/codex-acp-runtime + - name: Build the App Server boundary + run: cargo build --locked -p openaide-app-server + - name: Smoke-test App Server managed Codex launch + run: >- + node scripts/smoke-packaged-codex-acp.mjs + target/debug/openaide-app-server.exe + ${{ github.workspace }} smoke-tests: name: Task Chat smoke tests From 175d57811a52e778b3114be116c4a38d26954ac7 Mon Sep 17 00:00:00 2001 From: Daniil Shushakov <4shushakov@gmail.com> Date: Fri, 28 Aug 2026 17:05:45 +0300 Subject: [PATCH 3/6] Instrument Windows Codex process boundary --- .github/workflows/ci.yml | 2 ++ openaide-rs/app-server/src/agent/acp_agent_config.rs | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7eaa4f3..e6ff904a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,8 @@ jobs: - name: Build the App Server boundary run: cargo build --locked -p openaide-app-server - name: Smoke-test App Server managed Codex launch + env: + OPENAIDE_DEBUG_WINDOWS_CODEX_LAUNCH: "1" run: >- node scripts/smoke-packaged-codex-acp.mjs target/debug/openaide-app-server.exe diff --git a/openaide-rs/app-server/src/agent/acp_agent_config.rs b/openaide-rs/app-server/src/agent/acp_agent_config.rs index cabc89bf..e361e2fc 100644 --- a/openaide-rs/app-server/src/agent/acp_agent_config.rs +++ b/openaide-rs/app-server/src/agent/acp_agent_config.rs @@ -81,6 +81,13 @@ impl AcpAgentConfig { env.extend(self.secret_env_values(host_bridge, secret_resolver)?); let args = process_args(&self.command, &self.args, &env, cfg!(windows)); let agent = AcpAgent::from_args(args).map_err(super::acp_errors::acp_error)?; + let agent = if std::env::var_os("OPENAIDE_DEBUG_WINDOWS_CODEX_LAUNCH").is_some() { + agent.with_debug(|line, direction| { + eprintln!("[DEBUG-a4f2] {direction:?}: {line}"); + }) + } else { + agent + }; Ok(match trace { Some(trace) => agent.with_debug(move |line, direction| { trace.record_line(line, direction); From ae387d6d8992d4863e0cca6d9ead9752fa0c07f3 Mon Sep 17 00:00:00 2001 From: Daniil Shushakov <4shushakov@gmail.com> Date: Fri, 28 Aug 2026 17:11:41 +0300 Subject: [PATCH 4/6] Normalize managed Windows Codex process paths --- .github/workflows/ci.yml | 2 -- .../app-server/src/agent/acp_agent_config.rs | 7 ------- .../src/agent/codex_acp_provisioner.rs | 19 +++++++++++++++---- .../src/agent/codex_acp_provisioner_tests.rs | 14 ++++++++++++-- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6ff904a..b7eaa4f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,8 +81,6 @@ jobs: - name: Build the App Server boundary run: cargo build --locked -p openaide-app-server - name: Smoke-test App Server managed Codex launch - env: - OPENAIDE_DEBUG_WINDOWS_CODEX_LAUNCH: "1" run: >- node scripts/smoke-packaged-codex-acp.mjs target/debug/openaide-app-server.exe diff --git a/openaide-rs/app-server/src/agent/acp_agent_config.rs b/openaide-rs/app-server/src/agent/acp_agent_config.rs index e361e2fc..cabc89bf 100644 --- a/openaide-rs/app-server/src/agent/acp_agent_config.rs +++ b/openaide-rs/app-server/src/agent/acp_agent_config.rs @@ -81,13 +81,6 @@ impl AcpAgentConfig { env.extend(self.secret_env_values(host_bridge, secret_resolver)?); let args = process_args(&self.command, &self.args, &env, cfg!(windows)); let agent = AcpAgent::from_args(args).map_err(super::acp_errors::acp_error)?; - let agent = if std::env::var_os("OPENAIDE_DEBUG_WINDOWS_CODEX_LAUNCH").is_some() { - agent.with_debug(|line, direction| { - eprintln!("[DEBUG-a4f2] {direction:?}: {line}"); - }) - } else { - agent - }; Ok(match trace { Some(trace) => agent.with_debug(move |line, direction| { trace.record_line(line, direction); diff --git a/openaide-rs/app-server/src/agent/codex_acp_provisioner.rs b/openaide-rs/app-server/src/agent/codex_acp_provisioner.rs index 68934bb0..e9abcf2b 100644 --- a/openaide-rs/app-server/src/agent/codex_acp_provisioner.rs +++ b/openaide-rs/app-server/src/agent/codex_acp_provisioner.rs @@ -229,16 +229,18 @@ impl CodexAcpProvisioner { let entrypoint = package_root(version_root).join("dist/index.js"); let mut config = config; config.command = resolved_command_or_name("node"); - config.args = vec![entrypoint.to_string_lossy().into_owned()]; + // Backslashes in an absolute Windows script argument can be consumed + // while the ACP SDK constructs the child command line, leaving Node a + // drive-relative `C:` entrypoint. Node accepts forward slashes on + // Windows, so keep this process boundary unambiguous. + config.args = vec![process_path_argument(&entrypoint, self.windows)]; if self.windows { // The @openai/codex Node launcher can terminate when nested under // codex-acp on Windows. Use its pinned native binary directly. config.env.retain(|(name, _)| name != "CODEX_PATH"); config.env.push(( "CODEX_PATH".to_string(), - windows_codex_binary(version_root) - .to_string_lossy() - .into_owned(), + process_path_argument(&windows_codex_binary(version_root), true), )); } Ok(PreparedCodexAcpLaunch { @@ -342,6 +344,15 @@ fn windows_codex_binary(version_root: &Path) -> PathBuf { .join("vendor/x86_64-pc-windows-msvc/bin/codex.exe") } +fn process_path_argument(path: &Path, windows: bool) -> String { + let value = path.to_string_lossy(); + if windows { + value.replace('\\', "/") + } else { + value.into_owned() + } +} + fn validate_platform_runtime(version_root: &Path, windows: bool) -> Result<(), RuntimeError> { if windows && !windows_codex_binary(version_root).is_file() { return Err(provisioning_error( diff --git a/openaide-rs/app-server/src/agent/codex_acp_provisioner_tests.rs b/openaide-rs/app-server/src/agent/codex_acp_provisioner_tests.rs index 11077d11..d44d81f2 100644 --- a/openaide-rs/app-server/src/agent/codex_acp_provisioner_tests.rs +++ b/openaide-rs/app-server/src/agent/codex_acp_provisioner_tests.rs @@ -5,7 +5,7 @@ use std::time::Duration; use fs2::FileExt; use tempfile::TempDir; -use super::{CodexAcpInstaller, CodexAcpProvisioner, CODEX_ACP_VERSION}; +use super::{process_path_argument, CodexAcpInstaller, CodexAcpProvisioner, CODEX_ACP_VERSION}; use crate::agent::acp_agent_config::AcpAgentConfig; use crate::agent::status_cache::AgentStatusCache; use crate::logging::capture_test_logs; @@ -177,13 +177,23 @@ fn windows_launch_uses_the_managed_native_codex_binary() { .iter() .find(|(name, _)| name == "CODEX_PATH") .map(|(_, value)| value.as_str()), - Some(expected_codex.to_string_lossy().as_ref()), + Some(process_path_argument(&expected_codex, true).as_str()), ); assert_eq!(launch.config.args.len(), 1); assert!(launch.config.args[0].ends_with("node_modules/@openaide/codex-acp/dist/index.js")); assert!(!launch.config.command.ends_with(".cmd")); } +#[test] +fn windows_process_paths_are_unambiguous_to_node() { + let path = std::path::Path::new(r"C:\Users\runneradmin\agent-runtimes\codex-acp\dist\index.js"); + + assert_eq!( + process_path_argument(path, true), + "C:/Users/runneradmin/agent-runtimes/codex-acp/dist/index.js" + ); +} + #[test] fn passive_discovery_can_detect_an_unprovisioned_integration_without_installing_it() { let storage = TempDir::new().expect("temporary storage root"); From 2662dd2ff11f4c3a9d24e3e4e597296d2c4f23d6 Mon Sep 17 00:00:00 2001 From: Daniil Shushakov <4shushakov@gmail.com> Date: Fri, 28 Aug 2026 17:16:18 +0300 Subject: [PATCH 5/6] Trace exact Windows ACP launch arguments --- .github/workflows/ci.yml | 2 ++ openaide-rs/app-server/src/agent/acp_agent_config.rs | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7eaa4f3..e6ff904a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,8 @@ jobs: - name: Build the App Server boundary run: cargo build --locked -p openaide-app-server - name: Smoke-test App Server managed Codex launch + env: + OPENAIDE_DEBUG_WINDOWS_CODEX_LAUNCH: "1" run: >- node scripts/smoke-packaged-codex-acp.mjs target/debug/openaide-app-server.exe diff --git a/openaide-rs/app-server/src/agent/acp_agent_config.rs b/openaide-rs/app-server/src/agent/acp_agent_config.rs index cabc89bf..8bb86523 100644 --- a/openaide-rs/app-server/src/agent/acp_agent_config.rs +++ b/openaide-rs/app-server/src/agent/acp_agent_config.rs @@ -80,7 +80,17 @@ impl AcpAgentConfig { let mut env = self.env.clone(); env.extend(self.secret_env_values(host_bridge, secret_resolver)?); let args = process_args(&self.command, &self.args, &env, cfg!(windows)); + if std::env::var_os("OPENAIDE_DEBUG_WINDOWS_CODEX_LAUNCH").is_some() { + eprintln!("[DEBUG-a4f2] process_args={args:?}"); + } let agent = AcpAgent::from_args(args).map_err(super::acp_errors::acp_error)?; + let agent = if std::env::var_os("OPENAIDE_DEBUG_WINDOWS_CODEX_LAUNCH").is_some() { + agent.with_debug(|line, direction| { + eprintln!("[DEBUG-a4f2] {direction:?}: {line}"); + }) + } else { + agent + }; Ok(match trace { Some(trace) => agent.with_debug(move |line, direction| { trace.record_line(line, direction); From c404bf161e47cd1b96f8e637c1033724952ffb0c Mon Sep 17 00:00:00 2001 From: Daniil Shushakov <4shushakov@gmail.com> Date: Fri, 28 Aug 2026 17:26:01 +0300 Subject: [PATCH 6/6] Strip Windows verbatim prefixes from Node paths --- .github/workflows/ci.yml | 2 -- .../app-server/src/agent/acp_agent_config.rs | 10 ---------- .../src/agent/codex_acp_provisioner.rs | 18 +++++++++++++----- .../src/agent/codex_acp_provisioner_tests.rs | 10 +++++++++- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6ff904a..b7eaa4f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,8 +81,6 @@ jobs: - name: Build the App Server boundary run: cargo build --locked -p openaide-app-server - name: Smoke-test App Server managed Codex launch - env: - OPENAIDE_DEBUG_WINDOWS_CODEX_LAUNCH: "1" run: >- node scripts/smoke-packaged-codex-acp.mjs target/debug/openaide-app-server.exe diff --git a/openaide-rs/app-server/src/agent/acp_agent_config.rs b/openaide-rs/app-server/src/agent/acp_agent_config.rs index 8bb86523..cabc89bf 100644 --- a/openaide-rs/app-server/src/agent/acp_agent_config.rs +++ b/openaide-rs/app-server/src/agent/acp_agent_config.rs @@ -80,17 +80,7 @@ impl AcpAgentConfig { let mut env = self.env.clone(); env.extend(self.secret_env_values(host_bridge, secret_resolver)?); let args = process_args(&self.command, &self.args, &env, cfg!(windows)); - if std::env::var_os("OPENAIDE_DEBUG_WINDOWS_CODEX_LAUNCH").is_some() { - eprintln!("[DEBUG-a4f2] process_args={args:?}"); - } let agent = AcpAgent::from_args(args).map_err(super::acp_errors::acp_error)?; - let agent = if std::env::var_os("OPENAIDE_DEBUG_WINDOWS_CODEX_LAUNCH").is_some() { - agent.with_debug(|line, direction| { - eprintln!("[DEBUG-a4f2] {direction:?}: {line}"); - }) - } else { - agent - }; Ok(match trace { Some(trace) => agent.with_debug(move |line, direction| { trace.record_line(line, direction); diff --git a/openaide-rs/app-server/src/agent/codex_acp_provisioner.rs b/openaide-rs/app-server/src/agent/codex_acp_provisioner.rs index e9abcf2b..c6d265e9 100644 --- a/openaide-rs/app-server/src/agent/codex_acp_provisioner.rs +++ b/openaide-rs/app-server/src/agent/codex_acp_provisioner.rs @@ -229,10 +229,10 @@ impl CodexAcpProvisioner { let entrypoint = package_root(version_root).join("dist/index.js"); let mut config = config; config.command = resolved_command_or_name("node"); - // Backslashes in an absolute Windows script argument can be consumed - // while the ACP SDK constructs the child command line, leaving Node a - // drive-relative `C:` entrypoint. Node accepts forward slashes on - // Windows, so keep this process boundary unambiguous. + // Rust may preserve Windows' `\\?\` verbatim prefix after the managed + // installation is published. Node interprets the slash-normalized + // `//?/C:/...` form as a drive-relative `C:` entrypoint, so hand this + // process boundary an ordinary Node-compatible Windows path. config.args = vec![process_path_argument(&entrypoint, self.windows)]; if self.windows { // The @openai/codex Node launcher can terminate when nested under @@ -347,7 +347,15 @@ fn windows_codex_binary(version_root: &Path) -> PathBuf { fn process_path_argument(path: &Path, windows: bool) -> String { let value = path.to_string_lossy(); if windows { - value.replace('\\', "/") + let normalized = value.replace('\\', "/"); + if let Some(path) = normalized.strip_prefix("//?/UNC/") { + format!("//{path}") + } else { + normalized + .strip_prefix("//?/") + .unwrap_or(&normalized) + .to_string() + } } else { value.into_owned() } diff --git a/openaide-rs/app-server/src/agent/codex_acp_provisioner_tests.rs b/openaide-rs/app-server/src/agent/codex_acp_provisioner_tests.rs index d44d81f2..eee78b3d 100644 --- a/openaide-rs/app-server/src/agent/codex_acp_provisioner_tests.rs +++ b/openaide-rs/app-server/src/agent/codex_acp_provisioner_tests.rs @@ -186,12 +186,20 @@ fn windows_launch_uses_the_managed_native_codex_binary() { #[test] fn windows_process_paths_are_unambiguous_to_node() { - let path = std::path::Path::new(r"C:\Users\runneradmin\agent-runtimes\codex-acp\dist\index.js"); + let path = + std::path::Path::new(r"\\?\C:\Users\runneradmin\agent-runtimes\codex-acp\dist\index.js"); assert_eq!( process_path_argument(path, true), "C:/Users/runneradmin/agent-runtimes/codex-acp/dist/index.js" ); + assert_eq!( + process_path_argument( + std::path::Path::new(r"\\?\UNC\server\share\codex-acp\dist\index.js"), + true, + ), + "//server/share/codex-acp/dist/index.js" + ); } #[test]