Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/ja/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ ocx codex-shim status
ocx codex-shim uninstall
```

:::note[Windows のトークン環境]
新しく生成される Windows CMD と PowerShell のシムは、実行後に呼び出し元の `OPENCODEX_API_AUTH_TOKEN` を元の状態に戻します。Codex とその子プロセスには、引き続きトークンが継承される可能性があります。

OpenCodex の更新後、既存の Windows シムにこの動作を適用するには、`ocx codex-shim uninstall`、続いて `ocx codex-shim install` を実行して再作成してください。通常の更新では、正常な Windows シムは書き換えられません。
:::

:::tip[サービス vs シム]
常時オンのバックグラウンド プロキシには `ocx service` を使用します (推奨)。デーモンを使用しない軽量のオンデマンド起動には、`ocx codex-shim` を使用します。プロキシは、`codex` が起動された場合にのみ起動します。
:::
Expand Down
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/ko/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,12 @@ ocx codex-shim status
ocx codex-shim uninstall
```

:::note[Windows 토큰 환경]
새로 생성된 Windows CMD 및 PowerShell shim은 실행 후 호출자의 `OPENCODEX_API_AUTH_TOKEN`을 원래 상태로 복원합니다. Codex와 자식 프로세스는 여전히 토큰을 상속할 수 있습니다.

OpenCodex를 업데이트한 뒤 기존 Windows shim에 이 동작을 적용하려면 `ocx codex-shim uninstall`을 실행한 다음 `ocx codex-shim install`로 다시 설치하세요. 일반 업데이트는 정상인 Windows shim을 다시 작성하지 않습니다.
:::

:::tip[서비스와 shim]
항상 켜져 있는 백그라운드 프록시에는 `ocx service`를 사용합니다(권장). 데몬 없이 가볍게 필요할
때만 시작하려면 `ocx codex-shim`을 사용합니다. 이 경우 프록시는 `codex`를 실행할 때만 시작됩니다.
Expand Down
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,12 @@ ocx codex-shim status
ocx codex-shim uninstall
```

:::note[Windows token environment]
Newly generated Windows CMD and PowerShell shims restore the caller's `OPENCODEX_API_AUTH_TOKEN` after execution. Codex and its child processes can still inherit the token.

After updating OpenCodex, recreate an existing Windows shim with `ocx codex-shim uninstall` followed by `ocx codex-shim install` to obtain this behavior. An ordinary update does not rewrite a healthy Windows shim.
:::

:::tip[Service vs Shim]
Use `ocx service` for an always-on background proxy (recommended). Use `ocx codex-shim` for
lightweight, on-demand startup without a daemon — the proxy starts only when `codex` is launched.
Expand Down
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/ru/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,12 @@ ocx codex-shim status
ocx codex-shim uninstall
```

:::note[Окружение токена в Windows]
Новые обёртки для Windows CMD и PowerShell восстанавливают исходное состояние `OPENCODEX_API_AUTH_TOKEN` в вызывающей оболочке после выполнения. Codex и его дочерние процессы по-прежнему могут унаследовать токен.

После обновления OpenCodex пересоздайте существующую обёртку Windows командами `ocx codex-shim uninstall`, затем `ocx codex-shim install`, чтобы применить это поведение. Обычное обновление не перезаписывает исправную обёртку Windows.
:::

:::tip[Service vs Shim]
Используйте `ocx service` для всегда работающего фонового прокси (рекомендуется). Используйте
`ocx codex-shim` для лёгкого on-demand запуска без демона — в этом случае прокси стартует только
Expand Down
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ ocx codex-shim status
ocx codex-shim uninstall
```

:::note[Windows 令牌环境]
新生成的 Windows CMD 和 PowerShell shim 会在执行后恢复调用方原有的 `OPENCODEX_API_AUTH_TOKEN` 状态。Codex 及其子进程仍可能继承令牌。

更新 OpenCodex 后,如需让现有 Windows shim 采用此行为,请先运行 `ocx codex-shim uninstall`,再运行 `ocx codex-shim install` 重新安装。常规更新不会重写正常的 Windows shim。
:::

:::tip[Service vs Shim]
将 `ocx service` 用于始终在线的后台代理(推荐)。将 `ocx codex-shim` 用于无需守护进程的轻量按需启动——代理只会在启动 `codex` 时运行。
:::
Expand Down
11 changes: 10 additions & 1 deletion src/codex/shim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,7 @@ export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cl
const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n");
return `@echo off\r
rem ${SHIM_MARKER}\r
setlocal\r
${windowsBatchSet("OCX_REAL_CODEX", realCodexPath)}\r
${windowsBatchSet("OCX_BUN", bunPath)}\r
${windowsBatchSet("OCX_CLI", cliPath)}\r
Expand Down Expand Up @@ -1115,6 +1116,9 @@ export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath:
const tokenFile = serviceApiTokenFilePath();
return `#!/usr/bin/env pwsh
# ${SHIM_MARKER}
$hadApiAuthToken = Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN
$priorApiAuthToken = $env:OPENCODEX_API_AUTH_TOKEN
try {
if (-not $env:OPENCODEX_API_AUTH_TOKEN -and (Test-Path -LiteralPath ${psString(tokenFile)})) {
$env:OPENCODEX_API_AUTH_TOKEN = (Get-Content -Raw -LiteralPath ${psString(tokenFile)}).Trim()
}
Expand Down Expand Up @@ -1147,7 +1151,12 @@ if (-not $skipEnsure) {
}
}
& ${psString(realCodexPath)} @args
exit $LASTEXITCODE
$codexExitCode = $LASTEXITCODE
} finally {
if ($hadApiAuthToken) { $env:OPENCODEX_API_AUTH_TOKEN = $priorApiAuthToken }
else { Remove-Item Env:\\OPENCODEX_API_AUTH_TOKEN -ErrorAction SilentlyContinue }
}
exit $codexExitCode
`;
}

Expand Down
108 changes: 107 additions & 1 deletion tests/codex-integration/codex-shim.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { tmpdir } from "node:os";
import { autoRestoreCodexShim, buildUnixCodexShim, buildWindowsCodexShim, buildWindowsPowerShellCodexShim, diagnoseCodexShim, findCodexOnPath, inspectCodexShimBackingForCommand, installCodexShim, isLocalAbsoluteInspectionPath, isVersionManagerOwnedCodexPath, isWindowsInteropDir, lastCodexDiscoveryError, setCodexShimFreshWriteHookForTests, setCodexShimGuardedWriteHookForTests, setCodexShimProbeHookForTests, setCodexShimProbeObservationMsForTests, setCodexShimProbeShellForTests, setCodexShimRollbackRestoreHookForTests, uninstallCodexShim } from "../../src/codex/shim";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { repoPath, repoRoot } from "../helpers/repo-root";
import { INTERNAL_DEADLINE_MS } from "../helpers/test-budget";
import { INTERNAL_DEADLINE_MS, SPAWN_BUDGET_MS } from "../helpers/test-budget";

const SHIM_MARKER = "opencodex codex autostart shim";
const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision 2";
Expand Down Expand Up @@ -1296,6 +1296,112 @@ printf '%s\\n' child-codex
}
});

for (const shell of ["cmd", "powershell", "pwsh"] as const) {
const cases = [
{ callerToken: undefined, bypass: false, label: "missing" },
...(shell === "cmd" ? [] : [{ callerToken: "", bypass: false, label: "empty" }]),
{ callerToken: "caller-token", bypass: false, label: "explicit token, ensure" },
{ callerToken: "caller-token", bypass: true, label: "explicit token, bypass" },
];
for (const { callerToken, bypass, label } of cases) {
test.skipIf(process.platform !== "win32")(`Windows ${shell} shim restores the caller token (${label})`, () => {
const dir = mkdtempSync(join(tmpdir(), "ocx-shim-token-scope-"));
const oldHome = process.env.OPENCODEX_HOME;
try {
process.env.OPENCODEX_HOME = dir;
const extension = shell === "cmd" ? "cmd" : "ps1";
const realPath = join(dir, `codex-real.${extension}`);
const wrapperPath = join(dir, `codex.${extension}`);
const driverPath = join(dir, `driver.${extension}`);
const ensurePath = join(dir, "ensure.ts");
const ensureLog = join(dir, "ensure.log");
writeFileSync(join(dir, "service-api-token"), "file-token\n");
writeFileSync(ensurePath, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(ensureLog)}, "ensure"); process.exit(19);`);
if (shell === "cmd") {
writeFileSync(realPath, "@echo off\r\necho child:%OPENCODEX_API_AUTH_TOKEN%\r\nexit /b 37\r\n");
writeFileSync(wrapperPath, buildWindowsCodexShim(realPath, process.execPath, ensurePath, "process"));
writeFileSync(driverPath, `@echo off\r\ncall "${wrapperPath}" exec "arg value"\r\nset "result=%ERRORLEVEL%"\r\necho after:%OPENCODEX_API_AUTH_TOKEN%\r\necho result:%result%\r\nexit /b 0\r\n`);
} else {
writeFileSync(realPath, '"child:$env:OPENCODEX_API_AUTH_TOKEN"\nexit 37\n');
writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realPath, process.execPath, ensurePath, "process")}`);
const emptyToken = callerToken === "" ? "$env:OPENCODEX_API_AUTH_TOKEN = ''\n" : "";
writeFileSync(driverPath, `\uFEFF$ErrorActionPreference = 'Stop'\n${emptyToken}$beforePresence = Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN\n& '${wrapperPath.replace(/'/g, "''")}' exec 'arg value'\n$result = $LASTEXITCODE\n"after:$env:OPENCODEX_API_AUTH_TOKEN"\n"result:$result"\n"presence-preserved:$($beforePresence -eq (Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN))"\n`);
}
const env = shimChildEnv({
OPENCODEX_HOME: dir,
OPENCODEX_API_AUTH_TOKEN: callerToken ?? "",
OCX_SHIM_BYPASS: bypass ? "1" : "",
});
if (callerToken === undefined) delete env.OPENCODEX_API_AUTH_TOKEN;
const result = shell === "cmd"
? spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/c", "driver.cmd"], { cwd: dir, env, encoding: "utf8", timeout: INTERNAL_DEADLINE_MS, windowsHide: true })
: spawnSync(`${shell}.exe`, ["-NoProfile", "-NonInteractive", "-File", driverPath], { env, encoding: "utf8", timeout: INTERNAL_DEADLINE_MS, windowsHide: true });
expect(result.status, result.stderr).toBe(0);
expect(result.stdout.trim().split(/\r?\n/)).toEqual([
`child:${callerToken || "file-token"}`,
`after:${callerToken ?? ""}`,
"result:37",
...(shell === "cmd" ? [] : ["presence-preserved:True"]),
]);
expect(existsSync(ensureLog)).toBe(!bypass);
} finally {
if (oldHome === undefined) delete process.env.OPENCODEX_HOME;
else process.env.OPENCODEX_HOME = oldHome;
removeTreeWithRetry(dir);
}
}, SPAWN_BUDGET_MS);
}
}

for (const failurePhase of ["ensure", "Codex"]) {
for (const executable of ["powershell.exe", "pwsh.exe"]) {
for (const callerToken of [undefined, "", "caller-token"]) {
test.skipIf(process.platform !== "win32")(`Windows ${executable} shim restores ${callerToken === undefined ? "missing" : callerToken === "" ? "empty" : "explicit"} caller token when ${failurePhase} throws`, () => {
const dir = mkdtempSync(join(tmpdir(), "ocx-shim-token-error-"));
const oldHome = process.env.OPENCODEX_HOME;
try {
process.env.OPENCODEX_HOME = dir;
const wrapperPath = join(dir, "codex.ps1");
const ensurePath = join(dir, "throw.ps1");
const driverPath = join(dir, "driver.ps1");
const realPath = join(dir, "codex-real.ps1");
writeFileSync(join(dir, "service-api-token"), "file-token\n");
writeFileSync(ensurePath, failurePhase === "ensure" ? "throw 'fixture ensure failure'\n" : "exit 19\n");
writeFileSync(realPath, "throw 'fixture Codex failure'\n");
writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realPath, ensurePath, "unused.ts", "process")}`);
const emptyToken = callerToken === "" ? "$env:OPENCODEX_API_AUTH_TOKEN = ''\n" : "";
writeFileSync(driverPath, `\uFEFF$ErrorActionPreference = 'Stop'\n${emptyToken}$beforePresence = Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN\ntry { & '${wrapperPath.replace(/'/g, "''")}' exec } catch { "error:$($_.Exception.Message)" }\n"after:$env:OPENCODEX_API_AUTH_TOKEN"\n"presence-preserved:$($beforePresence -eq (Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN))"\n`);
const env = shimChildEnv({ OPENCODEX_HOME: dir, OPENCODEX_API_AUTH_TOKEN: callerToken ?? "", OCX_SHIM_BYPASS: "" });
if (callerToken === undefined) delete env.OPENCODEX_API_AUTH_TOKEN;
const result = spawnSync(executable, ["-NoProfile", "-NonInteractive", "-File", driverPath], {
env, encoding: "utf8", timeout: INTERNAL_DEADLINE_MS, windowsHide: true,
});
expect(result.error).toBeUndefined();
expect(result.status, result.stderr).toBe(0);
expect(result.stdout.trim().split(/\r?\n/)).toEqual([
`error:fixture ${failurePhase} failure`, `after:${callerToken ?? ""}`, "presence-preserved:True",
]);

// A failed process must complete, rather than satisfy the check through a timeout.
writeFileSync(driverPath, `\uFEFF$ErrorActionPreference = 'Stop'\n& '${wrapperPath.replace(/'/g, "''")}' exec\n`);
const uncaught = spawnSync(executable, ["-NoProfile", "-NonInteractive", "-File", driverPath], {
env, encoding: "utf8", timeout: INTERNAL_DEADLINE_MS, windowsHide: true,
});
expect(uncaught.error).toBeUndefined();
expect(uncaught.signal).toBeNull();
expect(typeof uncaught.status, uncaught.stderr).toBe("number");
expect(uncaught.status, uncaught.stderr).not.toBe(0);
expect(uncaught.stderr).toContain(`fixture ${failurePhase} failure`);
} finally {
if (oldHome === undefined) delete process.env.OPENCODEX_HOME;
else process.env.OPENCODEX_HOME = oldHome;
removeTreeWithRetry(dir);
}
}, SPAWN_BUDGET_MS);
}
}
}

test("Unix shim skips ocx startup only for Codex management commands", () => {
if (process.platform === "win32") return;

Expand Down
Loading