fix(codex): bridge cli api-key auth into app-server

This commit is contained in:
Vincent Koc
2026-05-26 16:08:32 +02:00
parent 0f605ee003
commit 5ef812293b
10 changed files with 270 additions and 31 deletions
@@ -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<void> {
);
}
async function writeCodexCliApiKeyAuthFile(codexHome: string): Promise<void> {
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) => {
+83 -3
View File
@@ -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<typeof resolveAuthProfileOrder>[0]["cfg"];
@@ -228,6 +232,20 @@ export function resolveCodexAppServerEnvApiKeyCacheKey(params: {
return `${apiKey.key}:sha256:${hash.digest("hex")}`;
}
export function resolveCodexAppServerFallbackApiKeyCacheKey(params: {
startOptions: Pick<CodexAppServerStartOptions, "transport" | "env" | "clearEnv">;
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<CodexLoginAccountParams | undefined> {
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<string, unknown>).OPENAI_API_KEY;
return typeof apiKey === "string" && apiKey.trim() ? apiKey.trim() : undefined;
}
async function readCodexCliAuthFileApiKey(env: NodeJS.ProcessEnv): Promise<string | undefined> {
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,
+6 -1
View File
@@ -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,
});
}
@@ -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,
}
: {}),
}),
@@ -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();
@@ -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);
+2 -2
View File
@@ -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({
+15 -3
View File
@@ -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" \
@@ -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");
@@ -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");