mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 23:10:29 +00:00
fix: support Codex CLI QA auth
This commit is contained in:
@@ -68,6 +68,38 @@ describe("buildQaRuntimeEnv", () => {
|
||||
expect(env.OPENAI_API_KEY).toBe("openai-explicit");
|
||||
});
|
||||
|
||||
it("preserves Codex CLI auth home for live frontier runs while sandboxing OpenClaw home", async () => {
|
||||
const hostHome = await mkdtemp(path.join(os.tmpdir(), "qa-host-home-"));
|
||||
cleanups.push(async () => {
|
||||
await rm(hostHome, { recursive: true, force: true });
|
||||
});
|
||||
const codexHome = path.join(hostHome, ".codex");
|
||||
await mkdir(codexHome);
|
||||
|
||||
const env = buildQaRuntimeEnv({
|
||||
...createParams({
|
||||
HOME: hostHome,
|
||||
}),
|
||||
providerMode: "live-frontier",
|
||||
});
|
||||
|
||||
expect(env.HOME).toBe("/tmp/openclaw-qa/home");
|
||||
expect(env.OPENCLAW_HOME).toBe("/tmp/openclaw-qa/home");
|
||||
expect(env.CODEX_HOME).toBe(codexHome);
|
||||
});
|
||||
|
||||
it("keeps explicit Codex CLI auth home for live frontier runs", () => {
|
||||
const env = buildQaRuntimeEnv({
|
||||
...createParams({
|
||||
CODEX_HOME: "/custom/codex-home",
|
||||
HOME: "/host/home",
|
||||
}),
|
||||
providerMode: "live-frontier",
|
||||
});
|
||||
|
||||
expect(env.CODEX_HOME).toBe("/custom/codex-home");
|
||||
});
|
||||
|
||||
it("scrubs direct and live provider keys in mock mode", () => {
|
||||
const env = buildQaRuntimeEnv({
|
||||
...createParams({
|
||||
@@ -78,6 +110,7 @@ describe("buildQaRuntimeEnv", () => {
|
||||
GOOGLE_API_KEY: "google-live",
|
||||
OPENAI_API_KEY: "openai-live",
|
||||
OPENAI_API_KEYS: "openai-a,openai-b",
|
||||
CODEX_HOME: "/host/.codex",
|
||||
OPENCLAW_LIVE_ANTHROPIC_KEY: "anthropic-live",
|
||||
OPENCLAW_LIVE_ANTHROPIC_KEYS: "anthropic-a,anthropic-b",
|
||||
OPENCLAW_LIVE_GEMINI_KEY: "gemini-live",
|
||||
@@ -88,6 +121,7 @@ describe("buildQaRuntimeEnv", () => {
|
||||
|
||||
expect(env.OPENAI_API_KEY).toBeUndefined();
|
||||
expect(env.OPENAI_API_KEYS).toBeUndefined();
|
||||
expect(env.CODEX_HOME).toBeUndefined();
|
||||
expect(env.ANTHROPIC_API_KEY).toBeUndefined();
|
||||
expect(env.ANTHROPIC_OAUTH_TOKEN).toBeUndefined();
|
||||
expect(env.GEMINI_API_KEY).toBeUndefined();
|
||||
@@ -214,6 +248,30 @@ describe("qa bundled plugin dir", () => {
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
it("maps cli backend provider ids to their owning bundled plugin ids", async () => {
|
||||
const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-plugin-owner-"));
|
||||
cleanups.push(async () => {
|
||||
await rm(repoRoot, { recursive: true, force: true });
|
||||
});
|
||||
await mkdir(path.join(repoRoot, "dist", "extensions", "openai"), { recursive: true });
|
||||
await writeFile(
|
||||
path.join(repoRoot, "dist", "extensions", "openai", "openclaw.plugin.json"),
|
||||
JSON.stringify({
|
||||
id: "openai",
|
||||
providers: ["openai", "openai-codex"],
|
||||
cliBackends: ["codex-cli"],
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await expect(
|
||||
__testing.resolveQaOwnerPluginIdsForProviderIds({
|
||||
repoRoot,
|
||||
providerIds: ["codex-cli"],
|
||||
}),
|
||||
).resolves.toEqual(["openai"]);
|
||||
});
|
||||
|
||||
it("raises the QA runtime host version to the highest allowed plugin floor", async () => {
|
||||
const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-runtime-version-"));
|
||||
cleanups.push(async () => {
|
||||
|
||||
@@ -8,8 +8,9 @@ import path from "node:path";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { startQaGatewayRpcClient } from "./gateway-rpc-client.js";
|
||||
import { splitQaModelRef } from "./model-selection.js";
|
||||
import { seedQaAgentWorkspace } from "./qa-agent-workspace.js";
|
||||
import { buildQaGatewayConfig } from "./qa-gateway-config.js";
|
||||
import { buildQaGatewayConfig, type QaThinkingLevel } from "./qa-gateway-config.js";
|
||||
|
||||
const QA_LIVE_ENV_ALIASES = Object.freeze([
|
||||
{
|
||||
@@ -41,6 +42,7 @@ const QA_MOCK_BLOCKED_ENV_VARS = Object.freeze([
|
||||
"OPENAI_API_KEY",
|
||||
"OPENAI_API_KEYS",
|
||||
"OPENAI_BASE_URL",
|
||||
"CODEX_HOME",
|
||||
"OPENCLAW_LIVE_ANTHROPIC_KEY",
|
||||
"OPENCLAW_LIVE_ANTHROPIC_KEYS",
|
||||
"OPENCLAW_LIVE_GEMINI_KEY",
|
||||
@@ -106,6 +108,19 @@ export function normalizeQaProviderModeEnv(
|
||||
return env;
|
||||
}
|
||||
|
||||
function resolveQaLiveCliAuthEnv(baseEnv: NodeJS.ProcessEnv) {
|
||||
const configuredCodexHome = baseEnv.CODEX_HOME?.trim();
|
||||
if (configuredCodexHome) {
|
||||
return { CODEX_HOME: configuredCodexHome };
|
||||
}
|
||||
const hostHome = baseEnv.HOME?.trim();
|
||||
if (!hostHome) {
|
||||
return {};
|
||||
}
|
||||
const codexHome = path.join(hostHome, ".codex");
|
||||
return existsSync(codexHome) ? { CODEX_HOME: codexHome } : {};
|
||||
}
|
||||
|
||||
export function buildQaRuntimeEnv(params: {
|
||||
configPath: string;
|
||||
gatewayToken: string;
|
||||
@@ -119,9 +134,11 @@ export function buildQaRuntimeEnv(params: {
|
||||
providerMode?: "mock-openai" | "live-frontier";
|
||||
baseEnv?: NodeJS.ProcessEnv;
|
||||
}) {
|
||||
const baseEnv = params.baseEnv ?? process.env;
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...(params.baseEnv ?? process.env),
|
||||
...baseEnv,
|
||||
HOME: params.homeDir,
|
||||
...(params.providerMode === "live-frontier" ? resolveQaLiveCliAuthEnv(baseEnv) : {}),
|
||||
OPENCLAW_HOME: params.homeDir,
|
||||
OPENCLAW_CONFIG_PATH: params.configPath,
|
||||
OPENCLAW_STATE_DIR: params.stateDir,
|
||||
@@ -161,6 +178,8 @@ function isRetryableGatewayCallError(details: string): boolean {
|
||||
export const __testing = {
|
||||
buildQaRuntimeEnv,
|
||||
isRetryableGatewayCallError,
|
||||
resolveQaLiveCliAuthEnv,
|
||||
resolveQaOwnerPluginIdsForProviderIds,
|
||||
resolveQaBundledPluginsSourceRoot,
|
||||
resolveQaRuntimeHostVersion,
|
||||
createQaBundledPluginsDir,
|
||||
@@ -180,6 +199,57 @@ function resolveQaBundledPluginsSourceRoot(repoRoot: string) {
|
||||
throw new Error("failed to resolve qa bundled plugins source root");
|
||||
}
|
||||
|
||||
async function resolveQaOwnerPluginIdsForProviderIds(params: {
|
||||
repoRoot: string;
|
||||
providerIds: readonly string[];
|
||||
}) {
|
||||
const providerIds = [
|
||||
...new Set(params.providerIds.map((providerId) => providerId.trim())),
|
||||
].filter((providerId) => providerId.length > 0);
|
||||
if (providerIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const remainingProviderIds = new Set(providerIds);
|
||||
const ownerPluginIds = new Set<string>();
|
||||
const sourceRoot = resolveQaBundledPluginsSourceRoot(params.repoRoot);
|
||||
for (const entry of await fs.readdir(sourceRoot, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const manifestPath = path.join(sourceRoot, entry.name, "openclaw.plugin.json");
|
||||
if (!existsSync(manifestPath)) {
|
||||
continue;
|
||||
}
|
||||
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")) as {
|
||||
id?: unknown;
|
||||
providers?: unknown;
|
||||
cliBackends?: unknown;
|
||||
};
|
||||
const pluginId = typeof manifest.id === "string" ? manifest.id.trim() : entry.name;
|
||||
if (!pluginId) {
|
||||
continue;
|
||||
}
|
||||
const ownedIds = new Set(
|
||||
[
|
||||
pluginId,
|
||||
...(Array.isArray(manifest.providers) ? manifest.providers : []),
|
||||
...(Array.isArray(manifest.cliBackends) ? manifest.cliBackends : []),
|
||||
].filter((ownedId): ownedId is string => typeof ownedId === "string"),
|
||||
);
|
||||
for (const providerId of providerIds) {
|
||||
if (!ownedIds.has(providerId)) {
|
||||
continue;
|
||||
}
|
||||
ownerPluginIds.add(pluginId);
|
||||
remainingProviderIds.delete(providerId);
|
||||
}
|
||||
}
|
||||
for (const providerId of remainingProviderIds) {
|
||||
ownerPluginIds.add(providerId);
|
||||
}
|
||||
return [...ownerPluginIds];
|
||||
}
|
||||
|
||||
function parseStableSemverFloor(value: string | undefined) {
|
||||
if (!value) {
|
||||
return null;
|
||||
@@ -371,6 +441,7 @@ export async function startQaGatewayChild(params: {
|
||||
primaryModel?: string;
|
||||
alternateModel?: string;
|
||||
fastMode?: boolean;
|
||||
thinkingDefault?: QaThinkingLevel;
|
||||
controlUiEnabled?: boolean;
|
||||
}) {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-qa-suite-"));
|
||||
@@ -396,6 +467,21 @@ export async function startQaGatewayChild(params: {
|
||||
fs.mkdir(xdgDataHome, { recursive: true }),
|
||||
fs.mkdir(xdgCacheHome, { recursive: true }),
|
||||
]);
|
||||
const liveProviderIds =
|
||||
params.providerMode === "live-frontier"
|
||||
? [params.primaryModel, params.alternateModel]
|
||||
.map((modelRef) =>
|
||||
typeof modelRef === "string" ? splitQaModelRef(modelRef)?.provider : undefined,
|
||||
)
|
||||
.filter((providerId): providerId is string => Boolean(providerId))
|
||||
: [];
|
||||
const enabledPluginIds =
|
||||
liveProviderIds.length > 0
|
||||
? await resolveQaOwnerPluginIdsForProviderIds({
|
||||
repoRoot: params.repoRoot,
|
||||
providerIds: liveProviderIds,
|
||||
})
|
||||
: undefined;
|
||||
const cfg = buildQaGatewayConfig({
|
||||
bind: "loopback",
|
||||
gatewayPort,
|
||||
@@ -411,7 +497,9 @@ export async function startQaGatewayChild(params: {
|
||||
providerMode: params.providerMode,
|
||||
primaryModel: params.primaryModel,
|
||||
alternateModel: params.alternateModel,
|
||||
enabledPluginIds,
|
||||
fastMode: params.fastMode,
|
||||
thinkingDefault: params.thinkingDefault,
|
||||
controlUiEnabled: params.controlUiEnabled,
|
||||
});
|
||||
await fs.writeFile(configPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
|
||||
|
||||
@@ -5,6 +5,7 @@ import { startQaGatewayChild } from "./gateway-child.js";
|
||||
import { startQaLabServer } from "./lab-server.js";
|
||||
import { resolveQaLiveTurnTimeoutMs } from "./live-timeout.js";
|
||||
import { startQaMockOpenAiServer } from "./mock-openai-server.js";
|
||||
import type { QaThinkingLevel } from "./qa-gateway-config.js";
|
||||
|
||||
type QaManualLaneParams = {
|
||||
repoRoot: string;
|
||||
@@ -12,6 +13,7 @@ type QaManualLaneParams = {
|
||||
primaryModel: string;
|
||||
alternateModel: string;
|
||||
fastMode?: boolean;
|
||||
thinkingDefault?: QaThinkingLevel;
|
||||
message: string;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
@@ -61,6 +63,7 @@ export async function runQaManualLane(params: QaManualLaneParams) {
|
||||
primaryModel: params.primaryModel,
|
||||
alternateModel: params.alternateModel,
|
||||
fastMode: params.fastMode,
|
||||
thinkingDefault: params.thinkingDefault,
|
||||
controlUiEnabled: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -78,6 +78,42 @@ describe("buildQaGatewayConfig", () => {
|
||||
expect(cfg.agents?.defaults).not.toHaveProperty("imageGenerationModel");
|
||||
});
|
||||
|
||||
it("uses owning plugin ids separately from live model provider ids", () => {
|
||||
const cfg = buildQaGatewayConfig({
|
||||
bind: "loopback",
|
||||
gatewayPort: 18789,
|
||||
gatewayToken: "token",
|
||||
qaBusBaseUrl: "http://127.0.0.1:43124",
|
||||
workspaceDir: "/tmp/qa-workspace",
|
||||
providerMode: "live-frontier",
|
||||
primaryModel: "codex-cli/test-model",
|
||||
alternateModel: "codex-cli/test-model",
|
||||
imageGenerationModel: null,
|
||||
enabledPluginIds: ["openai"],
|
||||
});
|
||||
|
||||
expect(getPrimaryModel(cfg.agents?.defaults?.model)).toBe("codex-cli/test-model");
|
||||
expect(cfg.plugins?.allow).toEqual(["memory-core", "openai", "qa-channel"]);
|
||||
expect(cfg.plugins?.entries?.openai).toEqual({ enabled: true });
|
||||
expect(cfg.plugins?.entries?.["codex-cli"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("can set a QA default thinking level for judge turns", () => {
|
||||
const cfg = buildQaGatewayConfig({
|
||||
bind: "loopback",
|
||||
gatewayPort: 18789,
|
||||
gatewayToken: "token",
|
||||
qaBusBaseUrl: "http://127.0.0.1:43124",
|
||||
workspaceDir: "/tmp/qa-workspace",
|
||||
providerMode: "live-frontier",
|
||||
primaryModel: "openai/gpt-5.4",
|
||||
alternateModel: "openai/gpt-5.4",
|
||||
thinkingDefault: "xhigh",
|
||||
});
|
||||
|
||||
expect(cfg.agents?.defaults?.thinkingDefault).toBe("xhigh");
|
||||
});
|
||||
|
||||
it("can disable control ui for suite-only gateway children", () => {
|
||||
const cfg = buildQaGatewayConfig({
|
||||
bind: "loopback",
|
||||
|
||||
@@ -15,6 +15,8 @@ export const DEFAULT_QA_CONTROL_UI_ALLOWED_ORIGINS = Object.freeze([
|
||||
"http://localhost:43124",
|
||||
]);
|
||||
|
||||
export type QaThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive";
|
||||
|
||||
export function mergeQaControlUiAllowedOrigins(extraOrigins?: string[]) {
|
||||
const normalizedExtra = (extraOrigins ?? [])
|
||||
.map((origin) => origin.trim())
|
||||
@@ -37,7 +39,9 @@ export function buildQaGatewayConfig(params: {
|
||||
alternateModel?: string;
|
||||
imageGenerationModel?: string | null;
|
||||
enabledProviderIds?: string[];
|
||||
enabledPluginIds?: string[];
|
||||
fastMode?: boolean;
|
||||
thinkingDefault?: QaThinkingLevel;
|
||||
}): OpenClawConfig {
|
||||
const mockProviderBaseUrl = params.providerBaseUrl ?? "http://127.0.0.1:44080/v1";
|
||||
const mockOpenAiProvider: ModelProviderConfig = {
|
||||
@@ -119,13 +123,23 @@ export function buildQaGatewayConfig(params: {
|
||||
),
|
||||
]
|
||||
: [];
|
||||
const selectedPluginIds =
|
||||
providerMode === "live-frontier"
|
||||
? [
|
||||
...new Set(
|
||||
(params.enabledPluginIds?.length ?? 0) > 0
|
||||
? params.enabledPluginIds
|
||||
: selectedProviderIds,
|
||||
),
|
||||
]
|
||||
: [];
|
||||
const pluginEntries =
|
||||
providerMode === "live-frontier"
|
||||
? Object.fromEntries(selectedProviderIds.map((providerId) => [providerId, { enabled: true }]))
|
||||
? Object.fromEntries(selectedPluginIds.map((pluginId) => [pluginId, { enabled: true }]))
|
||||
: {};
|
||||
const allowedPlugins =
|
||||
providerMode === "live-frontier"
|
||||
? ["memory-core", ...selectedProviderIds, "qa-channel"]
|
||||
? ["memory-core", ...selectedPluginIds, "qa-channel"]
|
||||
: ["memory-core", "qa-channel"];
|
||||
const liveModelParams =
|
||||
providerMode === "live-frontier"
|
||||
@@ -166,6 +180,7 @@ export function buildQaGatewayConfig(params: {
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(params.thinkingDefault ? { thinkingDefault: params.thinkingDefault } : {}),
|
||||
memorySearch: {
|
||||
sync: {
|
||||
watch: true,
|
||||
|
||||
@@ -1149,6 +1149,7 @@ export async function runQaSuite(params?: {
|
||||
providerMode,
|
||||
primaryModel,
|
||||
alternateModel,
|
||||
fastMode,
|
||||
controlUiEnabled: true,
|
||||
});
|
||||
lab.setControlUi({
|
||||
|
||||
Reference in New Issue
Block a user