diff --git a/src/codex/app-server-processes.ts b/src/codex/app-server-processes.ts index 85fa17033..aacea3a90 100644 --- a/src/codex/app-server-processes.ts +++ b/src/codex/app-server-processes.ts @@ -350,7 +350,7 @@ export function listWindowsSnapshots(): ProcessSnapshot[] { // executable resolves from the trusted System32 directory (never PATH), and // windowsHide keeps the enumeration console-less on desktop sessions (#1278). const output = execFileSync(resolveTrustedWindowsPowerShellExe(), [ - "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden", + "-NoProfile", "-NoLogo", "-NonInteractive", "-Command", psCommand, ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 8_000, windowsHide: true }); @@ -461,7 +461,7 @@ function readDarwinProcStartMs(pid: number): number | null { function readWindowsProcStartMs(pid: number): number | null { try { const out = execFileSync(resolveTrustedWindowsPowerShellExe(), [ - "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden", + "-NoProfile", "-NoLogo", "-NonInteractive", "-Command", `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate.ToUniversalTime().ToString("o")`, ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 8_000, windowsHide: true }).trim(); @@ -517,7 +517,7 @@ export function readProcessStartMsBatch( try { const filter = pids.map(pid => `ProcessId=${pid}`).join(" OR "); const stdout = execFileSync(resolveTrustedWindowsPowerShellExe(), [ - "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden", + "-NoProfile", "-NoLogo", "-NonInteractive", "-Command", `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId)\t$($_.CreationDate.ToUniversalTime().ToString("o"))" }`, ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true }); diff --git a/src/codex/user-identity.ts b/src/codex/user-identity.ts index eb6d31700..3798d6ec7 100644 --- a/src/codex/user-identity.ts +++ b/src/codex/user-identity.ts @@ -58,8 +58,6 @@ function windowsIdentityPowerShellCommand(expression: string): string[] { "-NoLogo", "-NoProfile", "-NonInteractive", - "-WindowStyle", - "Hidden", "-Command", expression, ]; @@ -105,8 +103,9 @@ function powershellValue(expression: string): string { // `windowsHide` is the popup fix (#1278): the desktop proxy parent runs // without a console, so a console-subsystem child spawned without // CREATE_NO_WINDOW gets a fresh visible console window at startup and on - // every config write. `-WindowStyle Hidden` alone does not stop the - // allocation; the flag behind `windowsHide` does. + // every config write. Do not add PowerShell's `-WindowStyle Hidden` here: + // Bun 1.3.14 can fail that direct CLI combination before the SID command + // executes (#1589); the process-level `windowsHide` flag is sufficient. result = Bun.spawnSync(command, windowsIdentityPowerShellSpawnOptions()); } catch (cause) { refuse("Windows effective-account lookup could not start.", cause); diff --git a/src/config.ts b/src/config.ts index 703a444d8..b71a1a34c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3578,8 +3578,6 @@ function readProcessCommandLine(pid: number): string | undefined { "-NoProfile", "-NoLogo", "-NonInteractive", - "-WindowStyle", - "Hidden", "-Command", `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`, ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000, windowsHide: true }); diff --git a/src/lib/windows-user-principal.ts b/src/lib/windows-user-principal.ts index ecb92ca67..2f18b65eb 100644 --- a/src/lib/windows-user-principal.ts +++ b/src/lib/windows-user-principal.ts @@ -105,8 +105,6 @@ const POWERSHELL_ARGS = [ "-NoLogo", "-NoProfile", "-NonInteractive", - "-WindowStyle", - "Hidden", "-Command", SID_EXPRESSION, ] as const; diff --git a/src/tray/windows.ts b/src/tray/windows.ts index a40f29ca7..df30feff4 100644 --- a/src/tray/windows.ts +++ b/src/tray/windows.ts @@ -138,7 +138,6 @@ export function windowsTrayProcessArgs(entry: WindowsTrayEntry, mode: "Run" | "S "-NonInteractive", "-STA", "-ExecutionPolicy", "Bypass", - "-WindowStyle", "Hidden", "-File", safePath(entry.script), "-BunPath", safePath(entry.bun), "-BunRuntimeSource", entry.bunRuntimeSource, @@ -173,7 +172,6 @@ export function buildWindowsTrayPowerShellCommand(entry: WindowsTrayEntry, power "-NonInteractive", "-STA", "-ExecutionPolicy", "Bypass", - "-WindowStyle", "Hidden", "-File", quoteRunValue(entry.script), "-BunPath", quoteRunValue(entry.bun), "-BunRuntimeSource", entry.bunRuntimeSource, diff --git a/src/update/job.ts b/src/update/job.ts index d507061b8..b615e3b60 100644 --- a/src/update/job.ts +++ b/src/update/job.ts @@ -578,7 +578,7 @@ export function spawnGuiUpdateWorker( ].join("; "); const launched = spawnSync( resolveTrustedWindowsPowerShellExe(), - ["-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", ps], + ["-NoProfile", "-NoLogo", "-NonInteractive", "-Command", ps], { encoding: "utf8", windowsHide: true, timeout: 15_000 }, ); const pid = Number(String(launched.stdout ?? "").trim().split(/\r?\n/).pop()); @@ -1386,7 +1386,7 @@ function killWindowsServiceWrapperProcesses(): void { "} | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }", ].join(" "); spawnSync(resolveTrustedWindowsPowerShellExe(), [ - "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden", + "-NoProfile", "-NoLogo", "-NonInteractive", "-Command", ps, ], { stdio: "ignore", timeout: 5000, windowsHide: true }); } catch { diff --git a/structure/02_config-and-codex-home.md b/structure/02_config-and-codex-home.md index f3346020c..101f94203 100644 --- a/structure/02_config-and-codex-home.md +++ b/structure/02_config-and-codex-home.md @@ -110,6 +110,20 @@ path obtains System32 from `GetSystemDirectoryW`. Windows ARM64 Bun builds that Task Scheduler launch, never consults environment variables or `PATH`, and fails closed when the fixed executable is absent. +Direct PowerShell children rely on the process launcher's `windowsHide`/hidden-host mechanism and +must not also receive the PowerShell CLI pair `-WindowStyle Hidden`. On affected Windows 11 systems, +Bun 1.3.14 exits that direct invocation before the command runs, which turns a valid SID or process +lookup into `EACLIDENTITY` or a failed sync. This does not apply to `Start-Process -WindowStyle +Hidden` inside an already-running PowerShell script, nor to .NET/VBS process-window settings. + +[Decision Log] +- 목적과 의도: keep trusted Windows identity and process probes console-less without triggering Bun's direct PowerShell `-WindowStyle Hidden` failure. +- 기존 구현 및 제약 조건: the calls already used `windowsHide: true` or a hidden VBS host, but redundantly passed PowerShell's window-style CLI option; the same option remains valid inside `Start-Process` and must not be removed there. +- 검토한 주요 대안: decode the generic failure specially, retry after failure, remove all hidden-window controls, or remove only the redundant direct CLI pair. +- 선택한 방식: retain trusted executable resolution, non-interactive flags, timeouts, and process-level hiding; remove `-WindowStyle Hidden` only from direct PowerShell argv. +- 다른 대안 대신 이 방식을 선택한 이유: the command executes on affected Bun/Windows combinations, no console window is introduced, and working elevated/detached child-process behavior stays unchanged. +- 장점, 단점 및 영향: SID, process-owner, tray, update, and sync probes share the compatible launch contract; a future call must use launcher-level hiding rather than reintroducing the PowerShell CLI pair. + [Decision Log] - 목적과 의도: Preserve required Windows ACL hardening on the bundled Windows ARM64 runtime without weakening executable trust. - 기존 구현 및 제약 조건: The effective-SID query depended on the shared `GetSystemDirectoryW` FFI resolver; Bun 1.3.14 Windows ARM64 has no working `bun:ffi`, so config mutation reached `EACLIDENTITY` before PowerShell could start. diff --git a/tests/codex-app-server-processes.test.ts b/tests/codex-app-server-processes.test.ts index da7a7302b..3e1bdb08a 100644 --- a/tests/codex-app-server-processes.test.ts +++ b/tests/codex-app-server-processes.test.ts @@ -467,7 +467,7 @@ describe("Windows Win32_Process owner enumeration (#476)", () => { const child = spawn( "powershell.exe", [ - "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden", + "-NoProfile", "-NoLogo", "-NonInteractive", "-Command", "Start-Sleep -Seconds 45 # codex app-server integration-probe", ], diff --git a/tests/windows-deploy-close-regressions.test.ts b/tests/windows-deploy-close-regressions.test.ts index 432c65dda..f3e3097ec 100644 --- a/tests/windows-deploy-close-regressions.test.ts +++ b/tests/windows-deploy-close-regressions.test.ts @@ -39,6 +39,8 @@ describe("update-job restart avoids the shell-less .cmd EINVAL (Windows, bun/sou expect(src).toContain("Start-Process"); expect(src).toContain("buildWindowsElevatedArgumentList"); expect(src).toContain("resolveTrustedWindowsPowerShellExe"); + expect(src).toContain("windowsHide: true"); + expect(src).not.toContain('["-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", ps]'); expect(src).toContain("spawnWorkerFn: spawnGuiUpdateWorker"); // Foreign listeners must stay fail-closed; npm rename is covered by ocx identity. expect(src).not.toContain("killAnyListenPidOnPort"); diff --git a/tests/windows-popup-fix.test.ts b/tests/windows-popup-fix.test.ts index 79cb3a01b..54593cae7 100644 --- a/tests/windows-popup-fix.test.ts +++ b/tests/windows-popup-fix.test.ts @@ -25,7 +25,7 @@ afterEach(() => { }); describe("Windows identity lookup popup fix (#1278)", () => { - test("builds a hidden non-interactive command from the trusted PowerShell path", () => { + test("builds a non-interactive command without the Bun-incompatible PowerShell window flag", () => { setTrustedWindowsElevationExecutablesForTests({ powershell: TRUSTED_POWERSHELL }); const command = windowsIdentityPowerShellCommandForTests( "[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value", @@ -33,9 +33,8 @@ describe("Windows identity lookup popup fix (#1278)", () => { expect(command[0]).toBe(TRUSTED_POWERSHELL); expect(command).toContain("-NoProfile"); expect(command).toContain("-NonInteractive"); - const windowStyle = command.indexOf("-WindowStyle"); - expect(windowStyle).toBeGreaterThan(0); - expect(command[windowStyle + 1]).toBe("Hidden"); + expect(command).not.toContain("-WindowStyle"); + expect(command).not.toContain("Hidden"); expect(command[command.length - 2]).toBe("-Command"); expect(command[command.length - 1]) .toBe("[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value"); diff --git a/tests/windows-tray.test.ts b/tests/windows-tray.test.ts index 70cc0ed1b..ca6f782cf 100644 --- a/tests/windows-tray.test.ts +++ b/tests/windows-tray.test.ts @@ -89,7 +89,7 @@ describe("Windows tray packaging and command safety", () => { } }); - test("uses fixed argv for the hidden PowerShell host", () => { + test("uses fixed argv and leaves window suppression to the process launcher", () => { const args = windowsTrayProcessArgs(entry); expect(args).toContain("-NoProfile"); expect(args).toContain("-NonInteractive"); @@ -98,6 +98,8 @@ describe("Windows tray packaging and command safety", () => { expect(args).toContain(entry.bun); expect(args).toContain(entry.cli); expect(args).not.toContain("-Command"); + expect(args).not.toContain("-WindowStyle"); + expect(args).not.toContain("Hidden"); expect(windowsTrayProcessArgs(entry, "Run", 4242)).toContain("4242"); }); @@ -121,6 +123,7 @@ describe("Windows tray packaging and command safety", () => { expect(powershellCommand).toContain(`-OpenCodexHome "${entry.opencodexHome}"`); expect(powershellCommand).not.toContain("cmd /c"); expect(powershellCommand).not.toContain("-Command"); + expect(powershellCommand).not.toContain("-WindowStyle"); const runCommand = buildWindowsTrayRunCommand({ ...entry, launcherPath: `${entry.opencodexHome}\\opencodex-tray.vbs`, diff --git a/tests/windows-user-principal.test.ts b/tests/windows-user-principal.test.ts index d1d04ca66..80d6de0ee 100644 --- a/tests/windows-user-principal.test.ts +++ b/tests/windows-user-principal.test.ts @@ -29,7 +29,7 @@ afterEach(() => { }); describe("Windows effective ACL principal", () => { - test("builds a hidden non-interactive command from the trusted PowerShell path", () => { + test("builds a non-interactive command without the Bun-incompatible PowerShell window flag", () => { const trusted = "C:\\trusted-system32\\WindowsPowerShell\\v1.0\\powershell.exe"; setTrustedWindowsElevationExecutablesForTests({ powershell: trusted }); expect(windowsPrincipalPowerShellCommandForTests()).toEqual([ @@ -37,8 +37,6 @@ describe("Windows effective ACL principal", () => { "-NoLogo", "-NoProfile", "-NonInteractive", - "-WindowStyle", - "Hidden", "-Command", "[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value", ]);