From 5ef812293b08be065badac552c5656aaaf34093d Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 26 May 2026 16:08:32 +0200 Subject: [PATCH] fix(codex): bridge cli api-key auth into app-server --- .../codex/src/app-server/auth-bridge.test.ts | 98 +++++++++++++++++++ .../codex/src/app-server/auth-bridge.ts | 86 +++++++++++++++- extensions/codex/src/app-server/config.ts | 7 +- .../codex/src/app-server/run-attempt.ts | 13 +-- .../src/app-server/shared-client.test.ts | 30 ++++++ .../codex/src/app-server/shared-client.ts | 5 + extensions/codex/src/migration/apply.ts | 4 +- scripts/test-live-codex-harness-docker.sh | 18 +++- .../package-acceptance-workflow.test.ts | 28 +++--- .../test-live-codex-harness-docker.test.ts | 12 +++ 10 files changed, 270 insertions(+), 31 deletions(-) diff --git a/extensions/codex/src/app-server/auth-bridge.test.ts b/extensions/codex/src/app-server/auth-bridge.test.ts index 9bd3281a42b9..cf08864f1462 100644 --- a/extensions/codex/src/app-server/auth-bridge.test.ts +++ b/extensions/codex/src/app-server/auth-bridge.test.ts @@ -13,6 +13,7 @@ import { refreshCodexAppServerAuthTokens, resolveCodexAppServerAuthAccountCacheKey, resolveCodexAppServerAuthProfileId, + resolveCodexAppServerFallbackApiKeyCacheKey, resolveCodexAppServerHomeDir, resolveCodexAppServerNativeHomeDir, } from "./auth-bridge.js"; @@ -172,6 +173,17 @@ async function writeCodexCliAuthFile(codexHome: string): Promise { ); } +async function writeCodexCliApiKeyAuthFile(codexHome: string): Promise { + await fs.mkdir(codexHome, { recursive: true }); + await fs.writeFile( + path.join(codexHome, "auth.json"), + `${JSON.stringify({ + auth_mode: "apikey", + OPENAI_API_KEY: "cli-auth-json-api-key", + })}\n`, + ); +} + describe("bridgeCodexAppServerStartOptions", () => { it("sets agent-owned CODEX_HOME without overriding HOME for local app-server launches", async () => { const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); @@ -1042,6 +1054,92 @@ describe("bridgeCodexAppServerStartOptions", () => { } }); + it("uses Codex CLI api-key auth.json when no auth profile or env key exists", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); + const agentDir = path.join(root, "agent"); + const codexHome = path.join(root, "codex-cli"); + const request = vi.fn(async (method: string) => { + if (method === "account/read") { + return { account: null, requiresOpenaiAuth: true }; + } + return { type: "apiKey" }; + }); + vi.stubEnv("CODEX_HOME", codexHome); + vi.stubEnv("CODEX_API_KEY", ""); + vi.stubEnv("OPENAI_API_KEY", ""); + try { + await writeCodexCliApiKeyAuthFile(codexHome); + + await applyCodexAppServerAuthProfile({ + client: { request } as never, + agentDir, + startOptions: createStartOptions({ + env: { CODEX_HOME: path.join(root, "isolated-codex-home") }, + }), + }); + + expect(request).toHaveBeenNthCalledWith(1, "account/read", { refreshToken: false }); + expect(request).toHaveBeenNthCalledWith(2, "account/login/start", { + type: "apiKey", + apiKey: "cli-auth-json-api-key", + }); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it("includes Codex CLI api-key auth.json in fallback app-server cache keys", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); + const codexHome = path.join(root, "codex-cli"); + try { + await writeCodexCliApiKeyAuthFile(codexHome); + + const first = resolveCodexAppServerFallbackApiKeyCacheKey({ + startOptions: createStartOptions(), + baseEnv: { CODEX_HOME: codexHome }, + }); + await fs.writeFile( + path.join(codexHome, "auth.json"), + `${JSON.stringify({ + auth_mode: "apikey", + OPENAI_API_KEY: "second-cli-auth-json-api-key", + })}\n`, + ); + const second = resolveCodexAppServerFallbackApiKeyCacheKey({ + startOptions: createStartOptions(), + baseEnv: { CODEX_HOME: codexHome }, + }); + + expect(first).toMatch(/^CODEX_AUTH_JSON:sha256:[a-f0-9]{64}$/); + expect(second).toMatch(/^CODEX_AUTH_JSON:sha256:[a-f0-9]{64}$/); + expect(second).not.toBe(first); + expect(first).not.toContain("cli-auth-json-api-key"); + expect(second).not.toContain("second-cli-auth-json-api-key"); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it("does not include Codex CLI api-key auth.json in websocket fallback cache keys", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); + const codexHome = path.join(root, "codex-cli"); + try { + await writeCodexCliApiKeyAuthFile(codexHome); + + expect( + resolveCodexAppServerFallbackApiKeyCacheKey({ + startOptions: createStartOptions({ + transport: "websocket", + url: "ws://127.0.0.1:1455", + }), + baseEnv: { CODEX_HOME: codexHome }, + }), + ).toBeUndefined(); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + it("honors clearEnv before env API-key fallback", async () => { const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); const request = vi.fn(async (method: string) => { diff --git a/extensions/codex/src/app-server/auth-bridge.ts b/extensions/codex/src/app-server/auth-bridge.ts index 0cdc49bab1b3..aeabfbf71999 100644 --- a/extensions/codex/src/app-server/auth-bridge.ts +++ b/extensions/codex/src/app-server/auth-bridge.ts @@ -1,5 +1,7 @@ import { createHash } from "node:crypto"; +import fsSync from "node:fs"; import fs from "node:fs/promises"; +import os from "node:os"; import path from "node:path"; import { ensureAuthProfileStore, @@ -35,6 +37,8 @@ const CODEX_API_KEY_ENV_VAR = "CODEX_API_KEY"; const OPENAI_API_KEY_ENV_VAR = "OPENAI_API_KEY"; const CODEX_APP_SERVER_API_KEY_ENV_VARS = [CODEX_API_KEY_ENV_VAR, OPENAI_API_KEY_ENV_VAR]; const CODEX_APP_SERVER_HOME_ENV_VARS = [CODEX_HOME_ENV_VAR, HOME_ENV_VAR]; +const CODEX_AUTH_JSON_FILENAME = "auth.json"; +const CODEX_HOME_DIRNAME = ".codex"; type AuthProfileOrderConfig = Parameters[0]["cfg"]; @@ -228,6 +232,20 @@ export function resolveCodexAppServerEnvApiKeyCacheKey(params: { return `${apiKey.key}:sha256:${hash.digest("hex")}`; } +export function resolveCodexAppServerFallbackApiKeyCacheKey(params: { + startOptions: Pick; + baseEnv?: NodeJS.ProcessEnv; + platform?: NodeJS.Platform; +}): string | undefined { + if (params.startOptions.transport !== "stdio") { + return undefined; + } + return ( + resolveCodexAppServerEnvApiKeyCacheKey(params) ?? + resolveCodexCliAuthFileApiKeyCacheKey(params.baseEnv ?? process.env) + ); +} + function fingerprintApiKeyAuthProfileCacheKey(apiKey: string): string { const hash = createHash("sha256"); hash.update("openclaw:codex:app-server-auth-profile-api-key:v1"); @@ -244,6 +262,14 @@ function fingerprintTokenAuthProfileCacheKey(accessToken: string): string { return `token:sha256:${hash.digest("hex")}`; } +function fingerprintCodexCliAuthFileApiKeyCacheKey(apiKey: string): string { + const hash = createHash("sha256"); + hash.update("openclaw:codex:app-server-cli-auth-json-api-key:v1"); + hash.update("\0"); + hash.update(apiKey); + return `CODEX_AUTH_JSON:sha256:${hash.digest("hex")}`; +} + export function resolveCodexAppServerHomeDir(agentDir: string): string { return path.join(path.resolve(agentDir), CODEX_APP_SERVER_HOME_DIRNAME); } @@ -312,9 +338,10 @@ export async function applyCodexAppServerAuthProfile(params: { return; } const env = resolveCodexAppServerSpawnEnv(params.startOptions, process.env); - const fallbackLoginParams = await resolveCodexAppServerEnvApiKeyLoginParams({ + const fallbackLoginParams = await resolveCodexAppServerFallbackApiKeyLoginParams({ client: params.client, env, + codexCliAuthEnv: process.env, }); if (fallbackLoginParams) { await params.client.request("account/login/start", fallbackLoginParams); @@ -392,11 +419,14 @@ async function resolveCodexAppServerAuthProfileLoginParamsInternal(params: { return loginParams; } -async function resolveCodexAppServerEnvApiKeyLoginParams(params: { +async function resolveCodexAppServerFallbackApiKeyLoginParams(params: { client: CodexAppServerClient; env: NodeJS.ProcessEnv; + codexCliAuthEnv: NodeJS.ProcessEnv; }): Promise { - const apiKey = readFirstNonEmptyEnv(params.env, CODEX_APP_SERVER_API_KEY_ENV_VARS); + const apiKey = + readFirstNonEmptyEnv(params.env, CODEX_APP_SERVER_API_KEY_ENV_VARS) ?? + (await readCodexCliAuthFileApiKey(params.codexCliAuthEnv)); if (!apiKey) { return undefined; } @@ -409,6 +439,56 @@ async function resolveCodexAppServerEnvApiKeyLoginParams(params: { return { type: "apiKey", apiKey }; } +function resolveCodexCliAuthFilePath(env: NodeJS.ProcessEnv): string { + const configuredCodexHome = env[CODEX_HOME_ENV_VAR]?.trim(); + if (configuredCodexHome) { + return path.join(resolveHomeRelativePath(configuredCodexHome, env), CODEX_AUTH_JSON_FILENAME); + } + const home = env[HOME_ENV_VAR]?.trim() || env.USERPROFILE?.trim() || os.homedir(); + return path.join(home, CODEX_HOME_DIRNAME, CODEX_AUTH_JSON_FILENAME); +} + +function resolveHomeRelativePath(value: string, env: NodeJS.ProcessEnv): string { + if (value === "~" || value.startsWith("~/") || value.startsWith("~\\")) { + const home = env[HOME_ENV_VAR]?.trim() || env.USERPROFILE?.trim() || os.homedir(); + return path.join(home, value.slice(value === "~" ? 1 : 2)); + } + return value; +} + +function parseCodexCliAuthFileApiKey(raw: string): string | undefined { + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + return undefined; + } + if (!parsed || typeof parsed !== "object") { + return undefined; + } + const apiKey = (parsed as Record).OPENAI_API_KEY; + return typeof apiKey === "string" && apiKey.trim() ? apiKey.trim() : undefined; +} + +async function readCodexCliAuthFileApiKey(env: NodeJS.ProcessEnv): Promise { + try { + return parseCodexCliAuthFileApiKey(await fs.readFile(resolveCodexCliAuthFilePath(env), "utf8")); + } catch { + return undefined; + } +} + +function resolveCodexCliAuthFileApiKeyCacheKey(env: NodeJS.ProcessEnv): string | undefined { + try { + const apiKey = parseCodexCliAuthFileApiKey( + fsSync.readFileSync(resolveCodexCliAuthFilePath(env), "utf8"), + ); + return apiKey ? fingerprintCodexCliAuthFileApiKeyCacheKey(apiKey) : undefined; + } catch { + return undefined; + } +} + async function resolveLoginParamsForCredential( profileId: string, credential: AuthProfileCredential, diff --git a/extensions/codex/src/app-server/config.ts b/extensions/codex/src/app-server/config.ts index 36c533b70ea2..1448ad65d9a8 100644 --- a/extensions/codex/src/app-server/config.ts +++ b/extensions/codex/src/app-server/config.ts @@ -535,7 +535,11 @@ export function resolveCodexComputerUseConfig( export function codexAppServerStartOptionsKey( options: CodexAppServerStartOptions, - params: { authProfileId?: string; agentDir?: string } = {}, + params: { + authProfileId?: string; + agentDir?: string; + fallbackApiKeyCacheKey?: string; + } = {}, ): string { return JSON.stringify({ transport: options.transport, @@ -553,6 +557,7 @@ export function codexAppServerStartOptionsKey( clearEnv: [...(options.clearEnv ?? [])].toSorted(), authProfileId: params.authProfileId ?? null, agentDir: params.agentDir ?? null, + fallbackApiKeyCacheKey: params.fallbackApiKeyCacheKey ?? null, }); } diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index 9cb1d4ad7843..bf3386b62796 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -66,7 +66,7 @@ import { handleCodexAppServerApprovalRequest } from "./approval-bridge.js"; import { refreshCodexAppServerAuthTokens, resolveCodexAppServerAuthAccountCacheKey, - resolveCodexAppServerEnvApiKeyCacheKey, + resolveCodexAppServerFallbackApiKeyCacheKey, resolveCodexAppServerHomeDir, resolveCodexAppServerAuthProfileId, resolveCodexAppServerAuthProfileIdForAgent, @@ -1067,7 +1067,7 @@ export async function runCodexAppServerAttempt( }); const startupEnvApiKeyCacheKey = startupAuthProfileId ? undefined - : resolveCodexAppServerEnvApiKeyCacheKey({ + : resolveCodexAppServerFallbackApiKeyCacheKey({ startOptions: appServer.start, }); const nodeExecBlocksNativeExecution = isCodexNativeExecutionBlockedByNodeExecHost(params, { @@ -2722,9 +2722,8 @@ export async function runCodexAppServerAttempt( const codexDiagnosticToolDefinitions = codexModelContentCapture.toolDefinitions ? buildCodexDiagnosticToolDefinitions(tools) : undefined; - const codexModelContentPrivateData = ( - modelContent: DiagnosticModelCallContent | undefined, - ) => (modelContent && Object.keys(modelContent).length > 0 ? { modelContent } : undefined); + const codexModelContentPrivateData = (modelContent: DiagnosticModelCallContent | undefined) => + modelContent && Object.keys(modelContent).length > 0 ? { modelContent } : undefined; const buildCodexModelCallDiagnosticContent = (): DiagnosticModelCallContent | undefined => { const modelContent = { ...(codexModelContentCapture.inputMessages @@ -2768,9 +2767,7 @@ export async function runCodexAppServerAttempt( ...buildCodexModelCallDiagnosticContent(), ...(codexModelContentCapture.outputMessages ? { - outputMessages: result.lastAssistant - ? [result.lastAssistant] - : result.assistantTexts, + outputMessages: result.lastAssistant ? [result.lastAssistant] : result.assistantTexts, } : {}), }), diff --git a/extensions/codex/src/app-server/shared-client.test.ts b/extensions/codex/src/app-server/shared-client.test.ts index 5bf0602e29d0..4c1b86294f78 100644 --- a/extensions/codex/src/app-server/shared-client.test.ts +++ b/extensions/codex/src/app-server/shared-client.test.ts @@ -12,6 +12,7 @@ const mocks = vi.hoisted(() => ({ resolveCodexAppServerAuthProfileIdForAgent: vi.fn( (params?: { authProfileId?: string }) => params?.authProfileId, ), + resolveCodexAppServerFallbackApiKeyCacheKey: vi.fn(() => undefined as string | undefined), resolveManagedCodexAppServerStartOptions: vi.fn(async (startOptions) => startOptions), embeddedAgentLog: { debug: vi.fn(), warn: vi.fn() }, resolveDefaultAgentDir: vi.fn(() => "/tmp/openclaw-agent"), @@ -21,6 +22,7 @@ vi.mock("./auth-bridge.js", () => ({ applyCodexAppServerAuthProfile: mocks.applyCodexAppServerAuthProfile, bridgeCodexAppServerStartOptions: mocks.bridgeCodexAppServerStartOptions, resolveCodexAppServerAuthProfileIdForAgent: mocks.resolveCodexAppServerAuthProfileIdForAgent, + resolveCodexAppServerFallbackApiKeyCacheKey: mocks.resolveCodexAppServerFallbackApiKeyCacheKey, })); vi.mock("./managed-binary.js", () => ({ @@ -129,6 +131,8 @@ describe("shared Codex app-server client", () => { mocks.resolveCodexAppServerAuthProfileIdForAgent.mockImplementation( (params?: { authProfileId?: string }) => params?.authProfileId, ); + mocks.resolveCodexAppServerFallbackApiKeyCacheKey.mockClear(); + mocks.resolveCodexAppServerFallbackApiKeyCacheKey.mockReturnValue(undefined); mocks.resolveManagedCodexAppServerStartOptions.mockClear(); mocks.resolveManagedCodexAppServerStartOptions.mockImplementation( async (startOptions) => startOptions, @@ -408,6 +412,32 @@ describe("shared Codex app-server client", () => { expect(first.process.stdin.destroyed).toBe(false); }); + it("starts an independent shared client when fallback api-key auth changes", async () => { + const first = createClientHarness(); + const second = createClientHarness(); + const startSpy = vi + .spyOn(CodexAppServerClient, "start") + .mockReturnValueOnce(first.client) + .mockReturnValueOnce(second.client); + mocks.resolveCodexAppServerFallbackApiKeyCacheKey + .mockReturnValueOnce("api-key:first") + .mockReturnValueOnce("api-key:second"); + + const firstList = listCodexAppServerModels({ timeoutMs: 1000 }); + await sendInitializeResult(first, "openclaw/0.125.0 (macOS; test)"); + await sendEmptyModelList(first); + await expect(firstList).resolves.toEqual({ models: [] }); + + const secondList = listCodexAppServerModels({ timeoutMs: 1000 }); + await sendInitializeResult(second, "openclaw/0.125.0 (macOS; test)"); + await sendEmptyModelList(second); + await expect(secondList).resolves.toEqual({ models: [] }); + + expect(startSpy).toHaveBeenCalledTimes(2); + expect(first.process.stdin.destroyed).toBe(false); + expect(second.process.stdin.destroyed).toBe(false); + }); + it("does not let one shared-client failure tear down another keyed client", async () => { const first = createClientHarness(); const second = createClientHarness(); diff --git a/extensions/codex/src/app-server/shared-client.ts b/extensions/codex/src/app-server/shared-client.ts index 8df85c8eea44..7260492f7c40 100644 --- a/extensions/codex/src/app-server/shared-client.ts +++ b/extensions/codex/src/app-server/shared-client.ts @@ -3,6 +3,7 @@ import { applyCodexAppServerAuthProfile, bridgeCodexAppServerStartOptions, resolveCodexAppServerAuthProfileIdForAgent, + resolveCodexAppServerFallbackApiKeyCacheKey, } from "./auth-bridge.js"; import { CodexAppServerClient } from "./client.js"; import { @@ -97,9 +98,13 @@ export async function getSharedCodexAppServerClient(options?: { authProfileId: usesNativeAuth ? null : authProfileId, config: options?.config, }); + const fallbackApiKeyCacheKey = authProfileId + ? undefined + : resolveCodexAppServerFallbackApiKeyCacheKey({ startOptions }); const key = codexAppServerStartOptionsKey(startOptions, { authProfileId, agentDir: usesNativeAuth ? undefined : agentDir, + fallbackApiKeyCacheKey, }); const state = getSharedCodexAppServerClientState(); const entry = getOrCreateSharedClientEntry(state, key); diff --git a/extensions/codex/src/migration/apply.ts b/extensions/codex/src/migration/apply.ts index cb50f73120b3..4f05ca3cf93a 100644 --- a/extensions/codex/src/migration/apply.ts +++ b/extensions/codex/src/migration/apply.ts @@ -25,7 +25,7 @@ import { defaultCodexAppInventoryCache } from "../app-server/app-inventory-cache import { resolveCodexAppServerAuthAccountCacheKey, resolveCodexAppServerAuthProfileIdForAgent, - resolveCodexAppServerEnvApiKeyCacheKey, + resolveCodexAppServerFallbackApiKeyCacheKey, } from "../app-server/auth-bridge.js"; import { CODEX_PLUGINS_MARKETPLACE_NAME, @@ -393,7 +393,7 @@ async function buildTargetCodexPluginAppCacheKey(ctx: MigrationProviderContext): }); const envApiKeyFingerprint = authProfileId ? undefined - : resolveCodexAppServerEnvApiKeyCacheKey({ + : resolveCodexAppServerFallbackApiKeyCacheKey({ startOptions: appServer.start, }); return buildCodexPluginAppCacheKey({ diff --git a/scripts/test-live-codex-harness-docker.sh b/scripts/test-live-codex-harness-docker.sh index 0529fe92b627..ea6e2b5d8afe 100644 --- a/scripts/test-live-codex-harness-docker.sh +++ b/scripts/test-live-codex-harness-docker.sh @@ -17,6 +17,7 @@ CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}" WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}" PROFILE_FILE="$(openclaw_live_default_profile_file)" CODEX_HARNESS_AUTH_MODE="${OPENCLAW_LIVE_CODEX_HARNESS_AUTH:-codex-auth}" +CODEX_CLI_PACKAGE_SPEC="${OPENCLAW_LIVE_CODEX_CLI_PACKAGE_SPEC:-}" TEMP_DIRS=() DOCKER_USER="${OPENCLAW_DOCKER_USER:-node}" DOCKER_HOME_MOUNT=() @@ -70,6 +71,16 @@ if [[ "$CODEX_HARNESS_AUTH_MODE" != "api-key" && ! -s "$HOME/.codex/auth.json" ] fi exit 1 fi +if [[ -z "$CODEX_CLI_PACKAGE_SPEC" ]]; then + CODEX_CLI_PACKAGE_SPEC="$( + node -e ' + const pkg = require(process.argv[1]); + const version = pkg.dependencies?.["@openai/codex"]; + if (!version || typeof version !== "string") process.exit(1); + process.stdout.write(`@openai/codex@${version}`); + ' "$ROOT_DIR/extensions/codex/package.json" + )" +fi cleanup_temp_dirs() { if ((${#TEMP_DIRS[@]} > 0)); then @@ -227,9 +238,8 @@ trusted_scripts_dir="${OPENCLAW_LIVE_DOCKER_SCRIPTS_DIR:-/src/scripts}" if [ "${OPENCLAW_LIVE_CODEX_HARNESS_AUTH:-codex-auth}" != "api-key" ]; then node --import tsx "$trusted_scripts_dir/prepare-codex-ci-auth.ts" "$HOME/.codex/auth.json" fi -if [ ! -x "$NPM_CONFIG_PREFIX/bin/codex" ]; then - run_setup_command npm install -g @openai/codex -fi +run_setup_command npm install -g "$OPENCLAW_LIVE_CODEX_CLI_PACKAGE_SPEC" +"$NPM_CONFIG_PREFIX/bin/codex" --version if [ "${OPENCLAW_LIVE_CODEX_HARNESS_AUTH:-codex-auth}" = "api-key" ]; then printf '%s\n' "$OPENAI_API_KEY" | "$NPM_CONFIG_PREFIX/bin/codex" login --with-api-key >/dev/null fi @@ -299,6 +309,7 @@ echo "==> Auth mode: $CODEX_HARNESS_AUTH_MODE" echo "==> Profile file: $PROFILE_STATUS" echo "==> CI-safe Codex config: ${OPENCLAW_LIVE_CODEX_HARNESS_USE_CI_SAFE_CODEX_CONFIG:-1}" echo "==> Test files: ${OPENCLAW_LIVE_CODEX_TEST_FILES:-src/gateway/gateway-codex-harness.live.test.ts}" +echo "==> Codex CLI package: $CODEX_CLI_PACKAGE_SPEC" echo "==> Harness fallback: none" echo "==> Auth files: ${AUTH_FILES_CSV:-none}" DOCKER_RUN_ARGS=() @@ -334,6 +345,7 @@ DOCKER_RUN_ARGS+=(--rm -t \ -e OPENCLAW_LIVE_CODEX_HARNESS_SUBAGENT_ONLY="${OPENCLAW_LIVE_CODEX_HARNESS_SUBAGENT_ONLY:-}" \ -e OPENCLAW_LIVE_CODEX_HARNESS_SUBAGENT_PROBE="${OPENCLAW_LIVE_CODEX_HARNESS_SUBAGENT_PROBE:-1}" \ -e OPENCLAW_LIVE_CODEX_HARNESS_USE_CI_SAFE_CODEX_CONFIG="${OPENCLAW_LIVE_CODEX_HARNESS_USE_CI_SAFE_CODEX_CONFIG:-1}" \ + -e OPENCLAW_LIVE_CODEX_CLI_PACKAGE_SPEC="$CODEX_CLI_PACKAGE_SPEC" \ -e OPENCLAW_CLI_BACKEND_LOG_OUTPUT="${OPENCLAW_CLI_BACKEND_LOG_OUTPUT:-}" \ -e OPENCLAW_TEST_CONSOLE="${OPENCLAW_TEST_CONSOLE:-}" \ -e OPENCLAW_LIVE_DOCKER_SCRIPTS_DIR="${DOCKER_TRUSTED_HARNESS_CONTAINER_DIR}/scripts" \ diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index abe6b9fc7c9a..45465f728e50 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -88,9 +88,7 @@ describe("package acceptance workflow", () => { expect(packageJson.packageManager).toMatch(/^pnpm@\d+\.\d+\.\d+\+sha512\.[a-f0-9]+$/u); expect(setupPnpmAction).toContain("Setup pnpm from packageManager"); - expect(setupPnpmAction).toContain( - "PACKAGE_MANAGER_FILE: ${{ inputs.package-manager-file }}", - ); + expect(setupPnpmAction).toContain("PACKAGE_MANAGER_FILE: ${{ inputs.package-manager-file }}"); expect(setupPnpmAction).toContain('case "$package_manager" in'); expect(setupPnpmAction).toContain('corepack prepare "$package_manager" --activate'); expect(setupPnpmAction).toContain( @@ -650,7 +648,9 @@ describe("package artifact reuse", () => { expect(scheduler).toContain('liveDockerHarnessScriptCommand("test-live-build-docker.sh")'); expect(liveDockerAuth).toContain("codex-cli | openai | openai-codex)"); expect(liveDockerAuth).toContain("openclaw_live_init_docker_run_args()"); - expect(liveDockerAuth).toContain('timeout_value="${2:-${OPENCLAW_LIVE_DOCKER_RUN_TIMEOUT:-2700s}}"'); + expect(liveDockerAuth).toContain( + 'timeout_value="${2:-${OPENCLAW_LIVE_DOCKER_RUN_TIMEOUT:-2700s}}"', + ); expect(harness).toContain('source "$TRUSTED_HARNESS_DIR/scripts/lib/live-docker-auth.sh"'); expect(harness).not.toContain('source "$ROOT_DIR/scripts/lib/live-docker-auth.sh"'); expect(harness).toContain( @@ -681,22 +681,22 @@ describe("package artifact reuse", () => { ); } expect(readFileSync("scripts/test-live-models-docker.sh", "utf8")).toContain( - 'OPENCLAW_LIVE_MODELS_DOCKER_RUN_TIMEOUT:-2100s', + "OPENCLAW_LIVE_MODELS_DOCKER_RUN_TIMEOUT:-2100s", ); expect(readFileSync("scripts/test-live-gateway-models-docker.sh", "utf8")).toContain( - 'OPENCLAW_LIVE_GATEWAY_DOCKER_RUN_TIMEOUT:-2100s', + "OPENCLAW_LIVE_GATEWAY_DOCKER_RUN_TIMEOUT:-2100s", ); expect(readFileSync("scripts/test-live-cli-backend-docker.sh", "utf8")).toContain( - 'OPENCLAW_LIVE_CLI_BACKEND_DOCKER_RUN_TIMEOUT:-2700s', + "OPENCLAW_LIVE_CLI_BACKEND_DOCKER_RUN_TIMEOUT:-2700s", ); expect(readFileSync("scripts/test-live-cli-backend-docker.sh", "utf8")).toContain( 'timeout --kill-after=30s "${OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS:-180}s"', ); expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain( - 'OPENCLAW_LIVE_ACP_BIND_DOCKER_RUN_TIMEOUT:-2700s', + "OPENCLAW_LIVE_ACP_BIND_DOCKER_RUN_TIMEOUT:-2700s", ); expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain( - 'OPENCLAW_LIVE_ACP_BIND_SETUP_TIMEOUT_SECONDS:-180', + "OPENCLAW_LIVE_ACP_BIND_SETUP_TIMEOUT_SECONDS:-180", ); expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain( 'timeout --kill-after=30s "${OPENCLAW_LIVE_ACP_BIND_SETUP_TIMEOUT_SECONDS:-180}s"', @@ -708,19 +708,19 @@ describe("package artifact reuse", () => { "run_setup_command bash -lc 'curl -fsSL https://app.factory.ai/cli | sh'", ); expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain( - 'OPENCLAW_LIVE_CODEX_HARNESS_DOCKER_RUN_TIMEOUT:-2100s', + "OPENCLAW_LIVE_CODEX_HARNESS_DOCKER_RUN_TIMEOUT:-2100s", ); expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain( - 'OPENCLAW_LIVE_CODEX_HARNESS_SETUP_TIMEOUT_SECONDS:-180', + "OPENCLAW_LIVE_CODEX_HARNESS_SETUP_TIMEOUT_SECONDS:-180", ); expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain( 'timeout --kill-after=30s "${OPENCLAW_LIVE_CODEX_HARNESS_SETUP_TIMEOUT_SECONDS:-180}s"', ); expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain( - "run_setup_command npm install -g @openai/codex", + 'run_setup_command npm install -g "$OPENCLAW_LIVE_CODEX_CLI_PACKAGE_SPEC"', ); expect(readFileSync("scripts/test-live-subagent-announce-docker.sh", "utf8")).toContain( - 'OPENCLAW_LIVE_SUBAGENT_DOCKER_RUN_TIMEOUT:-1200s', + "OPENCLAW_LIVE_SUBAGENT_DOCKER_RUN_TIMEOUT:-1200s", ); expect(build).toContain('ROOT_DIR="${OPENCLAW_LIVE_DOCKER_REPO_ROOT:-$SCRIPT_ROOT_DIR}"'); expect(build).toContain('source "$SCRIPT_ROOT_DIR/scripts/lib/docker-build.sh"'); @@ -1029,7 +1029,7 @@ describe("package artifact reuse", () => { 'check_child "release_checks" "$RELEASE_CHECKS_RUN_ID" 1 1', "gh run cancel", "NORMAL_CI_RESULT: ${{ needs.normal_ci.result }}", - 'Sorry. Your account was suspended', + "Sorry. Your account was suspended", 'gh_with_retry run view "$run_id" --json status,conclusion,url,attempt,headSha,jobs', ]); expect(workflow).not.toContain("force-cancel"); diff --git a/test/scripts/test-live-codex-harness-docker.test.ts b/test/scripts/test-live-codex-harness-docker.test.ts index 2db7e6679ddd..82bd8cca1fb9 100644 --- a/test/scripts/test-live-codex-harness-docker.test.ts +++ b/test/scripts/test-live-codex-harness-docker.test.ts @@ -90,6 +90,18 @@ describe("scripts/test-live-codex-harness-docker.sh", () => { ); }); + it("installs the plugin-pinned Codex CLI package for app-server proof", () => { + const script = fs.readFileSync(SCRIPT_PATH, "utf8"); + + expect(script).toContain('"$ROOT_DIR/extensions/codex/package.json"'); + expect(script).toContain("process.stdout.write(`@openai/codex@${version}`);"); + expect(script).toContain('-e OPENCLAW_LIVE_CODEX_CLI_PACKAGE_SPEC="$CODEX_CLI_PACKAGE_SPEC"'); + expect(script).toContain( + 'run_setup_command npm install -g "$OPENCLAW_LIVE_CODEX_CLI_PACKAGE_SPEC"', + ); + expect(script).not.toContain("run_setup_command npm install -g @openai/codex"); + }); + it("fails instead of skipping when Codex auth cannot identify an account", () => { const script = fs.readFileSync(SCRIPT_PATH, "utf8");