mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:50:44 +00:00
fix(acpx): remove codex auth file fallback
This commit is contained in:
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Providers/OpenAI: stop advertising the removed `gpt-5.3-codex-spark` Codex model through fallback catalogs, and suppress stale rows with a GPT-5.5 recovery hint.
|
||||
- Plugins/QR: replace legacy `qrcode-terminal` QR rendering with bounded `qrcode-tui` helpers for plugin login/setup flows. (#65969) Thanks @vincentkoc.
|
||||
- ACPX/Codex: stop the embedded Codex ACP auth bridge from falling back to raw `~/.codex` file copies; ACPX now only uses OpenClaw's canonical Codex OAuth bridge.
|
||||
- Auto-reply/system events: route async exec-event completion replies through the persisted session delivery context, so long-running command results return to the originating channel instead of being dropped when live origin metadata is missing. (#70258) Thanks @wzfukui.
|
||||
- OpenAI/image generation: send reference-image edits as guarded multipart uploads instead of JSON data URLs, restoring complex multi-reference `gpt-image-2` edits. Fixes #70642. Thanks @dashhuang.
|
||||
- QA channel/security: reject non-HTTP(S) inbound attachment URLs before media fetch, and log rejected schemes so suspicious or misconfigured payloads are visible during debugging. (#70708) Thanks @vincentkoc.
|
||||
|
||||
@@ -641,6 +641,7 @@ Notes:
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENTS=claude,codex,gemini`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND='npx -y @agentclientprotocol/claude-agent-acp@<version>'`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_CODEX_MODEL=gpt-5.5`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_PARENT_MODEL=openai/gpt-5.4`
|
||||
- Notes:
|
||||
- This lane uses the gateway `chat.send` surface with admin-only synthetic originating-route fields so tests can attach message-channel context without pretending to deliver externally.
|
||||
- When `OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND` is unset, the test uses the embedded `acpx` plugin's built-in agent registry for the selected ACP harness agent.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { saveAuthProfileStore } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { prepareAcpxCodexAuthConfig } from "./codex-auth-bridge.js";
|
||||
import { resolveAcpxPluginConfig } from "./config.js";
|
||||
@@ -41,18 +42,28 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe("prepareAcpxCodexAuthConfig", () => {
|
||||
it("wraps built-in Codex ACP with an isolated CODEX_HOME copy", async () => {
|
||||
it("wraps built-in Codex ACP with an isolated CODEX_HOME from canonical OpenClaw OAuth", async () => {
|
||||
const root = await makeTempDir();
|
||||
const sourceCodexHome = path.join(root, "source-codex");
|
||||
const agentDir = path.join(root, "agent");
|
||||
const stateDir = path.join(root, "state");
|
||||
await fs.mkdir(sourceCodexHome, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(sourceCodexHome, "auth.json"),
|
||||
`${JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: "test-api-key" }, null, 2)}\n`,
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai-codex:default": {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
accountId: "acct-123",
|
||||
idToken: "id-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
{ filterExternalAuthProfiles: false },
|
||||
);
|
||||
await fs.writeFile(path.join(sourceCodexHome, "config.toml"), 'model = "gpt-5.4"\n');
|
||||
process.env.CODEX_HOME = sourceCodexHome;
|
||||
process.env.OPENCLAW_AGENT_DIR = agentDir;
|
||||
delete process.env.PI_CODING_AGENT_DIR;
|
||||
|
||||
@@ -69,21 +80,65 @@ describe("prepareAcpxCodexAuthConfig", () => {
|
||||
expect(wrapperPath).toBe(path.join(stateDir, "acpx", "codex-acp-wrapper.mjs"));
|
||||
await expect(fs.access(wrapperPath)).resolves.toBeUndefined();
|
||||
|
||||
const isolatedAuthPath = path.join(agentDir, "acp-auth", "codex-source", "auth.json");
|
||||
const bridgeRoot = path.join(agentDir, "acp-auth", "codex");
|
||||
const bridgeDirs = await fs.readdir(bridgeRoot);
|
||||
expect(bridgeDirs).toHaveLength(1);
|
||||
const bridgeDir = bridgeDirs[0];
|
||||
if (!bridgeDir) {
|
||||
throw new Error("expected one Codex auth bridge directory");
|
||||
}
|
||||
const isolatedAuthPath = path.join(bridgeRoot, bridgeDir, "auth.json");
|
||||
const copiedAuth = JSON.parse(await fs.readFile(isolatedAuthPath, "utf8")) as {
|
||||
auth_mode?: string;
|
||||
OPENAI_API_KEY?: string;
|
||||
tokens?: Record<string, unknown>;
|
||||
};
|
||||
expect(copiedAuth).toEqual({ auth_mode: "apikey", OPENAI_API_KEY: "test-api-key" });
|
||||
expect(copiedAuth).toEqual({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
id_token: "id-token",
|
||||
access_token: "access-token",
|
||||
refresh_token: "refresh-token",
|
||||
account_id: "acct-123",
|
||||
},
|
||||
last_refresh: expect.any(String),
|
||||
});
|
||||
expect((await fs.stat(isolatedAuthPath)).mode & 0o777).toBe(0o600);
|
||||
await expect(
|
||||
fs.readFile(path.join(agentDir, "acp-auth", "codex-source", "config.toml"), "utf8"),
|
||||
).resolves.toBe('model = "gpt-5.4"\n');
|
||||
fs.access(path.join(agentDir, "acp-auth", "codex-source", "auth.json")),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
|
||||
const wrapper = await fs.readFile(wrapperPath, "utf8");
|
||||
expect(wrapper).toContain(`CODEX_HOME: ${JSON.stringify(path.dirname(isolatedAuthPath))}`);
|
||||
expect(wrapper).toContain("for (const key of [])");
|
||||
expect(wrapper).not.toContain("test-api-key");
|
||||
expect(wrapper).not.toContain("access-token");
|
||||
});
|
||||
|
||||
it("does not copy source Codex auth when canonical OpenClaw OAuth is unavailable", async () => {
|
||||
const root = await makeTempDir();
|
||||
const sourceCodexHome = path.join(root, "source-codex");
|
||||
const agentDir = path.join(root, "agent");
|
||||
await fs.mkdir(sourceCodexHome, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(sourceCodexHome, "auth.json"),
|
||||
`${JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: "test-api-key" }, null, 2)}\n`,
|
||||
);
|
||||
process.env.CODEX_HOME = sourceCodexHome;
|
||||
process.env.OPENCLAW_AGENT_DIR = agentDir;
|
||||
delete process.env.PI_CODING_AGENT_DIR;
|
||||
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
const resolved = await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir: path.join(root, "state"),
|
||||
});
|
||||
|
||||
expect(resolved.agents.codex).toBeUndefined();
|
||||
await expect(
|
||||
fs.access(path.join(agentDir, "acp-auth", "codex-source", "auth.json")),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("does not override an explicitly configured Codex agent command", async () => {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { resolveOpenClawAgentDir } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { prepareCodexAuthBridge } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import { writePrivateSecretFileAtomic } from "openclaw/plugin-sdk/secret-file-runtime";
|
||||
import type { PluginLogger } from "../runtime-api.js";
|
||||
import type { ResolvedAcpxPluginConfig } from "./config.js";
|
||||
|
||||
@@ -13,67 +11,6 @@ const DEFAULT_CODEX_AUTH_PROFILE_ID = "openai-codex:default";
|
||||
// launches. Keep those env vars visible to the child so its auth method matches.
|
||||
const CODEX_AUTH_ENV_CLEAR_KEYS: string[] = [];
|
||||
|
||||
type PreparedAcpxCodexAuth = {
|
||||
codexHome: string;
|
||||
clearEnv: string[];
|
||||
};
|
||||
|
||||
function resolveSourceCodexHome(env: NodeJS.ProcessEnv = process.env): string {
|
||||
const configured = env.CODEX_HOME?.trim();
|
||||
if (configured) {
|
||||
if (configured === "~") {
|
||||
return os.homedir();
|
||||
}
|
||||
if (configured.startsWith("~/")) {
|
||||
return path.join(os.homedir(), configured.slice(2));
|
||||
}
|
||||
return path.resolve(configured);
|
||||
}
|
||||
return path.join(os.homedir(), ".codex");
|
||||
}
|
||||
|
||||
async function readOptionalFile(filePath: string): Promise<string | undefined> {
|
||||
try {
|
||||
return await fs.readFile(filePath, "utf8");
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
|
||||
return undefined;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareCopiedCodexHome(params: {
|
||||
agentDir: string;
|
||||
sourceCodexHome: string;
|
||||
}): Promise<PreparedAcpxCodexAuth | null> {
|
||||
const authJson = await readOptionalFile(path.join(params.sourceCodexHome, "auth.json"));
|
||||
if (!authJson) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const codexHome = path.join(params.agentDir, "acp-auth", "codex-source");
|
||||
await writePrivateSecretFileAtomic({
|
||||
rootDir: params.agentDir,
|
||||
filePath: path.join(codexHome, "auth.json"),
|
||||
content: authJson,
|
||||
});
|
||||
|
||||
const configToml = await readOptionalFile(path.join(params.sourceCodexHome, "config.toml"));
|
||||
if (configToml) {
|
||||
await writePrivateSecretFileAtomic({
|
||||
rootDir: params.agentDir,
|
||||
filePath: path.join(codexHome, "config.toml"),
|
||||
content: configToml,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
codexHome,
|
||||
clearEnv: [...CODEX_AUTH_ENV_CLEAR_KEYS],
|
||||
};
|
||||
}
|
||||
|
||||
function shellArg(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
@@ -125,28 +62,21 @@ export async function prepareAcpxCodexAuthConfig(params: {
|
||||
}
|
||||
|
||||
const agentDir = resolveOpenClawAgentDir();
|
||||
const sourceCodexHome = resolveSourceCodexHome();
|
||||
const bridge =
|
||||
(await prepareCodexAuthBridge({
|
||||
agentDir,
|
||||
bridgeDir: "acp-auth",
|
||||
profileId: DEFAULT_CODEX_AUTH_PROFILE_ID,
|
||||
sourceCodexHome,
|
||||
})) ??
|
||||
(await prepareCopiedCodexHome({
|
||||
agentDir,
|
||||
sourceCodexHome,
|
||||
}));
|
||||
const bridge = await prepareCodexAuthBridge({
|
||||
agentDir,
|
||||
bridgeDir: "acp-auth",
|
||||
profileId: DEFAULT_CODEX_AUTH_PROFILE_ID,
|
||||
});
|
||||
|
||||
if (!bridge) {
|
||||
params.logger?.debug?.("codex ACP auth bridge skipped: no Codex auth source found");
|
||||
params.logger?.debug?.("codex ACP auth bridge skipped: no canonical OpenClaw OAuth found");
|
||||
return params.pluginConfig;
|
||||
}
|
||||
|
||||
const wrapperCommand = await writeCodexAcpWrapper({
|
||||
wrapperPath: path.join(params.stateDir, "acpx", "codex-acp-wrapper.mjs"),
|
||||
codexHome: bridge.codexHome,
|
||||
clearEnv: bridge.clearEnv,
|
||||
clearEnv: [...CODEX_AUTH_ENV_CLEAR_KEYS],
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -37,6 +37,7 @@ const describeLive = LIVE && ACP_BIND_LIVE ? describe : describe.skip;
|
||||
const CONNECT_TIMEOUT_MS = 90_000;
|
||||
const LIVE_TIMEOUT_MS = 240_000;
|
||||
const DEFAULT_LIVE_CODEX_MODEL = "gpt-5.5";
|
||||
const DEFAULT_LIVE_PARENT_MODEL = "openai/gpt-5.5";
|
||||
type LiveAcpAgent = "claude" | "codex" | "gemini";
|
||||
|
||||
function createSlackCurrentConversationBindingRegistry() {
|
||||
@@ -135,6 +136,28 @@ function logLiveStep(message: string): void {
|
||||
console.info(`[live-acp-bind] ${message}`);
|
||||
}
|
||||
|
||||
function normalizeOpenAiModelRef(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return DEFAULT_LIVE_PARENT_MODEL;
|
||||
}
|
||||
return trimmed.includes("/") ? trimmed : `openai/${trimmed}`;
|
||||
}
|
||||
|
||||
function resolveLiveParentModel(): string {
|
||||
return normalizeOpenAiModelRef(
|
||||
process.env.OPENCLAW_LIVE_ACP_BIND_PARENT_MODEL?.trim() ||
|
||||
process.env.OPENCLAW_LIVE_ACP_BIND_CODEX_MODEL?.trim() ||
|
||||
DEFAULT_LIVE_PARENT_MODEL,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveModelObject(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
async function prepareCodexHomeForLiveBindTest(): Promise<void> {
|
||||
const home = process.env.HOME?.trim();
|
||||
if (!home) {
|
||||
@@ -449,6 +472,7 @@ describeLive("gateway live (ACP bind)", () => {
|
||||
const tempConfigPath = path.join(tempRoot, "openclaw.json");
|
||||
const port = await getFreeGatewayPort();
|
||||
const token = `test-${randomUUID()}`;
|
||||
const parentModel = resolveLiveParentModel();
|
||||
const originalSessionKey = "main";
|
||||
const slackUserId = `U${randomUUID().replace(/-/g, "").slice(0, 10).toUpperCase()}`;
|
||||
const conversationId = `user:${slackUserId}`;
|
||||
@@ -480,6 +504,20 @@ describeLive("gateway live (ACP bind)", () => {
|
||||
: {};
|
||||
const nextCfg = {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...resolveModelObject(cfg.agents?.defaults?.model),
|
||||
primary: parentModel,
|
||||
},
|
||||
models: {
|
||||
...cfg.agents?.defaults?.models,
|
||||
[parentModel]: cfg.agents?.defaults?.models?.[parentModel] ?? {},
|
||||
},
|
||||
},
|
||||
},
|
||||
gateway: {
|
||||
...cfg.gateway,
|
||||
mode: "local",
|
||||
@@ -534,6 +572,7 @@ describeLive("gateway live (ACP bind)", () => {
|
||||
};
|
||||
await fs.writeFile(tempConfigPath, `${JSON.stringify(nextCfg, null, 2)}\n`);
|
||||
process.env.OPENCLAW_CONFIG_PATH = tempConfigPath;
|
||||
logLiveStep(`using parent live model ${parentModel}`);
|
||||
clearConfigCache();
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearPluginLoaderCache();
|
||||
|
||||
Reference in New Issue
Block a user