From cdc16e5a789ba772cd072b0aaa6f66ae6a2c37d3 Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Wed, 29 Jul 2026 09:55:41 +0000 Subject: [PATCH 1/4] fix(windows): classify localized scheduler denial --- .../src/content/docs/ja/reference/cli.md | 6 ++ .../src/content/docs/ko/reference/cli.md | 6 ++ docs-site/src/content/docs/reference/cli.md | 6 ++ .../src/content/docs/ru/reference/cli.md | 6 ++ .../src/content/docs/zh-cn/reference/cli.md | 5 ++ src/lib/windows-elevation.ts | 80 ++++++++++++++++++- structure/05_gui-and-management-api.md | 16 ++++ tests/windows-elevation.test.ts | 61 ++++++++++++++ 8 files changed, 184 insertions(+), 2 deletions(-) diff --git a/docs-site/src/content/docs/ja/reference/cli.md b/docs-site/src/content/docs/ja/reference/cli.md index 442e21d534f..7e619dbede3 100644 --- a/docs-site/src/content/docs/ja/reference/cli.md +++ b/docs-site/src/content/docs/ja/reference/cli.md @@ -360,6 +360,12 @@ ocx service status ocx service uninstall ``` +Windows でタスク スケジューラのエントリを作成するには昇格が必要です。ダッシュボードの +Startup Safety アクションは UAC を自動的に要求できます。昇格していないシェルから直接 +`ocx service install` を実行した場合は、システム言語に依存しない対処案内が表示されます。 +ダッシュボードで UAC を承認するか、管理者 PowerShell で再実行してください。自動昇格による +復旧は、OpenCodex が所有する固定の `opencodex-proxy` タスク作成コマンドだけに限定されます。 + ### `ocx codex-shim ` PATH 上のスクリプトベース `codex` ランチャーを軽量な自動起動スクリプトで包みます。実際の `codex.exe` diff --git a/docs-site/src/content/docs/ko/reference/cli.md b/docs-site/src/content/docs/ko/reference/cli.md index 7ab091ef08d..ed5f6a6121d 100644 --- a/docs-site/src/content/docs/ko/reference/cli.md +++ b/docs-site/src/content/docs/ko/reference/cli.md @@ -379,6 +379,12 @@ ocx service status ocx service uninstall ``` +Windows에서 작업 스케줄러 항목을 만들려면 권한 상승이 필요합니다. 대시보드의 Startup Safety +작업은 UAC를 자동으로 요청할 수 있습니다. 권한이 상승되지 않은 셸에서 `ocx service install`을 +직접 실행하면 OpenCodex가 시스템 언어와 무관한 조치 안내를 출력하므로, 대시보드에서 UAC를 +승인하거나 관리자 PowerShell에서 명령을 다시 실행하세요. 자동 권한 상승 복구는 OpenCodex가 +소유한 고정 `opencodex-proxy` 작업 생성 명령에만 적용됩니다. + ### `ocx codex-shim ` PATH에 있는 스크립트 기반 `codex` 런처를 가벼운 자동 시작 스크립트로 감쌉니다. 실제 `codex.exe` diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index 59440bcb2a8..4ff9bd9b659 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -439,6 +439,12 @@ ocx service status ocx service uninstall ``` +On Windows, creating the Task Scheduler entry requires elevation. The dashboard's Startup Safety +action can request UAC automatically. A direct `ocx service install` from a non-elevated shell +instead prints language-independent guidance; approve UAC from the dashboard or rerun it in an +elevated PowerShell window. OpenCodex only uses the automatic elevation recovery for its owned, +fixed `opencodex-proxy` task-create command. + ### `ocx codex-shim ` Wrap a script-based `codex` launcher on PATH with a lightweight autostart script. Real `codex.exe` diff --git a/docs-site/src/content/docs/ru/reference/cli.md b/docs-site/src/content/docs/ru/reference/cli.md index 15a460279df..ac77f4f2edb 100644 --- a/docs-site/src/content/docs/ru/reference/cli.md +++ b/docs-site/src/content/docs/ru/reference/cli.md @@ -401,6 +401,12 @@ ocx service status ocx service uninstall ``` +В Windows для создания задания Планировщика требуется повышение прав. Действие Startup Safety в +панели может автоматически запросить UAC. При прямом запуске `ocx service install` из обычной +оболочки OpenCodex выводит инструкцию, не зависящую от языка системы: подтвердите UAC в панели или +повторите команду в PowerShell от имени администратора. Автоматическое повышение применяется только +к фиксированной команде создания принадлежащего OpenCodex задания `opencodex-proxy`. + ### `ocx codex-shim ` Оборачивает скриптовый лаунчер `codex` в PATH лёгким скриптом автозапуска. Настоящие цели diff --git a/docs-site/src/content/docs/zh-cn/reference/cli.md b/docs-site/src/content/docs/zh-cn/reference/cli.md index 9b744afcbac..032fbfb17f4 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli.md @@ -363,6 +363,11 @@ ocx service status ocx service uninstall ``` +在 Windows 上,创建任务计划程序条目需要提升权限。仪表板的 Startup Safety 操作可以自动请求 +UAC;如果在未提升权限的终端中直接运行 `ocx service install`,OpenCodex 会显示不依赖系统语言的 +处理说明。此时可在仪表板中批准 UAC,或在管理员 PowerShell 中重新运行该命令。自动提升恢复仅用于 +OpenCodex 自己固定的 `opencodex-proxy` 任务创建命令。 + ### `ocx codex-shim ` 把 PATH 上基于脚本的 `codex` launcher 包装成轻量自动启动脚本。真实 `codex.exe` 目标保持不变, diff --git a/src/lib/windows-elevation.ts b/src/lib/windows-elevation.ts index 2af1fd44501..a5224ab4af1 100644 --- a/src/lib/windows-elevation.ts +++ b/src/lib/windows-elevation.ts @@ -18,9 +18,13 @@ export function setWindowsElevationSpawnForTests(next: ElevationSpawn | null): v type GetSystemDirectoryW = (buffer: Pointer, size: number) => number; type TrustedSystemDirectoryResolver = () => string; +type IsUserAnAdmin = () => boolean; +type WindowsElevationProbe = () => boolean | null; let getSystemDirectoryWFn: GetSystemDirectoryW | null | undefined; let trustedSystemDirectoryResolverForTests: TrustedSystemDirectoryResolver | null = null; +let isUserAnAdminFn: IsUserAnAdmin | null | undefined; +let windowsElevationProbeForTests: WindowsElevationProbe | null = null; /** Test-only seam to replace GetSystemDirectoryW-backed resolution. */ export function setTrustedWindowsSystemDirectoryResolverForTests( @@ -29,6 +33,47 @@ export function setTrustedWindowsSystemDirectoryResolverForTests( trustedSystemDirectoryResolverForTests = next; } +/** Test-only seam for the locale-independent current-token elevation probe. */ +export function setWindowsElevationProbeForTests(next: WindowsElevationProbe | null): void { + windowsElevationProbeForTests = next; +} + +function loadIsUserAnAdmin(): IsUserAnAdmin | null { + if (isUserAnAdminFn !== undefined) return isUserAnAdminFn; + if (process.platform !== "win32") { + isUserAnAdminFn = null; + return null; + } + try { + const lib = dlopen("shell32.dll", { + IsUserAnAdmin: { + args: [], + returns: "bool", + }, + }); + isUserAnAdminFn = () => lib.symbols.IsUserAnAdmin() as boolean; + } catch { + isUserAnAdminFn = null; + } + return isUserAnAdminFn; +} + +/** + * Probe the effective Windows token without parsing localized command output. + * `null` fails closed: callers must retain the original scheduler error rather + * than infer that an unknown token state needs elevation. + */ +export function isCurrentWindowsProcessElevated(): boolean | null { + if (windowsElevationProbeForTests) return windowsElevationProbeForTests(); + const isUserAnAdmin = loadIsUserAnAdmin(); + if (!isUserAnAdmin) return null; + try { + return isUserAnAdmin(); + } catch { + return null; + } +} + function loadGetSystemDirectoryW(): GetSystemDirectoryW | null { if (getSystemDirectoryWFn !== undefined) return getSystemDirectoryWFn; if (process.platform !== "win32") { @@ -252,6 +297,35 @@ export function schtasksOperationFromArgs(args: string[]): WindowsSchtasksOperat return "other"; } +function schedulerExitStatus(error: unknown): number | null { + if (!error || typeof error !== "object") return null; + const status = (error as { status?: unknown }).status; + return typeof status === "number" && Number.isInteger(status) ? status : null; +} + +/** + * The owned scheduler install has a fixed shape. Restrict the locale-independent + * fallback to that shape so unrelated schtasks failures cannot request elevation. + */ +function isOwnedSchedulerCreate(args: string[]): boolean { + const normalized = args.map(arg => arg.toLowerCase()); + const taskIndex = normalized.indexOf("/tn"); + const xmlIndex = normalized.indexOf("/xml"); + return normalized[0] === "/create" + && normalized[taskIndex + 1] === "opencodex-proxy" + && taskIndex > 0 + && xmlIndex > 0 + && Boolean(args[xmlIndex + 1]) + && normalized.includes("/f"); +} + +function isWindowsSchtasksAccessDeniedError(error: unknown, args: string[]): boolean { + if (isWindowsAccessDeniedError(error)) return true; + return isOwnedSchedulerCreate(args) + && schedulerExitStatus(error) === 1 + && isCurrentWindowsProcessElevated() === false; +} + /** Structured Task Scheduler failure that survives formatting and process boundaries. */ export class WindowsSchtasksError extends Error { readonly code = "WINDOWS_SCHTASKS_ERROR" as const; @@ -287,7 +361,7 @@ export class WindowsElevationError extends Error { /** Replace raw schtasks access-denied output with dashboard-friendly guidance. */ export function formatWindowsSchtasksError(error: unknown, args: string[]): string { const operation = schtasksOperationFromArgs(args); - const accessDenied = isWindowsAccessDeniedError(error); + const accessDenied = isWindowsSchtasksAccessDeniedError(error, args); if (!accessDenied) { return error instanceof Error ? error.message : String(error); } @@ -306,7 +380,9 @@ export function formatWindowsSchtasksError(error: unknown, args: string[]): stri export function toWindowsSchtasksError(error: unknown, args: string[]): WindowsSchtasksError { if (error instanceof WindowsSchtasksError) return error; const operation = schtasksOperationFromArgs(args); - const reason: WindowsSchtasksFailureReason = isWindowsAccessDeniedError(error) ? "access-denied" : "other"; + const reason: WindowsSchtasksFailureReason = isWindowsSchtasksAccessDeniedError(error, args) + ? "access-denied" + : "other"; return new WindowsSchtasksError(operation, reason, formatWindowsSchtasksError(error, args)); } diff --git a/structure/05_gui-and-management-api.md b/structure/05_gui-and-management-api.md index c464c4361ec..dbd25c4c593 100644 --- a/structure/05_gui-and-management-api.md +++ b/structure/05_gui-and-management-api.md @@ -59,6 +59,22 @@ home-scoped singleton, and HKCU Run registration; fixed proxy actions delegate t service conflict handling, native restore, and PID identity remain centralized. Tray presence never makes `startup.status` protected. +Windows Task Scheduler create failures must not depend solely on localized `schtasks.exe` text. +When the owned fixed-shape `/create /tn opencodex-proxy /xml ... /f` command exits with status 1, +the effective-token elevation probe may classify it as access denied only when the token is known +to be non-elevated. An unavailable probe remains `other` and cannot trigger UAC. Query, run, delete, +native-service, file-write, and foreign task failures never use this fallback. + +```text +[Decision Log] +- 목적과 의도: Make Windows scheduler installation recovery work on non-English systems without broadening the commands that may request UAC. +- 기존 구현 및 제약 조건: Access-denied classification parsed English and German stderr. Chinese OEM output decoded as UTF-8 became mojibake, so the fixed scheduler-create failure lost its machine marker and the dashboard could not select its existing elevation transaction. +- 검토한 주요 대안: Add translations and code-page decoders; elevate every scheduler failure; always launch installation elevated; or combine a native effective-token probe with the already fixed command shape and exit status. +- 선택한 방식: Preserve text detection, then use the native token probe only for status-1 creation of the owned `opencodex-proxy` XML task. Unknown probe results fail closed. +- 다른 대안 대신 이 방식을 선택한 이유: Windows localization and OEM code pages are open-ended, while the token state and owned command shape are stable security signals already bounded by the elevated transaction protocol. +- 장점, 단점 및 영향: Non-English users receive stable guidance and dashboard UAC recovery. A non-permission status-1 failure from the exact owned command may be retried once elevated, but foreign operations cannot cross the elevation boundary and the elevated transaction still fails closed. +``` + Dashboard updates persist their detached worker PID before returning success. This lets a later run distinguish a live installer from a worker that crashed. Records created by older versions do not have a PID, so they remain exclusive for a conservative ten-minute window before automatic diff --git a/tests/windows-elevation.test.ts b/tests/windows-elevation.test.ts index 4a9e62466d0..73e6476f2c1 100644 --- a/tests/windows-elevation.test.ts +++ b/tests/windows-elevation.test.ts @@ -16,6 +16,7 @@ import { schtasksOperationFromArgs, setTrustedWindowsElevationExecutablesForTests, setTrustedWindowsSystemDirectoryResolverForTests, + setWindowsElevationProbeForTests, toWindowsSchtasksError, windowsCmdQuote, } from "../src/lib/windows-elevation"; @@ -78,6 +79,66 @@ describe("windows elevation helpers", () => { expect(schtasksOperationFromArgs(["/delete", "/tn", "x"])).toBe("delete"); }); + test("classifies an owned scheduler create failure without localized text", () => { + setWindowsElevationProbeForTests(() => false); + try { + const error = Object.assign(new Error("Command failed"), { + // A real non-English Windows install can arrive as mojibake here. + stderr: "����: �ܾ����ʡ�\r\n", + stdout: "", + status: 1, + }); + const structured = toWindowsSchtasksError(error, [ + "/create", + "/tn", + "opencodex-proxy", + "/xml", + "C:\\Users\\tester\\.opencodex\\opencodex-service-task.xml", + "/f", + ]); + expect(structured.reason).toBe("access-denied"); + expect(structured.message).toContain("Windows access denied while running Task Scheduler."); + expect(structured.machineMarker).toBe(WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER); + } finally { + setWindowsElevationProbeForTests(null); + } + }); + + test("locale-independent elevation fallback is scoped and fails closed", () => { + const error = Object.assign(new Error("Command failed"), { + stderr: "unrecognized localized output", + status: 1, + }); + setWindowsElevationProbeForTests(() => false); + try { + expect(toWindowsSchtasksError(error, ["/query", "/tn", "opencodex-proxy"]).reason).toBe("other"); + expect(toWindowsSchtasksError(error, [ + "/create", + "/tn", + "someone-elses-task", + "/xml", + "task.xml", + "/f", + ]).reason).toBe("other"); + } finally { + setWindowsElevationProbeForTests(null); + } + + setWindowsElevationProbeForTests(() => null); + try { + expect(toWindowsSchtasksError(error, [ + "/create", + "/tn", + "opencodex-proxy", + "/xml", + "task.xml", + "/f", + ]).reason).toBe("other"); + } finally { + setWindowsElevationProbeForTests(null); + } + }); + test("builds one Win32-quoted argument list for spaced paths", () => { expect(buildWindowsElevatedArgumentList([ "/create", From b9ae6592dac0705613f7df56f33e123eacff9d91 Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Wed, 29 Jul 2026 10:05:37 +0000 Subject: [PATCH 2/4] fix(windows): tighten scheduler elevation boundary --- .../src/content/docs/ja/reference/cli.md | 11 ++--- .../src/content/docs/ko/reference/cli.md | 10 ++--- docs-site/src/content/docs/reference/cli.md | 11 ++--- .../src/content/docs/ru/reference/cli.md | 11 ++--- .../src/content/docs/zh-cn/reference/cli.md | 9 ++-- src/lib/windows-elevation.ts | 24 ++++++----- tests/windows-elevation.test.ts | 42 +++++++++++++++++-- 7 files changed, 80 insertions(+), 38 deletions(-) diff --git a/docs-site/src/content/docs/ja/reference/cli.md b/docs-site/src/content/docs/ja/reference/cli.md index 7e619dbede3..7628aa81405 100644 --- a/docs-site/src/content/docs/ja/reference/cli.md +++ b/docs-site/src/content/docs/ja/reference/cli.md @@ -360,11 +360,12 @@ ocx service status ocx service uninstall ``` -Windows でタスク スケジューラのエントリを作成するには昇格が必要です。ダッシュボードの -Startup Safety アクションは UAC を自動的に要求できます。昇格していないシェルから直接 -`ocx service install` を実行した場合は、システム言語に依存しない対処案内が表示されます。 -ダッシュボードで UAC を承認するか、管理者 PowerShell で再実行してください。自動昇格による -復旧は、OpenCodex が所有する固定の `opencodex-proxy` タスク作成コマンドだけに限定されます。 +Windows でタスク スケジューラのエントリを作成するには昇格が必要です。OpenCodex が所有する +固定の `opencodex-proxy` タスク作成コマンドで、現在のトークンが未昇格と確認できた場合に限り、 +システム言語に依存しない案内を表示し、ダッシュボードの Startup Safety アクションが UAC を +自動的に要求できます。その UAC を承認するか、管理者 PowerShell で `ocx service install` を +再実行してください。トークンを確認できない場合や固定コマンド以外の失敗では、元のスケジューラ +エラーを保持し、昇格を要求しません。 ### `ocx codex-shim ` diff --git a/docs-site/src/content/docs/ko/reference/cli.md b/docs-site/src/content/docs/ko/reference/cli.md index ed5f6a6121d..75f85b8dd2f 100644 --- a/docs-site/src/content/docs/ko/reference/cli.md +++ b/docs-site/src/content/docs/ko/reference/cli.md @@ -379,11 +379,11 @@ ocx service status ocx service uninstall ``` -Windows에서 작업 스케줄러 항목을 만들려면 권한 상승이 필요합니다. 대시보드의 Startup Safety -작업은 UAC를 자동으로 요청할 수 있습니다. 권한이 상승되지 않은 셸에서 `ocx service install`을 -직접 실행하면 OpenCodex가 시스템 언어와 무관한 조치 안내를 출력하므로, 대시보드에서 UAC를 -승인하거나 관리자 PowerShell에서 명령을 다시 실행하세요. 자동 권한 상승 복구는 OpenCodex가 -소유한 고정 `opencodex-proxy` 작업 생성 명령에만 적용됩니다. +Windows에서 작업 스케줄러 항목을 만들려면 권한 상승이 필요합니다. OpenCodex가 소유한 고정 +`opencodex-proxy` 작업 생성 명령이고 현재 토큰이 권한 상승되지 않았음이 확인된 경우에만 시스템 +언어와 무관한 안내를 출력하며, 대시보드의 Startup Safety 작업이 UAC를 자동 요청할 수 있습니다. +해당 UAC를 승인하거나 관리자 PowerShell에서 `ocx service install`을 다시 실행하세요. 토큰을 +확인할 수 없거나 고정 명령 밖의 실패라면 원래 스케줄러 오류를 유지하고 권한 상승을 요청하지 않습니다. ### `ocx codex-shim ` diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index 4ff9bd9b659..6901329ed61 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -439,11 +439,12 @@ ocx service status ocx service uninstall ``` -On Windows, creating the Task Scheduler entry requires elevation. The dashboard's Startup Safety -action can request UAC automatically. A direct `ocx service install` from a non-elevated shell -instead prints language-independent guidance; approve UAC from the dashboard or rerun it in an -elevated PowerShell window. OpenCodex only uses the automatic elevation recovery for its owned, -fixed `opencodex-proxy` task-create command. +On Windows, creating the Task Scheduler entry requires elevation. For the owned, fixed +`opencodex-proxy` task-create command, a confirmed non-elevated token receives language-independent +guidance and the dashboard's Startup Safety action can request UAC automatically. Approve that UAC +prompt or rerun `ocx service install` in an elevated PowerShell window. If the token probe is +unavailable or the failure is outside that owned command, OpenCodex retains the original scheduler +error and does not request elevation. ### `ocx codex-shim ` diff --git a/docs-site/src/content/docs/ru/reference/cli.md b/docs-site/src/content/docs/ru/reference/cli.md index ac77f4f2edb..3a5eec91b79 100644 --- a/docs-site/src/content/docs/ru/reference/cli.md +++ b/docs-site/src/content/docs/ru/reference/cli.md @@ -401,11 +401,12 @@ ocx service status ocx service uninstall ``` -В Windows для создания задания Планировщика требуется повышение прав. Действие Startup Safety в -панели может автоматически запросить UAC. При прямом запуске `ocx service install` из обычной -оболочки OpenCodex выводит инструкцию, не зависящую от языка системы: подтвердите UAC в панели или -повторите команду в PowerShell от имени администратора. Автоматическое повышение применяется только -к фиксированной команде создания принадлежащего OpenCodex задания `opencodex-proxy`. +В Windows для создания задания Планировщика требуется повышение прав. Независимая от языка +инструкция и автоматический запрос UAC из Startup Safety доступны только для фиксированной команды +создания принадлежащего OpenCodex задания `opencodex-proxy`, когда текущий токен подтверждён как +неповышенный. Подтвердите UAC или повторите `ocx service install` в PowerShell от имени +администратора. Если состояние токена неизвестно либо сбой произошёл вне этой команды, OpenCodex +сохраняет исходную ошибку Планировщика и не запрашивает повышение. ### `ocx codex-shim ` diff --git a/docs-site/src/content/docs/zh-cn/reference/cli.md b/docs-site/src/content/docs/zh-cn/reference/cli.md index 032fbfb17f4..5bf4aa1ed64 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli.md @@ -363,10 +363,11 @@ ocx service status ocx service uninstall ``` -在 Windows 上,创建任务计划程序条目需要提升权限。仪表板的 Startup Safety 操作可以自动请求 -UAC;如果在未提升权限的终端中直接运行 `ocx service install`,OpenCodex 会显示不依赖系统语言的 -处理说明。此时可在仪表板中批准 UAC,或在管理员 PowerShell 中重新运行该命令。自动提升恢复仅用于 -OpenCodex 自己固定的 `opencodex-proxy` 任务创建命令。 +在 Windows 上,创建任务计划程序条目需要提升权限。仅当固定且由 OpenCodex 所有的 +`opencodex-proxy` 任务创建命令失败,并确认当前 token 未提升时,OpenCodex 才会显示不依赖系统 +语言的说明,仪表板的 Startup Safety 操作也可自动请求 UAC。请批准该 UAC,或在管理员 PowerShell +中重新运行 `ocx service install`。如果无法探测 token,或失败不属于该固定命令,OpenCodex 会保留 +原始计划程序错误且不会请求提升。 ### `ocx codex-shim ` diff --git a/src/lib/windows-elevation.ts b/src/lib/windows-elevation.ts index a5224ab4af1..591dec81eee 100644 --- a/src/lib/windows-elevation.ts +++ b/src/lib/windows-elevation.ts @@ -18,12 +18,12 @@ export function setWindowsElevationSpawnForTests(next: ElevationSpawn | null): v type GetSystemDirectoryW = (buffer: Pointer, size: number) => number; type TrustedSystemDirectoryResolver = () => string; -type IsUserAnAdmin = () => boolean; +type IsUserAnAdmin = () => number; type WindowsElevationProbe = () => boolean | null; let getSystemDirectoryWFn: GetSystemDirectoryW | null | undefined; let trustedSystemDirectoryResolverForTests: TrustedSystemDirectoryResolver | null = null; -let isUserAnAdminFn: IsUserAnAdmin | null | undefined; +let isUserAnAdminFn: (() => boolean) | null | undefined; let windowsElevationProbeForTests: WindowsElevationProbe | null = null; /** Test-only seam to replace GetSystemDirectoryW-backed resolution. */ @@ -38,7 +38,7 @@ export function setWindowsElevationProbeForTests(next: WindowsElevationProbe | n windowsElevationProbeForTests = next; } -function loadIsUserAnAdmin(): IsUserAnAdmin | null { +function loadIsUserAnAdmin(): (() => boolean) | null { if (isUserAnAdminFn !== undefined) return isUserAnAdminFn; if (process.platform !== "win32") { isUserAnAdminFn = null; @@ -48,10 +48,12 @@ function loadIsUserAnAdmin(): IsUserAnAdmin | null { const lib = dlopen("shell32.dll", { IsUserAnAdmin: { args: [], - returns: "bool", + // Win32 BOOL is a signed 32-bit integer, not C/C++ bool. + returns: "i32", }, }); - isUserAnAdminFn = () => lib.symbols.IsUserAnAdmin() as boolean; + const isUserAnAdmin = lib.symbols.IsUserAnAdmin as IsUserAnAdmin; + isUserAnAdminFn = () => isUserAnAdmin() !== 0; } catch { isUserAnAdminFn = null; } @@ -320,10 +322,9 @@ function isOwnedSchedulerCreate(args: string[]): boolean { } function isWindowsSchtasksAccessDeniedError(error: unknown, args: string[]): boolean { - if (isWindowsAccessDeniedError(error)) return true; - return isOwnedSchedulerCreate(args) - && schedulerExitStatus(error) === 1 - && isCurrentWindowsProcessElevated() === false; + if (!isOwnedSchedulerCreate(args)) return false; + return isWindowsAccessDeniedError(error) + || (schedulerExitStatus(error) === 1 && isCurrentWindowsProcessElevated() === false); } /** Structured Task Scheduler failure that survives formatting and process boundaries. */ @@ -361,7 +362,8 @@ export class WindowsElevationError extends Error { /** Replace raw schtasks access-denied output with dashboard-friendly guidance. */ export function formatWindowsSchtasksError(error: unknown, args: string[]): string { const operation = schtasksOperationFromArgs(args); - const accessDenied = isWindowsSchtasksAccessDeniedError(error, args); + const ownedCreateAccessDenied = isWindowsSchtasksAccessDeniedError(error, args); + const accessDenied = ownedCreateAccessDenied || isWindowsAccessDeniedError(error); if (!accessDenied) { return error instanceof Error ? error.message : String(error); } @@ -371,7 +373,7 @@ export function formatWindowsSchtasksError(error: unknown, args: string[]): stri `Command: schtasks ${argsText}`, "Approve the Windows UAC prompt to install the background service, or run `ocx service install` from an elevated PowerShell window.", ].join(" "); - if (operation === "create") { + if (operation === "create" && ownedCreateAccessDenied) { return `${guidance}\n${WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER}`; } return guidance; diff --git a/tests/windows-elevation.test.ts b/tests/windows-elevation.test.ts index 73e6476f2c1..ed35e067696 100644 --- a/tests/windows-elevation.test.ts +++ b/tests/windows-elevation.test.ts @@ -44,9 +44,16 @@ describe("windows elevation helpers", () => { stdout: "", status: 1, }); - const message = formatWindowsSchtasksError(error, ["/create", "/tn", "opencodex-proxy"]); + const message = formatWindowsSchtasksError(error, [ + "/create", + "/tn", + "opencodex-proxy", + "/xml", + "task.xml", + "/f", + ]); expect(message).toContain("Windows access denied while running Task Scheduler."); - expect(message).toContain("schtasks /create /tn opencodex-proxy"); + expect(message).toContain("schtasks /create /tn opencodex-proxy /xml task.xml /f"); expect(message).toContain("UAC prompt"); expect(message).toContain(WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER); expect(isWindowsSchtasksCreateAccessDenied(message)).toBe(true); @@ -71,7 +78,14 @@ describe("windows elevation helpers", () => { test("toWindowsSchtasksError preserves operation and reason", () => { const error = Object.assign(new Error("Command failed"), { stderr: "Access is denied." }); - const structured = toWindowsSchtasksError(error, ["/create", "/xml", "task.xml", "/f"]); + const structured = toWindowsSchtasksError(error, [ + "/create", + "/tn", + "opencodex-proxy", + "/xml", + "task.xml", + "/f", + ]); expect(structured).toBeInstanceOf(WindowsSchtasksError); expect(structured.operation).toBe("create"); expect(structured.reason).toBe("access-denied"); @@ -120,6 +134,14 @@ describe("windows elevation helpers", () => { "task.xml", "/f", ]).reason).toBe("other"); + + const foreignDenied = toWindowsSchtasksError( + Object.assign(new Error("Command failed"), { stderr: "Access is denied.", status: 1 }), + ["/create", "/tn", "someone-elses-task", "/xml", "task.xml", "/f"], + ); + expect(foreignDenied.reason).toBe("other"); + expect(foreignDenied.message).toContain("Windows access denied while running Task Scheduler."); + expect(foreignDenied.machineMarker).toBeNull(); } finally { setWindowsElevationProbeForTests(null); } @@ -137,6 +159,20 @@ describe("windows elevation helpers", () => { } finally { setWindowsElevationProbeForTests(null); } + + setWindowsElevationProbeForTests(() => true); + try { + expect(toWindowsSchtasksError(error, [ + "/create", + "/tn", + "opencodex-proxy", + "/xml", + "task.xml", + "/f", + ]).reason).toBe("other"); + } finally { + setWindowsElevationProbeForTests(null); + } }); test("builds one Win32-quoted argument list for spaced paths", () => { From 93385503e0436f28c4d7dd07d52d27e27b5965ae Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Wed, 29 Jul 2026 11:08:56 +0000 Subject: [PATCH 3/4] docs(windows): clarify scheduler fallback scope --- docs-site/src/content/docs/ja/reference/cli.md | 14 ++++++++------ docs-site/src/content/docs/ko/reference/cli.md | 12 +++++++----- docs-site/src/content/docs/reference/cli.md | 13 +++++++------ docs-site/src/content/docs/ru/reference/cli.md | 14 ++++++++------ docs-site/src/content/docs/zh-cn/reference/cli.md | 11 ++++++----- 5 files changed, 36 insertions(+), 28 deletions(-) diff --git a/docs-site/src/content/docs/ja/reference/cli.md b/docs-site/src/content/docs/ja/reference/cli.md index 7628aa81405..6eaf06e4f03 100644 --- a/docs-site/src/content/docs/ja/reference/cli.md +++ b/docs-site/src/content/docs/ja/reference/cli.md @@ -360,12 +360,14 @@ ocx service status ocx service uninstall ``` -Windows でタスク スケジューラのエントリを作成するには昇格が必要です。OpenCodex が所有する -固定の `opencodex-proxy` タスク作成コマンドで、現在のトークンが未昇格と確認できた場合に限り、 -システム言語に依存しない案内を表示し、ダッシュボードの Startup Safety アクションが UAC を -自動的に要求できます。その UAC を承認するか、管理者 PowerShell で `ocx service install` を -再実行してください。トークンを確認できない場合や固定コマンド以外の失敗では、元のスケジューラ -エラーを保持し、昇格を要求しません。 +Windows でタスク スケジューラのエントリを作成するには昇格が必要です。認識できるローカライズ +済みのアクセス拒否テキストは、既存の案内経路をそのまま使用します。そのテキストが読めない場合、 +OpenCodex が所有する固定の `opencodex-proxy` タスク作成コマンドが status 1 で失敗し、現在の +トークンが未昇格と確認できたときだけ、言語に依存しないフォールバックが働き、ダッシュボードの +Startup Safety アクションが UAC を自動的に要求できます。フォールバックでトークン状態を確認 +できない場合は、元のスケジューラエラーを保持します。他のタスクや操作は自動昇格 marker を生成 +できません。ダッシュボードの UAC を承認するか、管理者 PowerShell で `ocx service install` を +再実行してください。 ### `ocx codex-shim ` diff --git a/docs-site/src/content/docs/ko/reference/cli.md b/docs-site/src/content/docs/ko/reference/cli.md index 75f85b8dd2f..65fa7458ff2 100644 --- a/docs-site/src/content/docs/ko/reference/cli.md +++ b/docs-site/src/content/docs/ko/reference/cli.md @@ -379,11 +379,13 @@ ocx service status ocx service uninstall ``` -Windows에서 작업 스케줄러 항목을 만들려면 권한 상승이 필요합니다. OpenCodex가 소유한 고정 -`opencodex-proxy` 작업 생성 명령이고 현재 토큰이 권한 상승되지 않았음이 확인된 경우에만 시스템 -언어와 무관한 안내를 출력하며, 대시보드의 Startup Safety 작업이 UAC를 자동 요청할 수 있습니다. -해당 UAC를 승인하거나 관리자 PowerShell에서 `ocx service install`을 다시 실행하세요. 토큰을 -확인할 수 없거나 고정 명령 밖의 실패라면 원래 스케줄러 오류를 유지하고 권한 상승을 요청하지 않습니다. +Windows에서 작업 스케줄러 항목을 만들려면 권한 상승이 필요합니다. 인식 가능한 현지화 권한 거부 +문자열은 기존 안내 경로를 그대로 사용합니다. 문자열을 읽을 수 없을 때는 OpenCodex가 소유한 고정 +`opencodex-proxy` 작업 생성 명령이 상태 1로 실패하고 현재 토큰이 권한 상승되지 않았음이 확인돼야만 +언어 독립 fallback이 작동하며, 대시보드의 Startup Safety 작업이 UAC를 자동 요청할 수 있습니다. +fallback에서 토큰 상태를 확인할 수 없으면 원래 스케줄러 오류를 유지합니다. 다른 작업과 동작은 자동 +권한 상승 marker를 만들 수 없습니다. 대시보드의 UAC를 승인하거나 관리자 PowerShell에서 +`ocx service install`을 다시 실행하세요. ### `ocx codex-shim ` diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index 6901329ed61..7c0693a97d7 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -439,12 +439,13 @@ ocx service status ocx service uninstall ``` -On Windows, creating the Task Scheduler entry requires elevation. For the owned, fixed -`opencodex-proxy` task-create command, a confirmed non-elevated token receives language-independent -guidance and the dashboard's Startup Safety action can request UAC automatically. Approve that UAC -prompt or rerun `ocx service install` in an elevated PowerShell window. If the token probe is -unavailable or the failure is outside that owned command, OpenCodex retains the original scheduler -error and does not request elevation. +On Windows, creating the Task Scheduler entry requires elevation. Recognized localized +access-denied text keeps the existing guidance path. If that text is unreadable, the fallback for +the owned, fixed `opencodex-proxy` task-create command additionally requires status 1 and a confirmed +non-elevated token; the dashboard's Startup Safety action can then request UAC automatically. If +that fallback cannot determine the token state, it retains the original scheduler error. Foreign +tasks and operations can never emit the automatic-elevation marker. Approve the dashboard UAC +prompt or rerun `ocx service install` in an elevated PowerShell window. ### `ocx codex-shim ` diff --git a/docs-site/src/content/docs/ru/reference/cli.md b/docs-site/src/content/docs/ru/reference/cli.md index 3a5eec91b79..a9af6810c85 100644 --- a/docs-site/src/content/docs/ru/reference/cli.md +++ b/docs-site/src/content/docs/ru/reference/cli.md @@ -401,12 +401,14 @@ ocx service status ocx service uninstall ``` -В Windows для создания задания Планировщика требуется повышение прав. Независимая от языка -инструкция и автоматический запрос UAC из Startup Safety доступны только для фиксированной команды -создания принадлежащего OpenCodex задания `opencodex-proxy`, когда текущий токен подтверждён как -неповышенный. Подтвердите UAC или повторите `ocx service install` в PowerShell от имени -администратора. Если состояние токена неизвестно либо сбой произошёл вне этой команды, OpenCodex -сохраняет исходную ошибку Планировщика и не запрашивает повышение. +В Windows для создания задания Планировщика требуется повышение прав. Распознанный локализованный +текст об отказе в доступе продолжает использовать прежний путь подсказки. Если текст нечитаем, +независимый от языка fallback применяется только когда фиксированная команда создания принадлежащего +OpenCodex задания `opencodex-proxy` завершилась со статусом 1 и текущий токен подтверждён как +неповышенный; тогда Startup Safety может автоматически запросить UAC. Если fallback не может +определить состояние токена, сохраняется исходная ошибка Планировщика. Другие задания и операции не +могут создать marker автоматического повышения. Подтвердите UAC в панели или повторите +`ocx service install` в PowerShell от имени администратора. ### `ocx codex-shim ` diff --git a/docs-site/src/content/docs/zh-cn/reference/cli.md b/docs-site/src/content/docs/zh-cn/reference/cli.md index 5bf4aa1ed64..defb1754358 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli.md @@ -363,11 +363,12 @@ ocx service status ocx service uninstall ``` -在 Windows 上,创建任务计划程序条目需要提升权限。仅当固定且由 OpenCodex 所有的 -`opencodex-proxy` 任务创建命令失败,并确认当前 token 未提升时,OpenCodex 才会显示不依赖系统 -语言的说明,仪表板的 Startup Safety 操作也可自动请求 UAC。请批准该 UAC,或在管理员 PowerShell -中重新运行 `ocx service install`。如果无法探测 token,或失败不属于该固定命令,OpenCodex 会保留 -原始计划程序错误且不会请求提升。 +在 Windows 上,创建任务计划程序条目需要提升权限。能识别的本地化“拒绝访问”文本继续使用现有 +处理路径。如果该文本不可读,则仅当固定且由 OpenCodex 所有的 `opencodex-proxy` 任务创建命令以 +状态 1 失败,并确认当前 token 未提升时,回退路径才会显示不依赖系统语言的说明;仪表板的 Startup +Safety 操作也可自动请求 UAC。如果回退路径无法确定 token 状态,则保留原始计划程序错误。其他任务 +和操作绝不会产生自动提升 marker。请批准仪表板的 UAC,或在管理员 PowerShell 中重新运行 +`ocx service install`。 ### `ocx codex-shim ` From 34688f641a364514b99fb556c3cc1bb897ef13c1 Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Wed, 29 Jul 2026 11:39:11 +0000 Subject: [PATCH 4/4] docs(windows): spell out scheduler create predicate --- docs-site/src/content/docs/ja/reference/cli.md | 12 ++++++------ docs-site/src/content/docs/ko/reference/cli.md | 12 ++++++------ docs-site/src/content/docs/reference/cli.md | 12 ++++++------ docs-site/src/content/docs/ru/reference/cli.md | 12 ++++++------ docs-site/src/content/docs/zh-cn/reference/cli.md | 10 +++++----- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs-site/src/content/docs/ja/reference/cli.md b/docs-site/src/content/docs/ja/reference/cli.md index 6eaf06e4f03..d54e2f63c42 100644 --- a/docs-site/src/content/docs/ja/reference/cli.md +++ b/docs-site/src/content/docs/ja/reference/cli.md @@ -362,12 +362,12 @@ ocx service uninstall Windows でタスク スケジューラのエントリを作成するには昇格が必要です。認識できるローカライズ 済みのアクセス拒否テキストは、既存の案内経路をそのまま使用します。そのテキストが読めない場合、 -OpenCodex が所有する固定の `opencodex-proxy` タスク作成コマンドが status 1 で失敗し、現在の -トークンが未昇格と確認できたときだけ、言語に依存しないフォールバックが働き、ダッシュボードの -Startup Safety アクションが UAC を自動的に要求できます。フォールバックでトークン状態を確認 -できない場合は、元のスケジューラエラーを保持します。他のタスクや操作は自動昇格 marker を生成 -できません。ダッシュボードの UAC を承認するか、管理者 PowerShell で `ocx service install` を -再実行してください。 +コマンド形状が `/create /tn opencodex-proxy /xml <空でないパス> /f` と正確に一致し、終了 status +が 1 で、現在のトークンが未昇格と確認できたときだけ、言語に依存しないフォールバックが働きます。 +その場合、ダッシュボードの Startup Safety アクションが UAC を自動的に要求できます。フォール +バックでトークン状態を確認できない場合は、元のスケジューラエラーを保持します。他のタスクや操作 +は自動昇格 marker を生成できません。ダッシュボードの UAC を承認するか、管理者 PowerShell で +`ocx service install` を再実行してください。 ### `ocx codex-shim ` diff --git a/docs-site/src/content/docs/ko/reference/cli.md b/docs-site/src/content/docs/ko/reference/cli.md index 65fa7458ff2..db267493e1c 100644 --- a/docs-site/src/content/docs/ko/reference/cli.md +++ b/docs-site/src/content/docs/ko/reference/cli.md @@ -380,12 +380,12 @@ ocx service uninstall ``` Windows에서 작업 스케줄러 항목을 만들려면 권한 상승이 필요합니다. 인식 가능한 현지화 권한 거부 -문자열은 기존 안내 경로를 그대로 사용합니다. 문자열을 읽을 수 없을 때는 OpenCodex가 소유한 고정 -`opencodex-proxy` 작업 생성 명령이 상태 1로 실패하고 현재 토큰이 권한 상승되지 않았음이 확인돼야만 -언어 독립 fallback이 작동하며, 대시보드의 Startup Safety 작업이 UAC를 자동 요청할 수 있습니다. -fallback에서 토큰 상태를 확인할 수 없으면 원래 스케줄러 오류를 유지합니다. 다른 작업과 동작은 자동 -권한 상승 marker를 만들 수 없습니다. 대시보드의 UAC를 승인하거나 관리자 PowerShell에서 -`ocx service install`을 다시 실행하세요. +문자열은 기존 안내 경로를 그대로 사용합니다. 문자열을 읽을 수 없을 때는 명령 모양이 +`/create /tn opencodex-proxy /xml <비어 있지 않은 경로> /f`와 정확히 일치하고, 종료 상태가 1이며, +현재 토큰이 권한 상승되지 않았음이 확인돼야만 언어 독립 fallback이 작동합니다. 이때 대시보드의 +Startup Safety 작업이 UAC를 자동 요청할 수 있습니다. fallback에서 토큰 상태를 확인할 수 없으면 원래 +스케줄러 오류를 유지합니다. 다른 작업과 동작은 자동 권한 상승 marker를 만들 수 없습니다. 대시보드의 +UAC를 승인하거나 관리자 PowerShell에서 `ocx service install`을 다시 실행하세요. ### `ocx codex-shim ` diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index 7c0693a97d7..1d37ba14b9f 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -440,12 +440,12 @@ ocx service uninstall ``` On Windows, creating the Task Scheduler entry requires elevation. Recognized localized -access-denied text keeps the existing guidance path. If that text is unreadable, the fallback for -the owned, fixed `opencodex-proxy` task-create command additionally requires status 1 and a confirmed -non-elevated token; the dashboard's Startup Safety action can then request UAC automatically. If -that fallback cannot determine the token state, it retains the original scheduler error. Foreign -tasks and operations can never emit the automatic-elevation marker. Approve the dashboard UAC -prompt or rerun `ocx service install` in an elevated PowerShell window. +access-denied text keeps the existing guidance path. If that text is unreadable, the fallback +requires the owned command shape `/create /tn opencodex-proxy /xml /f`, status 1, +and a confirmed non-elevated token; the dashboard's Startup Safety action can then request UAC +automatically. If that fallback cannot determine the token state, it retains the original scheduler +error. Foreign tasks and operations can never emit the automatic-elevation marker. Approve the +dashboard UAC prompt or rerun `ocx service install` in an elevated PowerShell window. ### `ocx codex-shim ` diff --git a/docs-site/src/content/docs/ru/reference/cli.md b/docs-site/src/content/docs/ru/reference/cli.md index a9af6810c85..0c14563543f 100644 --- a/docs-site/src/content/docs/ru/reference/cli.md +++ b/docs-site/src/content/docs/ru/reference/cli.md @@ -403,12 +403,12 @@ ocx service uninstall В Windows для создания задания Планировщика требуется повышение прав. Распознанный локализованный текст об отказе в доступе продолжает использовать прежний путь подсказки. Если текст нечитаем, -независимый от языка fallback применяется только когда фиксированная команда создания принадлежащего -OpenCodex задания `opencodex-proxy` завершилась со статусом 1 и текущий токен подтверждён как -неповышенный; тогда Startup Safety может автоматически запросить UAC. Если fallback не может -определить состояние токена, сохраняется исходная ошибка Планировщика. Другие задания и операции не -могут создать marker автоматического повышения. Подтвердите UAC в панели или повторите -`ocx service install` в PowerShell от имени администратора. +независимый от языка fallback требует точную форму команды +`/create /tn opencodex-proxy /xml <непустой-путь> /f`, статус 1 и подтверждённый неповышенный токен; +тогда Startup Safety может автоматически запросить UAC. Если fallback не может определить состояние +токена, сохраняется исходная ошибка Планировщика. Другие задания и операции не могут создать marker +автоматического повышения. Подтвердите UAC в панели или повторите `ocx service install` в PowerShell +от имени администратора. ### `ocx codex-shim ` diff --git a/docs-site/src/content/docs/zh-cn/reference/cli.md b/docs-site/src/content/docs/zh-cn/reference/cli.md index defb1754358..4895752d365 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli.md @@ -364,11 +364,11 @@ ocx service uninstall ``` 在 Windows 上,创建任务计划程序条目需要提升权限。能识别的本地化“拒绝访问”文本继续使用现有 -处理路径。如果该文本不可读,则仅当固定且由 OpenCodex 所有的 `opencodex-proxy` 任务创建命令以 -状态 1 失败,并确认当前 token 未提升时,回退路径才会显示不依赖系统语言的说明;仪表板的 Startup -Safety 操作也可自动请求 UAC。如果回退路径无法确定 token 状态,则保留原始计划程序错误。其他任务 -和操作绝不会产生自动提升 marker。请批准仪表板的 UAC,或在管理员 PowerShell 中重新运行 -`ocx service install`。 +处理路径。如果该文本不可读,则回退路径要求命令形状严格为 +`/create /tn opencodex-proxy /xml <非空路径> /f`、退出状态为 1,并确认当前 token 未提升;此时 +仪表板的 Startup Safety 操作才可自动请求 UAC。如果回退路径无法确定 token 状态,则保留原始计划 +程序错误。其他任务和操作绝不会产生自动提升 marker。请批准仪表板的 UAC,或在管理员 PowerShell +中重新运行 `ocx service install`。 ### `ocx codex-shim `