mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(codex): bridge cli api-key auth into app-server
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user