mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 20:46:57 +02:00
fix: keep codex oauth bridge extension-owned (#68284) (thanks @vincentkoc)
This commit is contained in:
@@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Exec approvals/display: escape raw control characters (including newline and carriage return) in the shared and macOS approval-prompt command sanitizers, so trailing command payloads no longer render on hidden extra lines in the approval UI. (#68198)
|
||||
- Telegram/streaming: fence same-session stale preview and finalization work after aborts so Telegram no longer replays an older reply or flushes a hidden short preview after the abort confirmation lands. (#68100) Thanks @rubencu.
|
||||
- OpenAI Codex/OAuth + Pi: keep imported Codex CLI OAuth bootstrap, Pi auth export, and runtime overlay handling aligned so Codex sessions survive refresh and health checks without leaking transient CLI state into saved auth files. Thanks @vincentkoc.
|
||||
- OpenAI Codex/OAuth: keep Codex-specific auth bridging inside the owning plugins, preserve canonical imported CLI profiles, and allow legacy identity-less main-store OAuth sessions to upgrade during refresh mirroring. (#68284) Thanks @vincentkoc.
|
||||
- Config/redact: add `browser.cdpUrl` and `browser.profiles.*.cdpUrl` to sensitive URL config paths so embedded credentials (query tokens and HTTP Basic auth) are properly redacted in `config.get` API responses and availability error messages. (#67679) Thanks @Ziy1-Tan.
|
||||
- Agents/TTS: report failed speech synthesis as a real tool error so unconfigured providers no longer feed successful TTS failure output back into agent loops. (#67980) Thanks @lawrence3699.
|
||||
- Gateway/wake: allow unknown properties on wake payloads so external senders like Paperclip can attach opaque metadata without failing schema validation. (#68355) Thanks @kagura-agent.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
3447b4257e1eeaf3388b2c148e0086c534ba43147004100e9baa7cb662835c79 plugin-sdk-api-baseline.json
|
||||
78590addd53ec7db1ef5a56eecc93c39303344ccfad8349a14e8f97480fe64b2 plugin-sdk-api-baseline.jsonl
|
||||
f2002dd0661ca25eee382e1ab14c4655cfa3f4d616eaf4ac98db307df8453687 plugin-sdk-api-baseline.json
|
||||
34ad0f68b93b49eed4bce4f5e56511e3c555917c0b8743d399534d9ec3e8a8e2 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -41,10 +41,12 @@ describe("bridgeCodexAppServerStartOptions", () => {
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
accountId: "acct-123",
|
||||
idToken: "id-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
{ filterExternalAuthProfiles: false },
|
||||
);
|
||||
|
||||
const result = await bridgeCodexAppServerStartOptions({
|
||||
@@ -73,6 +75,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
|
||||
expect(authFile).toEqual({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
id_token: "id-token",
|
||||
access_token: "access-token",
|
||||
refresh_token: "refresh-token",
|
||||
account_id: "acct-123",
|
||||
@@ -93,7 +96,9 @@ describe("bridgeCodexAppServerStartOptions", () => {
|
||||
args: ["app-server"],
|
||||
headers: { authorization: "Bearer dev-token" },
|
||||
};
|
||||
saveAuthProfileStore({ version: 1, profiles: {} }, agentDir);
|
||||
saveAuthProfileStore({ version: 1, profiles: {} }, agentDir, {
|
||||
filterExternalAuthProfiles: false,
|
||||
});
|
||||
|
||||
await expect(
|
||||
bridgeCodexAppServerStartOptions({
|
||||
@@ -121,6 +126,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
{ filterExternalAuthProfiles: false },
|
||||
);
|
||||
|
||||
const codexHome = resolveHashedCodexHome(agentDir, "openai-codex:default");
|
||||
|
||||
@@ -1,7 +1,54 @@
|
||||
import { prepareCodexAuthBridgeFromProfile } from "openclaw/plugin-sdk/codex-auth-bridge-runtime";
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import {
|
||||
ensureAuthProfileStoreForLocalUpdate,
|
||||
type OAuthCredential,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { writePrivateSecretFileAtomic } from "openclaw/plugin-sdk/secret-file-runtime";
|
||||
import type { CodexAppServerStartOptions } from "./config.js";
|
||||
|
||||
const DEFAULT_CODEX_AUTH_PROFILE_ID = "openai-codex:default";
|
||||
const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
|
||||
const CODEX_AUTH_ENV_CLEAR_KEYS = ["OPENAI_API_KEY"] as const;
|
||||
|
||||
function isCodexBridgeableOAuthCredential(value: unknown): value is OAuthCredential {
|
||||
return Boolean(
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"type" in value &&
|
||||
"provider" in value &&
|
||||
"access" in value &&
|
||||
"refresh" in value &&
|
||||
value.type === "oauth" &&
|
||||
value.provider === OPENAI_CODEX_PROVIDER_ID &&
|
||||
typeof value.access === "string" &&
|
||||
value.access.trim().length > 0 &&
|
||||
typeof value.refresh === "string" &&
|
||||
value.refresh.trim().length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveCodexBridgeHome(agentDir: string, profileId: string): string {
|
||||
const digest = crypto.createHash("sha256").update(profileId).digest("hex").slice(0, 16);
|
||||
return path.join(agentDir, "harness-auth", "codex", digest);
|
||||
}
|
||||
|
||||
function buildCodexAuthFile(credential: OAuthCredential): string {
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
...(credential.idToken ? { id_token: credential.idToken } : {}),
|
||||
access_token: credential.access,
|
||||
refresh_token: credential.refresh,
|
||||
...(credential.accountId ? { account_id: credential.accountId } : {}),
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
export async function bridgeCodexAppServerStartOptions(params: {
|
||||
startOptions: CodexAppServerStartOptions;
|
||||
@@ -9,21 +56,27 @@ export async function bridgeCodexAppServerStartOptions(params: {
|
||||
authProfileId?: string;
|
||||
}): Promise<CodexAppServerStartOptions> {
|
||||
const profileId = params.authProfileId?.trim() || DEFAULT_CODEX_AUTH_PROFILE_ID;
|
||||
const bridge = await prepareCodexAuthBridgeFromProfile({
|
||||
agentDir: params.agentDir,
|
||||
authProfileId: profileId,
|
||||
bridgeRoot: "harness-auth",
|
||||
});
|
||||
if (!bridge) {
|
||||
const store = ensureAuthProfileStoreForLocalUpdate(params.agentDir);
|
||||
const credential = store.profiles[profileId];
|
||||
if (!isCodexBridgeableOAuthCredential(credential)) {
|
||||
return params.startOptions;
|
||||
}
|
||||
|
||||
const codexHome = resolveCodexBridgeHome(params.agentDir, profileId);
|
||||
await writePrivateSecretFileAtomic({
|
||||
rootDir: params.agentDir,
|
||||
filePath: path.join(codexHome, "auth.json"),
|
||||
content: buildCodexAuthFile(credential),
|
||||
});
|
||||
|
||||
return {
|
||||
...params.startOptions,
|
||||
env: {
|
||||
...params.startOptions.env,
|
||||
CODEX_HOME: bridge.codexHome,
|
||||
CODEX_HOME: codexHome,
|
||||
},
|
||||
clearEnv: Array.from(new Set([...(params.startOptions.clearEnv ?? []), ...bridge.clearEnv])),
|
||||
clearEnv: Array.from(
|
||||
new Set([...(params.startOptions.clearEnv ?? []), ...CODEX_AUTH_ENV_CLEAR_KEYS]),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -532,7 +532,7 @@ describe("runCodexAppServerAttempt", () => {
|
||||
|
||||
const binding = await startOrResumeThread({
|
||||
client: {
|
||||
request: async (method) => {
|
||||
request: async (method: string) => {
|
||||
if (method === "thread/resume") {
|
||||
return { thread: { id: "thread-existing" }, modelProvider: "openai" };
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ describe("readOpenAICodexCliOAuthProfile", () => {
|
||||
JSON.stringify({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
id_token: "id-token",
|
||||
access_token: accessToken,
|
||||
refresh_token: "refresh-token",
|
||||
account_id: "acct_123",
|
||||
@@ -61,6 +62,7 @@ describe("readOpenAICodexCliOAuthProfile", () => {
|
||||
access: accessToken,
|
||||
refresh: "refresh-token",
|
||||
accountId: "acct_123",
|
||||
idToken: "id-token",
|
||||
email: "codex@example.com",
|
||||
},
|
||||
});
|
||||
@@ -172,6 +174,7 @@ describe("readOpenAICodexCliOAuthProfile", () => {
|
||||
JSON.stringify({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
id_token: "id-token",
|
||||
access_token: accessToken,
|
||||
refresh_token: "refresh-token",
|
||||
account_id: "acct_123",
|
||||
@@ -201,6 +204,7 @@ describe("readOpenAICodexCliOAuthProfile", () => {
|
||||
access: accessToken,
|
||||
refresh: "refresh-token",
|
||||
accountId: "acct_123",
|
||||
idToken: "id-token",
|
||||
email: "codex@example.com",
|
||||
},
|
||||
});
|
||||
@@ -217,6 +221,7 @@ describe("readOpenAICodexCliOAuthProfile", () => {
|
||||
JSON.stringify({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
id_token: "id-token",
|
||||
access_token: accessToken,
|
||||
refresh_token: "refresh-token",
|
||||
account_id: "acct_123",
|
||||
@@ -254,6 +259,7 @@ describe("readOpenAICodexCliOAuthProfile", () => {
|
||||
JSON.stringify({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
id_token: "id-token",
|
||||
access_token: accessToken,
|
||||
refresh_token: "refresh-token",
|
||||
account_id: "acct_123",
|
||||
@@ -281,6 +287,7 @@ describe("readOpenAICodexCliOAuthProfile", () => {
|
||||
access: accessToken,
|
||||
refresh: "refresh-token",
|
||||
accountId: "acct_123",
|
||||
idToken: "id-token",
|
||||
email: "codex@example.com",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ export const OPENAI_CODEX_DEFAULT_PROFILE_ID = `${PROVIDER_ID}:default`;
|
||||
type CodexCliAuthFile = {
|
||||
auth_mode?: unknown;
|
||||
tokens?: {
|
||||
id_token?: unknown;
|
||||
access_token?: unknown;
|
||||
refresh_token?: unknown;
|
||||
account_id?: unknown;
|
||||
@@ -76,7 +77,8 @@ function oauthCredentialMatches(a: OAuthCredential, b: OAuthCredential): boolean
|
||||
a.displayName === b.displayName &&
|
||||
a.enterpriseUrl === b.enterpriseUrl &&
|
||||
a.projectId === b.projectId &&
|
||||
a.accountId === b.accountId
|
||||
a.accountId === b.accountId &&
|
||||
a.idToken === b.idToken
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,6 +133,7 @@ export function readOpenAICodexCliOAuthProfile(params: {
|
||||
}
|
||||
|
||||
const accountId = trimNonEmptyString(authFile.tokens?.account_id);
|
||||
const idToken = trimNonEmptyString(authFile.tokens?.id_token);
|
||||
const identity = resolveCodexAuthIdentity({ accessToken: access });
|
||||
const credential: OAuthCredential = {
|
||||
type: "oauth",
|
||||
@@ -139,6 +142,7 @@ export function readOpenAICodexCliOAuthProfile(params: {
|
||||
refresh,
|
||||
expires: resolveCodexAccessTokenExpiry(access) ?? 0,
|
||||
...(accountId ? { accountId } : {}),
|
||||
...(idToken ? { idToken } : {}),
|
||||
...(identity.email ? { email: identity.email } : {}),
|
||||
...(identity.profileName ? { displayName: identity.profileName } : {}),
|
||||
};
|
||||
|
||||
@@ -36,10 +36,12 @@ describe("prepareOpenAICodexCliExecution", () => {
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
accountId: "acct-123",
|
||||
idToken: "id-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
{ filterExternalAuthProfiles: false },
|
||||
);
|
||||
|
||||
const result = await prepareOpenAICodexCliExecution({
|
||||
@@ -64,6 +66,7 @@ describe("prepareOpenAICodexCliExecution", () => {
|
||||
expect(authFile).toEqual({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
id_token: "id-token",
|
||||
access_token: "access-token",
|
||||
refresh_token: "refresh-token",
|
||||
account_id: "acct-123",
|
||||
@@ -90,6 +93,7 @@ describe("prepareOpenAICodexCliExecution", () => {
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
{ filterExternalAuthProfiles: false },
|
||||
);
|
||||
|
||||
await expect(
|
||||
@@ -124,6 +128,7 @@ describe("prepareOpenAICodexCliExecution", () => {
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
{ filterExternalAuthProfiles: false },
|
||||
);
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -1,8 +1,56 @@
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import type {
|
||||
CliBackendPreparedExecution,
|
||||
CliBackendPrepareExecutionContext,
|
||||
} from "openclaw/plugin-sdk/cli-backend";
|
||||
import { prepareCodexAuthBridgeFromProfile } from "openclaw/plugin-sdk/codex-auth-bridge-runtime";
|
||||
import {
|
||||
ensureAuthProfileStoreForLocalUpdate,
|
||||
type OAuthCredential,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { writePrivateSecretFileAtomic } from "openclaw/plugin-sdk/secret-file-runtime";
|
||||
|
||||
const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
|
||||
const CODEX_AUTH_ENV_CLEAR_KEYS = ["OPENAI_API_KEY"] as const;
|
||||
|
||||
function isCodexBridgeableOAuthCredential(value: unknown): value is OAuthCredential {
|
||||
return Boolean(
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"type" in value &&
|
||||
"provider" in value &&
|
||||
"access" in value &&
|
||||
"refresh" in value &&
|
||||
value.type === "oauth" &&
|
||||
value.provider === OPENAI_CODEX_PROVIDER_ID &&
|
||||
typeof value.access === "string" &&
|
||||
value.access.trim().length > 0 &&
|
||||
typeof value.refresh === "string" &&
|
||||
value.refresh.trim().length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveCodexBridgeHome(agentDir: string, profileId: string): string {
|
||||
const digest = crypto.createHash("sha256").update(profileId).digest("hex").slice(0, 16);
|
||||
return path.join(agentDir, "cli-auth", "codex", digest);
|
||||
}
|
||||
|
||||
function buildCodexAuthFile(credential: OAuthCredential): string {
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
...(credential.idToken ? { id_token: credential.idToken } : {}),
|
||||
access_token: credential.access,
|
||||
refresh_token: credential.refresh,
|
||||
...(credential.accountId ? { account_id: credential.accountId } : {}),
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
export async function prepareOpenAICodexCliExecution(
|
||||
ctx: CliBackendPrepareExecutionContext,
|
||||
@@ -11,19 +59,23 @@ export async function prepareOpenAICodexCliExecution(
|
||||
return null;
|
||||
}
|
||||
|
||||
const bridge = await prepareCodexAuthBridgeFromProfile({
|
||||
agentDir: ctx.agentDir,
|
||||
authProfileId: ctx.authProfileId,
|
||||
bridgeRoot: "cli-auth",
|
||||
});
|
||||
if (!bridge) {
|
||||
const store = ensureAuthProfileStoreForLocalUpdate(ctx.agentDir);
|
||||
const credential = store.profiles[ctx.authProfileId];
|
||||
if (!isCodexBridgeableOAuthCredential(credential)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const codexHome = resolveCodexBridgeHome(ctx.agentDir, ctx.authProfileId);
|
||||
await writePrivateSecretFileAtomic({
|
||||
rootDir: ctx.agentDir,
|
||||
filePath: path.join(codexHome, "auth.json"),
|
||||
content: buildCodexAuthFile(credential),
|
||||
});
|
||||
|
||||
return {
|
||||
env: {
|
||||
CODEX_HOME: bridge.codexHome,
|
||||
CODEX_HOME: codexHome,
|
||||
},
|
||||
clearEnv: bridge.clearEnv,
|
||||
clearEnv: [...CODEX_AUTH_ENV_CLEAR_KEYS],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,6 +118,41 @@ describe("openai codex provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("exposes Codex CLI auth as a runtime-only external profile", () => {
|
||||
const provider = buildOpenAICodexProviderPlugin();
|
||||
const credential = {
|
||||
type: "oauth" as const,
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
accountId: "acct-123",
|
||||
};
|
||||
readOpenAICodexCliOAuthProfileMock.mockReturnValueOnce({
|
||||
profileId: "openai-codex:default",
|
||||
credential,
|
||||
});
|
||||
|
||||
expect(
|
||||
provider.resolveExternalAuthProfiles?.({
|
||||
env: { CODEX_HOME: "/sandboxed/codex-home" } as NodeJS.ProcessEnv,
|
||||
store: { version: 1, profiles: {} },
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
profileId: "openai-codex:default",
|
||||
credential,
|
||||
persistence: "runtime-only",
|
||||
},
|
||||
]);
|
||||
expect(readOpenAICodexCliOAuthProfileMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
env: expect.objectContaining({ CODEX_HOME: "/sandboxed/codex-home" }),
|
||||
store: { version: 1, profiles: {} },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the provider auth context env when importing Codex CLI auth", async () => {
|
||||
const provider = buildOpenAICodexProviderPlugin();
|
||||
const importMethod = provider.auth?.find((method) => method.id === "import-codex-cli");
|
||||
@@ -156,7 +191,7 @@ describe("openai codex provider", () => {
|
||||
).resolves.toMatchObject({
|
||||
profiles: [
|
||||
{
|
||||
profileId: "default:codex@example.com",
|
||||
profileId: "openai-codex:default",
|
||||
credential: expect.objectContaining({
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
|
||||
@@ -5,9 +5,10 @@ import type {
|
||||
ProviderRuntimeModel,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import {
|
||||
ensureAuthProfileStore,
|
||||
ensureAuthProfileStoreForLocalUpdate,
|
||||
listProfilesForProvider,
|
||||
type OAuthCredential,
|
||||
type ProviderAuthResult,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { loginOpenAICodexOAuth } from "openclaw/plugin-sdk/provider-auth-login";
|
||||
@@ -285,9 +286,7 @@ async function runOpenAICodexOAuth(ctx: ProviderAuthContext) {
|
||||
async function runImportOpenAICodexCliAuth(ctx: ProviderAuthContext) {
|
||||
const profile = readOpenAICodexCliOAuthProfile({
|
||||
env: ctx.env ?? process.env,
|
||||
store: ensureAuthProfileStore(ctx.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
}),
|
||||
store: ensureAuthProfileStoreForLocalUpdate(ctx.agentDir),
|
||||
});
|
||||
if (!profile) {
|
||||
throw new Error(
|
||||
@@ -295,20 +294,38 @@ async function runImportOpenAICodexCliAuth(ctx: ProviderAuthContext) {
|
||||
);
|
||||
}
|
||||
|
||||
return buildOauthProviderAuthResult({
|
||||
providerId: PROVIDER_ID,
|
||||
return {
|
||||
profiles: [{ profileId: profile.profileId, credential: profile.credential }],
|
||||
configPatch: {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
[OPENAI_CODEX_DEFAULT_MODEL]: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultModel: OPENAI_CODEX_DEFAULT_MODEL,
|
||||
access: profile.credential.access,
|
||||
refresh: profile.credential.refresh,
|
||||
expires: profile.credential.expires,
|
||||
email: profile.credential.email,
|
||||
displayName: profile.credential.displayName,
|
||||
profilePrefix: "default",
|
||||
credentialExtra: profile.credential.accountId
|
||||
? { accountId: profile.credential.accountId }
|
||||
: {},
|
||||
notes: ["Imported existing Codex CLI login into OpenClaw canonical auth."],
|
||||
} satisfies ProviderAuthResult;
|
||||
}
|
||||
|
||||
function ensureOpenAICodexCatalogAuthStore(ctx: { agentDir?: string; env?: NodeJS.ProcessEnv }) {
|
||||
const store = ensureAuthProfileStoreForLocalUpdate(ctx.agentDir);
|
||||
const profile = readOpenAICodexCliOAuthProfile({
|
||||
env: ctx.env ?? process.env,
|
||||
store,
|
||||
});
|
||||
if (!profile) {
|
||||
return store;
|
||||
}
|
||||
return {
|
||||
...store,
|
||||
profiles: {
|
||||
...store.profiles,
|
||||
[profile.profileId]: profile.credential,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildOpenAICodexAuthDoctorHint(ctx: { profileId?: string }) {
|
||||
@@ -350,9 +367,7 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin {
|
||||
catalog: {
|
||||
order: "profile",
|
||||
run: async (ctx) => {
|
||||
const authStore = ensureAuthProfileStore(ctx.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const authStore = ensureOpenAICodexCatalogAuthStore(ctx);
|
||||
if (listProfilesForProvider(authStore, PROVIDER_ID).length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -395,6 +410,13 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin {
|
||||
fetchUsageSnapshot: async (ctx) =>
|
||||
await fetchCodexUsage(ctx.token, ctx.accountId, ctx.timeoutMs, ctx.fetchFn),
|
||||
refreshOAuth: async (cred) => await refreshOpenAICodexOAuthCredential(cred),
|
||||
resolveExternalAuthProfiles: (ctx) => {
|
||||
const profile = readOpenAICodexCliOAuthProfile({
|
||||
env: ctx.env,
|
||||
store: ctx.store,
|
||||
});
|
||||
return profile ? [{ ...profile, persistence: "runtime-only" }] : [];
|
||||
},
|
||||
augmentModelCatalog: (ctx) => {
|
||||
const gpt54Template = findCatalogTemplate({
|
||||
entries: ctx.entries,
|
||||
|
||||
@@ -305,10 +305,6 @@
|
||||
"types": "./dist/plugin-sdk/agent-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/agent-runtime.js"
|
||||
},
|
||||
"./plugin-sdk/codex-auth-bridge-runtime": {
|
||||
"types": "./dist/plugin-sdk/codex-auth-bridge-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/codex-auth-bridge-runtime.js"
|
||||
},
|
||||
"./plugin-sdk/simple-completion-runtime": {
|
||||
"types": "./dist/plugin-sdk/simple-completion-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/simple-completion-runtime.js"
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
"text-runtime",
|
||||
"text-chunking",
|
||||
"agent-runtime",
|
||||
"codex-auth-bridge-runtime",
|
||||
"simple-completion-runtime",
|
||||
"speech-core",
|
||||
"plugin-runtime",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ProviderExternalAuthProfile } from "../plugins/provider-external-auth.types.js";
|
||||
import {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
ensureAuthProfileStore,
|
||||
@@ -10,8 +11,12 @@ import {
|
||||
import { AUTH_STORE_VERSION, log } from "./auth-profiles/constants.js";
|
||||
import type { AuthProfileCredential } from "./auth-profiles/types.js";
|
||||
|
||||
const resolveExternalAuthProfilesWithPluginsMock = vi.hoisted(() =>
|
||||
vi.fn<() => ProviderExternalAuthProfile[]>(() => []),
|
||||
);
|
||||
|
||||
vi.mock("../plugins/provider-runtime.js", () => ({
|
||||
resolveExternalAuthProfilesWithPlugins: () => [],
|
||||
resolveExternalAuthProfilesWithPlugins: resolveExternalAuthProfilesWithPluginsMock,
|
||||
}));
|
||||
|
||||
vi.mock("./cli-credentials.js", () => ({
|
||||
@@ -51,6 +56,11 @@ vi.mock("./cli-credentials.js", () => ({
|
||||
}));
|
||||
|
||||
describe("ensureAuthProfileStore", () => {
|
||||
afterEach(() => {
|
||||
resolveExternalAuthProfilesWithPluginsMock.mockReset();
|
||||
resolveExternalAuthProfilesWithPluginsMock.mockReturnValue([]);
|
||||
});
|
||||
|
||||
function withTempAgentDir<T>(prefix: string, run: (agentDir: string) => T): T {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
try {
|
||||
@@ -385,55 +395,43 @@ describe("ensureAuthProfileStore", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("exposes Codex CLI auth without persisting copied tokens into auth-profiles.json", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-external-sync-"));
|
||||
const previousCodexHome = process.env.CODEX_HOME;
|
||||
it("exposes provider-managed runtime auth without persisting copied tokens", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-external-auth-"));
|
||||
const previousAgentDir = process.env.OPENCLAW_AGENT_DIR;
|
||||
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
try {
|
||||
const agentDir = path.join(root, "agent");
|
||||
const codexHome = path.join(root, "codex-home");
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
fs.mkdirSync(codexHome, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(codexHome, "auth.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
access_token: "codex-access-token",
|
||||
refresh_token: "codex-refresh-token",
|
||||
account_id: "acct_123",
|
||||
},
|
||||
last_refresh: "2026-03-01T00:00:00.000Z",
|
||||
resolveExternalAuthProfilesWithPluginsMock.mockReturnValueOnce([
|
||||
{
|
||||
profileId: "demo-provider:external",
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: "demo-provider",
|
||||
access: "external-access-token",
|
||||
refresh: "external-refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
accountId: "acct_123",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
persistence: "runtime-only",
|
||||
},
|
||||
]);
|
||||
|
||||
process.env.CODEX_HOME = codexHome;
|
||||
process.env.OPENCLAW_AGENT_DIR = agentDir;
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
expect(store.profiles["openai-codex:default"]).toMatchObject({
|
||||
expect(store.profiles["demo-provider:external"]).toMatchObject({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "codex-access-token",
|
||||
refresh: "codex-refresh-token",
|
||||
provider: "demo-provider",
|
||||
access: "external-access-token",
|
||||
refresh: "external-refresh-token",
|
||||
});
|
||||
|
||||
expect(fs.existsSync(path.join(agentDir, "auth-profiles.json"))).toBe(false);
|
||||
} finally {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
if (previousCodexHome === undefined) {
|
||||
delete process.env.CODEX_HOME;
|
||||
} else {
|
||||
process.env.CODEX_HOME = previousCodexHome;
|
||||
}
|
||||
if (previousAgentDir === undefined) {
|
||||
delete process.env.OPENCLAW_AGENT_DIR;
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { captureEnv } from "../../test-utils/env.js";
|
||||
import { __testing as externalAuthTesting } from "./external-auth.js";
|
||||
import {
|
||||
createOAuthManager,
|
||||
isSafeToAdoptBootstrapOAuthIdentity,
|
||||
@@ -9,7 +11,11 @@ import {
|
||||
isSafeToOverwriteStoredOAuthIdentity,
|
||||
OAuthManagerRefreshError,
|
||||
} from "./oauth-manager.js";
|
||||
import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js";
|
||||
import {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
ensureAuthProfileStore,
|
||||
saveAuthProfileStore,
|
||||
} from "./store.js";
|
||||
import type { AuthProfileStore, OAuthCredential } from "./types.js";
|
||||
|
||||
function createCredential(overrides: Partial<OAuthCredential> = {}): OAuthCredential {
|
||||
@@ -24,8 +30,17 @@ function createCredential(overrides: Partial<OAuthCredential> = {}): OAuthCreden
|
||||
}
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const envSnapshot = captureEnv(["OPENCLAW_STATE_DIR", "OPENCLAW_AGENT_DIR", "PI_CODING_AGENT_DIR"]);
|
||||
|
||||
beforeEach(() => {
|
||||
externalAuthTesting.setResolveExternalAuthProfilesForTest(() => []);
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
envSnapshot.restore();
|
||||
externalAuthTesting.resetResolveExternalAuthProfilesForTest();
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
@@ -75,7 +90,7 @@ describe("isSafeToOverwriteStoredOAuthIdentity", () => {
|
||||
});
|
||||
|
||||
describe("isSafeToAdoptMainStoreOAuthIdentity", () => {
|
||||
it("requires positive identity binding before adopting from the main store", () => {
|
||||
it("allows identity-less credentials to adopt from the main store", () => {
|
||||
expect(
|
||||
isSafeToAdoptMainStoreOAuthIdentity(
|
||||
createCredential({
|
||||
@@ -88,7 +103,7 @@ describe("isSafeToAdoptMainStoreOAuthIdentity", () => {
|
||||
accountId: "acct-main",
|
||||
}),
|
||||
),
|
||||
).toBe(false);
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts matching account identities", () => {
|
||||
@@ -131,8 +146,15 @@ describe("OAuthManagerRefreshError", () => {
|
||||
|
||||
describe("createOAuthManager", () => {
|
||||
it("refreshes with the adopted external oauth credential", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "oauth-manager-refresh-"));
|
||||
tempDirs.push(agentDir);
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "oauth-manager-refresh-"));
|
||||
tempDirs.push(tempRoot);
|
||||
process.env.OPENCLAW_STATE_DIR = tempRoot;
|
||||
const mainAgentDir = path.join(tempRoot, "agents", "main", "agent");
|
||||
const agentDir = path.join(tempRoot, "agents", "sub", "agent");
|
||||
process.env.OPENCLAW_AGENT_DIR = mainAgentDir;
|
||||
process.env.PI_CODING_AGENT_DIR = mainAgentDir;
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.mkdir(mainAgentDir, { recursive: true });
|
||||
const profileId = "minimax-portal:default";
|
||||
const localCredential = createCredential({
|
||||
provider: "minimax-portal",
|
||||
@@ -148,6 +170,7 @@ describe("createOAuthManager", () => {
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
{ filterExternalAuthProfiles: false },
|
||||
);
|
||||
|
||||
const manager = createOAuthManager({
|
||||
|
||||
@@ -310,7 +310,7 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) {
|
||||
if (existing && existing.provider !== params.refreshed.provider) {
|
||||
return false;
|
||||
}
|
||||
if (existing && !isSafeToOverwriteStoredOAuthIdentity(existing, params.refreshed)) {
|
||||
if (existing && !isSafeToAdoptMainStoreOAuthIdentity(existing, params.refreshed)) {
|
||||
log.warn("refused to mirror OAuth credential: identity mismatch or regression", {
|
||||
profileId: params.profileId,
|
||||
});
|
||||
|
||||
@@ -22,7 +22,8 @@ export function areOAuthCredentialsEquivalent(
|
||||
a.email === b.email &&
|
||||
a.enterpriseUrl === b.enterpriseUrl &&
|
||||
a.projectId === b.projectId &&
|
||||
a.accountId === b.accountId
|
||||
a.accountId === b.accountId &&
|
||||
a.idToken === b.idToken
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,6 +142,12 @@ export function isSafeToAdoptMainStoreOAuthIdentity(
|
||||
if (existing.provider !== incoming.provider) {
|
||||
return false;
|
||||
}
|
||||
if (areOAuthCredentialsEquivalent(existing, incoming)) {
|
||||
return true;
|
||||
}
|
||||
if (!hasOAuthIdentity(existing)) {
|
||||
return true;
|
||||
}
|
||||
return hasMatchingOAuthIdentity(existing, incoming);
|
||||
}
|
||||
|
||||
|
||||
@@ -652,10 +652,9 @@ describe("resolveApiKeyForProfile OAuth refresh mirror-to-main (#26322)", () =>
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses to mirror into a main store that lacks identity binding", async () => {
|
||||
// Authoritative main-store overwrites now require a positive identity
|
||||
// match. A pre-capture main credential must not be replaced by a
|
||||
// sub-agent refresh from an arbitrary account.
|
||||
it("mirrors identity-bearing refreshes into a pre-capture main store", async () => {
|
||||
// Pre-capture main credentials may lack account identity. Allow the
|
||||
// refreshed sub-agent credential to upgrade the main store with identity.
|
||||
const profileId = "openai-codex:default";
|
||||
const provider = "openai-codex";
|
||||
const freshExpiry = Date.now() + 60 * 60 * 1000;
|
||||
@@ -703,14 +702,13 @@ describe("resolveApiKeyForProfile OAuth refresh mirror-to-main (#26322)", () =>
|
||||
});
|
||||
expect(result?.apiKey).toBe("sub-refreshed-access");
|
||||
|
||||
// Main must remain unchanged because there was no positive identity
|
||||
// binding to authorize the overwrite.
|
||||
const mainRaw = JSON.parse(
|
||||
await fs.readFile(path.join(mainAgentDir, "auth-profiles.json"), "utf8"),
|
||||
) as AuthProfileStore;
|
||||
expect(mainRaw.profiles[profileId]).toMatchObject({
|
||||
access: "main-pre-capture-access",
|
||||
refresh: "main-pre-capture-refresh",
|
||||
access: "sub-refreshed-access",
|
||||
refresh: "sub-refreshed-refresh",
|
||||
accountId: "acct-sub",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export type OAuthCredentials = {
|
||||
enterpriseUrl?: string;
|
||||
projectId?: string;
|
||||
accountId?: string;
|
||||
idToken?: string;
|
||||
};
|
||||
|
||||
export type ApiKeyCredential = {
|
||||
|
||||
@@ -255,6 +255,7 @@ describe("cli credentials", () => {
|
||||
expect(cmd).toContain(accountHash);
|
||||
return JSON.stringify({
|
||||
tokens: {
|
||||
id_token: "keychain-id-token",
|
||||
access_token: createJwtWithExp(expSeconds),
|
||||
refresh_token: "keychain-refresh",
|
||||
},
|
||||
@@ -269,6 +270,7 @@ describe("cli credentials", () => {
|
||||
refresh: "keychain-refresh",
|
||||
provider: "openai-codex",
|
||||
expires: expSeconds * 1000,
|
||||
idToken: "keychain-id-token",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -286,6 +288,7 @@ describe("cli credentials", () => {
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
tokens: {
|
||||
id_token: "file-id-token",
|
||||
access_token: createJwtWithExp(expSeconds),
|
||||
refresh_token: "file-refresh",
|
||||
},
|
||||
@@ -300,6 +303,7 @@ describe("cli credentials", () => {
|
||||
refresh: "file-refresh",
|
||||
provider: "openai-codex",
|
||||
expires: expSeconds * 1000,
|
||||
idToken: "file-id-token",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -393,6 +397,7 @@ describe("cli credentials", () => {
|
||||
access: "new-access",
|
||||
refresh: "new-refresh",
|
||||
expires: Date.now() + 60_000,
|
||||
idToken: "new-id-token",
|
||||
accountId: "acct-new",
|
||||
});
|
||||
|
||||
@@ -403,7 +408,7 @@ describe("cli credentials", () => {
|
||||
OPENAI_API_KEY: "sk-existing",
|
||||
});
|
||||
expect(persisted.tokens).toMatchObject({
|
||||
id_token: "id-token",
|
||||
id_token: "new-id-token",
|
||||
access_token: "new-access",
|
||||
refresh_token: "new-refresh",
|
||||
account_id: "acct-new",
|
||||
@@ -439,6 +444,7 @@ describe("cli credentials", () => {
|
||||
access: "new-access",
|
||||
refresh: "new-refresh",
|
||||
expires: Date.now() + 60_000,
|
||||
idToken: "new-id-token",
|
||||
accountId: "acct-new",
|
||||
},
|
||||
{
|
||||
@@ -460,7 +466,7 @@ describe("cli credentials", () => {
|
||||
expect(payload).toBeDefined();
|
||||
const parsed = JSON.parse(String(payload)) as Record<string, unknown>;
|
||||
expect(parsed.tokens).toMatchObject({
|
||||
id_token: "id-token",
|
||||
id_token: "new-id-token",
|
||||
access_token: "new-access",
|
||||
refresh_token: "new-refresh",
|
||||
account_id: "acct-new",
|
||||
|
||||
@@ -56,6 +56,7 @@ export type CodexCliCredential = {
|
||||
refresh: string;
|
||||
expires: number;
|
||||
accountId?: string;
|
||||
idToken?: string;
|
||||
};
|
||||
|
||||
export type MiniMaxCliCredential = {
|
||||
@@ -289,6 +290,7 @@ function readCodexKeychainCredentials(options?: {
|
||||
: Date.now() + 60 * 60 * 1000;
|
||||
const expires = decodeJwtExpiryMs(accessToken) ?? fallbackExpiry;
|
||||
const accountId = typeof tokens?.account_id === "string" ? tokens.account_id : undefined;
|
||||
const idToken = typeof tokens?.id_token === "string" ? tokens.id_token : undefined;
|
||||
|
||||
log.info("read codex credentials from keychain", {
|
||||
source: "keychain",
|
||||
@@ -302,6 +304,7 @@ function readCodexKeychainCredentials(options?: {
|
||||
refresh: refreshToken,
|
||||
expires,
|
||||
accountId,
|
||||
idToken,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -542,6 +545,9 @@ function buildUpdatedCodexAuthRecord(
|
||||
...existingTokens,
|
||||
access_token: newCredentials.access,
|
||||
refresh_token: newCredentials.refresh,
|
||||
...(typeof newCredentials.idToken === "string" && newCredentials.idToken.trim().length > 0
|
||||
? { id_token: newCredentials.idToken }
|
||||
: {}),
|
||||
...(typeof newCredentials.accountId === "string" && newCredentials.accountId.trim().length > 0
|
||||
? { account_id: newCredentials.accountId }
|
||||
: {}),
|
||||
@@ -698,6 +704,7 @@ export function readCodexCliCredentials(options?: {
|
||||
refresh: refreshToken,
|
||||
expires,
|
||||
accountId: typeof tokens.account_id === "string" ? tokens.account_id : undefined,
|
||||
idToken: typeof tokens.id_token === "string" ? tokens.id_token : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { writePrivateSecretFileAtomic } from "../infra/secret-file.js";
|
||||
import { loadAuthProfileStoreForSecretsRuntime } from "./auth-profiles/store.js";
|
||||
import type { OAuthCredential } from "./auth-profiles/types.js";
|
||||
|
||||
export const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
|
||||
export const CODEX_AUTH_ENV_CLEAR_KEYS = ["OPENAI_API_KEY"] as const;
|
||||
|
||||
export function isCodexBridgeableOAuthCredential(value: unknown): value is OAuthCredential {
|
||||
return Boolean(
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"type" in value &&
|
||||
"provider" in value &&
|
||||
"access" in value &&
|
||||
"refresh" in value &&
|
||||
value.type === "oauth" &&
|
||||
value.provider === OPENAI_CODEX_PROVIDER_ID &&
|
||||
typeof value.access === "string" &&
|
||||
value.access.trim().length > 0 &&
|
||||
typeof value.refresh === "string" &&
|
||||
value.refresh.trim().length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveCodexBridgeHome(
|
||||
agentDir: string,
|
||||
profileId: string,
|
||||
bridgeRoot: "cli-auth" | "harness-auth",
|
||||
): string {
|
||||
const digest = crypto.createHash("sha256").update(profileId).digest("hex").slice(0, 16);
|
||||
return path.join(agentDir, bridgeRoot, "codex", digest);
|
||||
}
|
||||
|
||||
export function buildCodexAuthFile(credential: OAuthCredential): string {
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
access_token: credential.access,
|
||||
refresh_token: credential.refresh,
|
||||
...(credential.accountId ? { account_id: credential.accountId } : {}),
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
export async function prepareCodexAuthBridgeFromProfile(params: {
|
||||
agentDir: string;
|
||||
authProfileId: string;
|
||||
bridgeRoot: "cli-auth" | "harness-auth";
|
||||
}): Promise<{ codexHome: string; clearEnv: string[] } | null> {
|
||||
const store = loadAuthProfileStoreForSecretsRuntime(params.agentDir);
|
||||
const credential = store.profiles[params.authProfileId];
|
||||
if (!isCodexBridgeableOAuthCredential(credential)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const codexHome = resolveCodexBridgeHome(
|
||||
params.agentDir,
|
||||
params.authProfileId,
|
||||
params.bridgeRoot,
|
||||
);
|
||||
await writePrivateSecretFileAtomic({
|
||||
rootDir: params.agentDir,
|
||||
filePath: path.join(codexHome, "auth.json"),
|
||||
content: buildCodexAuthFile(credential),
|
||||
});
|
||||
return {
|
||||
codexHome,
|
||||
clearEnv: [...CODEX_AUTH_ENV_CLEAR_KEYS],
|
||||
};
|
||||
}
|
||||
@@ -32,6 +32,16 @@ describe("gateway codex harness live helpers", () => {
|
||||
expect(isExpectedCodexModelsCommandText(text)).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts sandbox namespace failures with current-session model fallback", () => {
|
||||
const text = [
|
||||
"I can’t enumerate `/codex models` from this sandbox because the local `codex` CLI fails to start here with a user-namespace restriction (`bwrap: No permissions to create a new namespace`).",
|
||||
"",
|
||||
"What I can confirm from the current session is that it’s running on `codex/gpt-5.4`.",
|
||||
].join("\n");
|
||||
|
||||
expect(isExpectedCodexModelsCommandText(text)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects unrelated codex command output", () => {
|
||||
expect(isExpectedCodexModelsCommandText("Codex is healthy.")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -42,18 +42,23 @@ export const EXPECTED_CODEX_MODELS_COMMAND_TEXT = [
|
||||
|
||||
export function isExpectedCodexModelsCommandText(text: string): boolean {
|
||||
const normalized = text.toLowerCase();
|
||||
const mentionsCodexModelsCommand =
|
||||
text.includes("`codex models`") || text.includes("`/codex models`");
|
||||
const isSandboxFallback =
|
||||
text.includes("`codex models`") &&
|
||||
(text.includes("did not run") ||
|
||||
text.includes("could not run") ||
|
||||
text.includes("could not be run") ||
|
||||
text.includes("failed in this sandbox") ||
|
||||
text.includes("failed with:") ||
|
||||
text.includes("repo-local fallback") ||
|
||||
text.includes("sandbox blocks") ||
|
||||
text.includes("interactive in this environment") ||
|
||||
text.includes("sandboxed session") ||
|
||||
text.includes("required user namespace"));
|
||||
mentionsCodexModelsCommand &&
|
||||
(normalized.includes("did not run") ||
|
||||
normalized.includes("could not run") ||
|
||||
normalized.includes("could not be run") ||
|
||||
normalized.includes("failed in this sandbox") ||
|
||||
normalized.includes("failed with:") ||
|
||||
normalized.includes("fails to start") ||
|
||||
normalized.includes("repo-local fallback") ||
|
||||
normalized.includes("sandbox blocks") ||
|
||||
normalized.includes("interactive in this environment") ||
|
||||
normalized.includes("sandboxed session") ||
|
||||
normalized.includes("required user namespace") ||
|
||||
normalized.includes("user-namespace restriction") ||
|
||||
normalized.includes("bwrap: no permissions to create a new namespace"));
|
||||
|
||||
const mentionsConfiguredModels =
|
||||
normalized.includes("configured model") ||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export {
|
||||
buildCodexAuthFile,
|
||||
CODEX_AUTH_ENV_CLEAR_KEYS,
|
||||
isCodexBridgeableOAuthCredential,
|
||||
OPENAI_CODEX_PROVIDER_ID,
|
||||
prepareCodexAuthBridgeFromProfile,
|
||||
resolveCodexBridgeHome,
|
||||
} from "../agents/codex-auth-bridge.js";
|
||||
@@ -24,6 +24,7 @@ import type { ReplyToMode } from "../config/types.base.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { buildOutboundBaseSessionKey } from "../infra/outbound/base-session-key.js";
|
||||
import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js";
|
||||
import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
|
||||
import type { ProviderRuntimeModel } from "../plugins/provider-runtime-model.types.js";
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
import type { OpenClawPluginApi } from "../plugins/types.js";
|
||||
@@ -227,11 +228,22 @@ export type ChannelOutboundSessionRouteParams = Parameters<
|
||||
NonNullable<ChannelMessagingAdapter["resolveOutboundSessionRoute"]>
|
||||
>[0];
|
||||
|
||||
var cachedSdkChatChannelMeta: ReturnType<typeof buildChatChannelMetaById> | undefined;
|
||||
var cachedSdkChatChannelMeta:
|
||||
| {
|
||||
cacheKey: string;
|
||||
metaById: ReturnType<typeof buildChatChannelMetaById>;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
function resolveSdkChatChannelMeta(id: string) {
|
||||
cachedSdkChatChannelMeta ??= buildChatChannelMetaById();
|
||||
return cachedSdkChatChannelMeta[id];
|
||||
const cacheKey = resolveBundledPluginsDir(process.env) ?? "";
|
||||
if (cachedSdkChatChannelMeta?.cacheKey !== cacheKey) {
|
||||
cachedSdkChatChannelMeta = {
|
||||
cacheKey,
|
||||
metaById: buildChatChannelMetaById(),
|
||||
};
|
||||
}
|
||||
return cachedSdkChatChannelMeta.metaById[id];
|
||||
}
|
||||
|
||||
export function getChatChannelMeta(id: ChatChannelId): ChannelMeta {
|
||||
|
||||
@@ -12,7 +12,10 @@ export type { ProviderAuthContext } from "../plugins/types.js";
|
||||
export type { AuthProfileStore, OAuthCredential } from "../agents/auth-profiles/types.js";
|
||||
|
||||
export { CLAUDE_CLI_PROFILE_ID, CODEX_CLI_PROFILE_ID } from "../agents/auth-profiles/constants.js";
|
||||
export { ensureAuthProfileStore } from "../agents/auth-profiles/store.js";
|
||||
export {
|
||||
ensureAuthProfileStore,
|
||||
ensureAuthProfileStoreForLocalUpdate,
|
||||
} from "../agents/auth-profiles/store.js";
|
||||
export {
|
||||
listProfilesForProvider,
|
||||
removeProviderAuthProfilesWithLock,
|
||||
|
||||
Reference in New Issue
Block a user