mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:50:42 +00:00
fix(codex): honor app-server auth order
This commit is contained in:
@@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Channels/CLI: keep `openclaw channels list --json` usable when provider usage fetching fails, and report per-provider usage errors without aborting the channel list. Refs #67595.
|
||||
- Agents/messaging: deliver distinct final commentary after same-target `message` tool sends while still deduping text/media already sent by the tool, so short closing remarks are no longer silently dropped. Fixes #76915. Thanks @hclsys.
|
||||
- Agents/messaging: preserve string thread IDs when matching message-tool reply dedupe routes, avoiding precision loss on numeric-looking topic IDs before channel plugin comparison. Thanks @vincentkoc.
|
||||
- OpenAI Codex: honor `auth.order.openai-codex` when starting app-server clients without an explicit auth profile, so status/model probes and implicit startup use the configured Codex account instead of falling back to the default profile. Thanks @vincentkoc.
|
||||
- OpenAI Codex: let SSRF-guarded provider requests inherit OpenClaw's undici IPv4/IPv6 fallback policy, so ChatGPT-backed Codex runs recover on IPv4-working hosts when DNS still returns unreachable IPv6 addresses. Fixes #76857. Thanks @jplavoiemtl and @SymbolStar.
|
||||
- Gateway/systemd: preserve operator-added secrets in the Gateway env file across re-stage while clearing OpenClaw-managed keys (such as `OPENCLAW_GATEWAY_TOKEN`) so a fresh staging value is never shadowed by a stale env-file copy; operator secrets are also retained when the state-dir `.env` is empty. Fixes #76860. Thanks @hclsys.
|
||||
- Plugin updates: do not short-circuit trusted official npm updates as unchanged when the default/latest spec still resolves to an already-installed prerelease that the installer should replace with a stable fallback. Thanks @vincentkoc.
|
||||
|
||||
@@ -421,6 +421,58 @@ describe("bridgeCodexAppServerStartOptions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("honors config auth order when selecting an implicit Codex profile", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
||||
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));
|
||||
try {
|
||||
upsertAuthProfile({
|
||||
agentDir,
|
||||
profileId: "openai-codex:default",
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "default-access-token",
|
||||
refresh: "default-refresh-token",
|
||||
expires: Date.now() + 24 * 60 * 60_000,
|
||||
accountId: "account-default",
|
||||
},
|
||||
});
|
||||
upsertAuthProfile({
|
||||
agentDir,
|
||||
profileId: "openai-codex:work",
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "work-access-token",
|
||||
refresh: "work-refresh-token",
|
||||
expires: Date.now() + 24 * 60 * 60_000,
|
||||
accountId: "account-work",
|
||||
},
|
||||
});
|
||||
|
||||
await applyCodexAppServerAuthProfile({
|
||||
client: { request } as never,
|
||||
agentDir,
|
||||
config: {
|
||||
auth: {
|
||||
order: {
|
||||
"openai-codex": ["openai-codex:work", "openai-codex:default"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(request).toHaveBeenCalledWith("account/login/start", {
|
||||
type: "chatgptAuthTokens",
|
||||
accessToken: "work-access-token",
|
||||
chatgptAccountId: "account-work",
|
||||
chatgptPlanType: null,
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("refreshes an expired OpenAI Codex OAuth profile before app-server login", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
||||
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));
|
||||
|
||||
@@ -36,6 +36,7 @@ export async function bridgeCodexAppServerStartOptions(params: {
|
||||
startOptions: CodexAppServerStartOptions;
|
||||
agentDir: string;
|
||||
authProfileId?: string;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): Promise<CodexAppServerStartOptions> {
|
||||
if (params.startOptions.transport !== "stdio") {
|
||||
return params.startOptions;
|
||||
@@ -48,10 +49,12 @@ export async function bridgeCodexAppServerStartOptions(params: {
|
||||
const authProfileId = resolveCodexAppServerAuthProfileId({
|
||||
authProfileId: params.authProfileId,
|
||||
store,
|
||||
config: params.config,
|
||||
});
|
||||
const shouldClearInheritedOpenAiApiKey = shouldClearOpenAiApiKeyForCodexAuthProfile({
|
||||
store,
|
||||
authProfileId,
|
||||
config: params.config,
|
||||
});
|
||||
return shouldClearInheritedOpenAiApiKey
|
||||
? withClearedEnvironmentVariables(isolatedStartOptions, CODEX_APP_SERVER_API_KEY_ENV_VARS)
|
||||
@@ -139,10 +142,12 @@ export async function applyCodexAppServerAuthProfile(params: {
|
||||
agentDir: string;
|
||||
authProfileId?: string;
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): Promise<void> {
|
||||
const loginParams = await resolveCodexAppServerAuthProfileLoginParams({
|
||||
agentDir: params.agentDir,
|
||||
authProfileId: params.authProfileId,
|
||||
config: params.config,
|
||||
});
|
||||
if (!loginParams) {
|
||||
if (params.startOptions?.transport !== "stdio") {
|
||||
@@ -164,6 +169,7 @@ export async function applyCodexAppServerAuthProfile(params: {
|
||||
function resolveCodexAppServerAuthProfileLoginParams(params: {
|
||||
agentDir: string;
|
||||
authProfileId?: string;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): Promise<LoginAccountParams | undefined> {
|
||||
return resolveCodexAppServerAuthProfileLoginParamsInternal(params);
|
||||
}
|
||||
@@ -171,6 +177,7 @@ function resolveCodexAppServerAuthProfileLoginParams(params: {
|
||||
export async function refreshCodexAppServerAuthTokens(params: {
|
||||
agentDir: string;
|
||||
authProfileId?: string;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): Promise<ChatgptAuthTokensRefreshResponse> {
|
||||
const loginParams = await resolveCodexAppServerAuthProfileLoginParamsInternal({
|
||||
...params,
|
||||
@@ -190,11 +197,13 @@ async function resolveCodexAppServerAuthProfileLoginParamsInternal(params: {
|
||||
agentDir: string;
|
||||
authProfileId?: string;
|
||||
forceOAuthRefresh?: boolean;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): Promise<LoginAccountParams | undefined> {
|
||||
const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
|
||||
const profileId = resolveCodexAppServerAuthProfileId({
|
||||
authProfileId: params.authProfileId,
|
||||
store,
|
||||
config: params.config,
|
||||
});
|
||||
if (!profileId) {
|
||||
return undefined;
|
||||
@@ -203,7 +212,7 @@ async function resolveCodexAppServerAuthProfileLoginParamsInternal(params: {
|
||||
if (!credential) {
|
||||
throw new Error(`Codex app-server auth profile "${profileId}" was not found.`);
|
||||
}
|
||||
if (!isCodexAppServerAuthProvider(credential.provider)) {
|
||||
if (!isCodexAppServerAuthProvider(credential.provider, params.config)) {
|
||||
throw new Error(
|
||||
`Codex app-server auth profile "${profileId}" must belong to provider "openai-codex" or a supported alias.`,
|
||||
);
|
||||
@@ -211,6 +220,7 @@ async function resolveCodexAppServerAuthProfileLoginParamsInternal(params: {
|
||||
const loginParams = await resolveLoginParamsForCredential(profileId, credential, {
|
||||
agentDir: params.agentDir,
|
||||
forceOAuthRefresh: params.forceOAuthRefresh === true,
|
||||
config: params.config,
|
||||
});
|
||||
if (!loginParams) {
|
||||
throw new Error(
|
||||
@@ -240,7 +250,7 @@ async function resolveCodexAppServerEnvApiKeyLoginParams(params: {
|
||||
async function resolveLoginParamsForCredential(
|
||||
profileId: string,
|
||||
credential: AuthProfileCredential,
|
||||
params: { agentDir: string; forceOAuthRefresh: boolean },
|
||||
params: { agentDir: string; forceOAuthRefresh: boolean; config?: AuthProfileOrderConfig },
|
||||
): Promise<LoginAccountParams | undefined> {
|
||||
if (credential.type === "api_key") {
|
||||
const resolved = await resolveApiKeyForProfile({
|
||||
@@ -265,6 +275,7 @@ async function resolveLoginParamsForCredential(
|
||||
const resolvedCredential = await resolveOAuthCredentialForCodexAppServer(profileId, credential, {
|
||||
agentDir: params.agentDir,
|
||||
forceRefresh: params.forceOAuthRefresh,
|
||||
config: params.config,
|
||||
});
|
||||
const accessToken = resolvedCredential.access?.trim();
|
||||
return accessToken
|
||||
@@ -275,7 +286,7 @@ async function resolveLoginParamsForCredential(
|
||||
async function resolveOAuthCredentialForCodexAppServer(
|
||||
profileId: string,
|
||||
credential: OAuthCredential,
|
||||
params: { agentDir: string; forceRefresh: boolean },
|
||||
params: { agentDir: string; forceRefresh: boolean; config?: AuthProfileOrderConfig },
|
||||
): Promise<OAuthCredential> {
|
||||
const ownerAgentDir = resolvePersistedAuthProfileOwnerAgentDir({
|
||||
agentDir: params.agentDir,
|
||||
@@ -284,7 +295,8 @@ async function resolveOAuthCredentialForCodexAppServer(
|
||||
const store = ensureAuthProfileStore(ownerAgentDir, { allowKeychainPrompt: false });
|
||||
const ownerCredential = store.profiles[profileId];
|
||||
const credentialForOwner =
|
||||
ownerCredential?.type === "oauth" && isCodexAppServerAuthProvider(ownerCredential.provider)
|
||||
ownerCredential?.type === "oauth" &&
|
||||
isCodexAppServerAuthProvider(ownerCredential.provider, params.config)
|
||||
? ownerCredential
|
||||
: credential;
|
||||
if (params.forceRefresh) {
|
||||
@@ -299,32 +311,36 @@ async function resolveOAuthCredentialForCodexAppServer(
|
||||
const refreshed = loadAuthProfileStoreForSecretsRuntime(ownerAgentDir).profiles[profileId];
|
||||
const storedCredential = store.profiles[profileId];
|
||||
const candidate =
|
||||
refreshed?.type === "oauth" && isCodexAppServerAuthProvider(refreshed.provider)
|
||||
refreshed?.type === "oauth" && isCodexAppServerAuthProvider(refreshed.provider, params.config)
|
||||
? refreshed
|
||||
: storedCredential?.type === "oauth" &&
|
||||
isCodexAppServerAuthProvider(storedCredential.provider)
|
||||
isCodexAppServerAuthProvider(storedCredential.provider, params.config)
|
||||
? storedCredential
|
||||
: credential;
|
||||
return resolved?.apiKey ? { ...candidate, access: resolved.apiKey } : candidate;
|
||||
}
|
||||
|
||||
function isCodexAppServerAuthProvider(provider: string): boolean {
|
||||
return resolveProviderIdForAuth(provider) === CODEX_APP_SERVER_AUTH_PROVIDER;
|
||||
function isCodexAppServerAuthProvider(provider: string, config?: AuthProfileOrderConfig): boolean {
|
||||
return resolveProviderIdForAuth(provider, { config }) === CODEX_APP_SERVER_AUTH_PROVIDER;
|
||||
}
|
||||
|
||||
function shouldClearOpenAiApiKeyForCodexAuthProfile(params: {
|
||||
store: ReturnType<typeof ensureAuthProfileStore>;
|
||||
authProfileId?: string;
|
||||
config?: AuthProfileOrderConfig;
|
||||
}): boolean {
|
||||
const profileId = params.authProfileId?.trim();
|
||||
const credential = profileId
|
||||
? params.store.profiles[profileId]
|
||||
: params.store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID];
|
||||
return isCodexSubscriptionCredential(credential);
|
||||
return isCodexSubscriptionCredential(credential, params.config);
|
||||
}
|
||||
|
||||
function isCodexSubscriptionCredential(credential: AuthProfileCredential | undefined): boolean {
|
||||
if (!credential || !isCodexAppServerAuthProvider(credential.provider)) {
|
||||
function isCodexSubscriptionCredential(
|
||||
credential: AuthProfileCredential | undefined,
|
||||
config?: AuthProfileOrderConfig,
|
||||
): boolean {
|
||||
if (!credential || !isCodexAppServerAuthProvider(credential.provider, config)) {
|
||||
return false;
|
||||
}
|
||||
return credential.type === "oauth" || credential.type === "token";
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
import type { resolveCodexAppServerAuthProfileIdForAgent } from "./auth-bridge.js";
|
||||
import type { CodexAppServerClient } from "./client.js";
|
||||
import type { CodexAppServerStartOptions } from "./config.js";
|
||||
|
||||
type AuthProfileOrderConfig = Parameters<
|
||||
typeof resolveCodexAppServerAuthProfileIdForAgent
|
||||
>[0]["config"];
|
||||
|
||||
export type CodexAppServerClientFactory = (
|
||||
startOptions?: CodexAppServerStartOptions,
|
||||
authProfileId?: string,
|
||||
agentDir?: string,
|
||||
config?: AuthProfileOrderConfig,
|
||||
) => Promise<CodexAppServerClient>;
|
||||
|
||||
export const defaultCodexAppServerClientFactory: CodexAppServerClientFactory = (
|
||||
startOptions,
|
||||
authProfileId,
|
||||
agentDir,
|
||||
config,
|
||||
) =>
|
||||
import("./shared-client.js").then(({ getSharedCodexAppServerClient }) =>
|
||||
getSharedCodexAppServerClient({ startOptions, authProfileId, agentDir }),
|
||||
getSharedCodexAppServerClient({ startOptions, authProfileId, agentDir, config }),
|
||||
);
|
||||
|
||||
export function createCodexAppServerClientFactoryTestHooks(
|
||||
|
||||
@@ -110,7 +110,7 @@ async function compactCodexNativeThread(
|
||||
options: { pluginConfig?: unknown } = {},
|
||||
): Promise<EmbeddedPiCompactResult | undefined> {
|
||||
const appServer = resolveCodexAppServerRuntimeOptions({ pluginConfig: options.pluginConfig });
|
||||
const binding = await readCodexAppServerBinding(params.sessionFile);
|
||||
const binding = await readCodexAppServerBinding(params.sessionFile, { config: params.config });
|
||||
if (!binding?.threadId) {
|
||||
return { ok: false, compacted: false, reason: "no codex app-server thread binding" };
|
||||
}
|
||||
@@ -127,6 +127,7 @@ async function compactCodexNativeThread(
|
||||
appServer.start,
|
||||
requestedAuthProfileId ?? binding.authProfileId,
|
||||
params.agentDir,
|
||||
params.config,
|
||||
);
|
||||
const waiter = createCodexNativeCompactionWaiter(client, binding.threadId);
|
||||
let completion: CodexNativeCompactionCompletion;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { resolveCodexAppServerAuthProfileIdForAgent } from "./auth-bridge.js";
|
||||
import type { CodexAppServerClient } from "./client.js";
|
||||
import type { CodexAppServerStartOptions } from "./config.js";
|
||||
import type { v2 } from "./protocol-generated/typescript/index.js";
|
||||
@@ -29,6 +30,7 @@ export type CodexAppServerListModelsOptions = {
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
authProfileId?: string;
|
||||
agentDir?: string;
|
||||
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
|
||||
sharedClient?: boolean;
|
||||
};
|
||||
|
||||
@@ -79,12 +81,14 @@ async function withCodexAppServerModelClient<T>(
|
||||
timeoutMs,
|
||||
authProfileId: options.authProfileId,
|
||||
agentDir: options.agentDir,
|
||||
config: options.config,
|
||||
})
|
||||
: await createIsolatedCodexAppServerClient({
|
||||
startOptions: options.startOptions,
|
||||
timeoutMs,
|
||||
authProfileId: options.authProfileId,
|
||||
agentDir: options.agentDir,
|
||||
config: options.config,
|
||||
});
|
||||
try {
|
||||
return await run({ client, timeoutMs });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { resolveCodexAppServerAuthProfileIdForAgent } from "./auth-bridge.js";
|
||||
import type { CodexAppServerStartOptions } from "./config.js";
|
||||
import type {
|
||||
CodexAppServerRequestMethod,
|
||||
@@ -14,6 +15,7 @@ export async function requestCodexAppServerJson<M extends CodexAppServerRequestM
|
||||
timeoutMs?: number;
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
authProfileId?: string;
|
||||
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
|
||||
}): Promise<CodexAppServerRequestResult<M>>;
|
||||
export async function requestCodexAppServerJson<T = JsonValue | undefined>(params: {
|
||||
method: string;
|
||||
@@ -21,6 +23,7 @@ export async function requestCodexAppServerJson<T = JsonValue | undefined>(param
|
||||
timeoutMs?: number;
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
authProfileId?: string;
|
||||
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
|
||||
}): Promise<T>;
|
||||
export async function requestCodexAppServerJson<T = JsonValue | undefined>(params: {
|
||||
method: string;
|
||||
@@ -28,6 +31,7 @@ export async function requestCodexAppServerJson<T = JsonValue | undefined>(param
|
||||
timeoutMs?: number;
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
authProfileId?: string;
|
||||
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
|
||||
}): Promise<T> {
|
||||
const timeoutMs = params.timeoutMs ?? 60_000;
|
||||
return await withTimeout(
|
||||
@@ -36,6 +40,7 @@ export async function requestCodexAppServerJson<T = JsonValue | undefined>(param
|
||||
startOptions: params.startOptions,
|
||||
timeoutMs,
|
||||
authProfileId: params.authProfileId,
|
||||
config: params.config,
|
||||
});
|
||||
return await client.request<T>(params.method, params.requestParams, { timeoutMs });
|
||||
})(),
|
||||
|
||||
@@ -561,6 +561,7 @@ export async function runCodexAppServerAttempt(
|
||||
appServer.start,
|
||||
startupAuthProfileId,
|
||||
agentDir,
|
||||
params.config,
|
||||
);
|
||||
attemptedClient = startupClient;
|
||||
startupClientForCleanup = startupClient;
|
||||
|
||||
@@ -6,6 +6,9 @@ import { createClientHarness } from "./test-support.js";
|
||||
const mocks = vi.hoisted(() => ({
|
||||
bridgeCodexAppServerStartOptions: vi.fn(async ({ startOptions }) => startOptions),
|
||||
applyCodexAppServerAuthProfile: vi.fn(async () => undefined),
|
||||
resolveCodexAppServerAuthProfileIdForAgent: vi.fn(
|
||||
(params?: { authProfileId?: string }) => params?.authProfileId,
|
||||
),
|
||||
resolveManagedCodexAppServerStartOptions: vi.fn(async (startOptions) => startOptions),
|
||||
embeddedAgentLog: { debug: vi.fn(), warn: vi.fn() },
|
||||
resolveOpenClawAgentDir: vi.fn(() => "/tmp/openclaw-agent"),
|
||||
@@ -14,6 +17,7 @@ const mocks = vi.hoisted(() => ({
|
||||
vi.mock("./auth-bridge.js", () => ({
|
||||
applyCodexAppServerAuthProfile: mocks.applyCodexAppServerAuthProfile,
|
||||
bridgeCodexAppServerStartOptions: mocks.bridgeCodexAppServerStartOptions,
|
||||
resolveCodexAppServerAuthProfileIdForAgent: mocks.resolveCodexAppServerAuthProfileIdForAgent,
|
||||
}));
|
||||
|
||||
vi.mock("./managed-binary.js", () => ({
|
||||
@@ -67,6 +71,10 @@ describe("shared Codex app-server client", () => {
|
||||
vi.restoreAllMocks();
|
||||
mocks.bridgeCodexAppServerStartOptions.mockClear();
|
||||
mocks.applyCodexAppServerAuthProfile.mockClear();
|
||||
mocks.resolveCodexAppServerAuthProfileIdForAgent.mockClear();
|
||||
mocks.resolveCodexAppServerAuthProfileIdForAgent.mockImplementation(
|
||||
(params?: { authProfileId?: string }) => params?.authProfileId,
|
||||
);
|
||||
mocks.resolveManagedCodexAppServerStartOptions.mockClear();
|
||||
mocks.resolveManagedCodexAppServerStartOptions.mockImplementation(
|
||||
async (startOptions) => startOptions,
|
||||
@@ -147,6 +155,37 @@ describe("shared Codex app-server client", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves the configured implicit auth profile before sharing a client", async () => {
|
||||
const harness = createClientHarness();
|
||||
vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
|
||||
const config = { auth: { order: { "openai-codex": ["openai-codex:work"] } } };
|
||||
mocks.resolveCodexAppServerAuthProfileIdForAgent.mockReturnValue("openai-codex:work");
|
||||
|
||||
const listPromise = listCodexAppServerModels({
|
||||
timeoutMs: 1000,
|
||||
config,
|
||||
});
|
||||
await sendInitializeResult(harness, "openclaw/0.125.0 (macOS; test)");
|
||||
await sendEmptyModelList(harness);
|
||||
|
||||
await expect(listPromise).resolves.toEqual({ models: [] });
|
||||
expect(mocks.resolveCodexAppServerAuthProfileIdForAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ config }),
|
||||
);
|
||||
expect(mocks.bridgeCodexAppServerStartOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
authProfileId: "openai-codex:work",
|
||||
config,
|
||||
}),
|
||||
);
|
||||
expect(mocks.applyCodexAppServerAuthProfile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
authProfileId: "openai-codex:work",
|
||||
config,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the selected agent dir for shared app-server auth bridging", async () => {
|
||||
const harness = createClientHarness();
|
||||
vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { resolveOpenClawAgentDir } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { applyCodexAppServerAuthProfile, bridgeCodexAppServerStartOptions } from "./auth-bridge.js";
|
||||
import {
|
||||
applyCodexAppServerAuthProfile,
|
||||
bridgeCodexAppServerStartOptions,
|
||||
resolveCodexAppServerAuthProfileIdForAgent,
|
||||
} from "./auth-bridge.js";
|
||||
import { CodexAppServerClient } from "./client.js";
|
||||
import {
|
||||
codexAppServerStartOptionsKey,
|
||||
@@ -30,19 +34,26 @@ export async function getSharedCodexAppServerClient(options?: {
|
||||
timeoutMs?: number;
|
||||
authProfileId?: string;
|
||||
agentDir?: string;
|
||||
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
|
||||
}): Promise<CodexAppServerClient> {
|
||||
const state = getSharedCodexAppServerClientState();
|
||||
const agentDir = options?.agentDir ?? resolveOpenClawAgentDir();
|
||||
const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
|
||||
authProfileId: options?.authProfileId,
|
||||
agentDir,
|
||||
config: options?.config,
|
||||
});
|
||||
const requestedStartOptions =
|
||||
options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
|
||||
const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions);
|
||||
const startOptions = await bridgeCodexAppServerStartOptions({
|
||||
startOptions: managedStartOptions,
|
||||
agentDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
authProfileId,
|
||||
config: options?.config,
|
||||
});
|
||||
const key = codexAppServerStartOptionsKey(startOptions, {
|
||||
authProfileId: options?.authProfileId,
|
||||
authProfileId,
|
||||
agentDir,
|
||||
});
|
||||
if (state.key && state.key !== key) {
|
||||
@@ -60,8 +71,9 @@ export async function getSharedCodexAppServerClient(options?: {
|
||||
await applyCodexAppServerAuthProfile({
|
||||
client,
|
||||
agentDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
authProfileId,
|
||||
startOptions,
|
||||
config: options?.config,
|
||||
});
|
||||
return client;
|
||||
} catch (error) {
|
||||
@@ -90,15 +102,22 @@ export async function createIsolatedCodexAppServerClient(options?: {
|
||||
timeoutMs?: number;
|
||||
authProfileId?: string;
|
||||
agentDir?: string;
|
||||
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
|
||||
}): Promise<CodexAppServerClient> {
|
||||
const agentDir = options?.agentDir ?? resolveOpenClawAgentDir();
|
||||
const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
|
||||
authProfileId: options?.authProfileId,
|
||||
agentDir,
|
||||
config: options?.config,
|
||||
});
|
||||
const requestedStartOptions =
|
||||
options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
|
||||
const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions);
|
||||
const startOptions = await bridgeCodexAppServerStartOptions({
|
||||
startOptions: managedStartOptions,
|
||||
agentDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
authProfileId,
|
||||
config: options?.config,
|
||||
});
|
||||
const client = CodexAppServerClient.start(startOptions);
|
||||
const initialize = client.initialize();
|
||||
@@ -107,8 +126,9 @@ export async function createIsolatedCodexAppServerClient(options?: {
|
||||
await applyCodexAppServerAuthProfile({
|
||||
client,
|
||||
agentDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
authProfileId,
|
||||
startOptions,
|
||||
config: options?.config,
|
||||
});
|
||||
return client;
|
||||
} catch (error) {
|
||||
|
||||
@@ -185,12 +185,16 @@ export async function handleCodexSubcommand(
|
||||
return { text: buildHelp() };
|
||||
}
|
||||
if (normalized === "status") {
|
||||
return { text: formatCodexStatus(await deps.readCodexStatusProbes(options.pluginConfig)) };
|
||||
return {
|
||||
text: formatCodexStatus(await deps.readCodexStatusProbes(options.pluginConfig, ctx.config)),
|
||||
};
|
||||
}
|
||||
if (normalized === "models") {
|
||||
return {
|
||||
text: formatModels(
|
||||
await deps.listCodexAppServerModels(deps.requestOptions(options.pluginConfig, 100)),
|
||||
await deps.listCodexAppServerModels(
|
||||
deps.requestOptions(options.pluginConfig, 100, ctx.config),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { resolveCodexAppServerAuthProfileIdForAgent } from "./app-server/auth-bridge.js";
|
||||
import {
|
||||
CODEX_CONTROL_METHODS,
|
||||
describeControlFailure,
|
||||
@@ -15,12 +16,21 @@ import { requestCodexAppServerJson } from "./app-server/request.js";
|
||||
|
||||
export type SafeValue<T> = { ok: true; value: T } | { ok: false; error: string };
|
||||
|
||||
export function requestOptions(pluginConfig: unknown, limit: number) {
|
||||
type AuthProfileOrderConfig = Parameters<
|
||||
typeof resolveCodexAppServerAuthProfileIdForAgent
|
||||
>[0]["config"];
|
||||
|
||||
export function requestOptions(
|
||||
pluginConfig: unknown,
|
||||
limit: number,
|
||||
config?: AuthProfileOrderConfig,
|
||||
) {
|
||||
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig });
|
||||
return {
|
||||
limit,
|
||||
timeoutMs: runtime.requestTimeoutMs,
|
||||
startOptions: runtime.start,
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,16 +40,19 @@ export function codexControlRequest<M extends CodexControlRequestMethod>(
|
||||
pluginConfig: unknown,
|
||||
method: M,
|
||||
requestParams: CodexAppServerRequestParams<M>,
|
||||
options?: { config?: AuthProfileOrderConfig },
|
||||
): Promise<CodexAppServerRequestResult<M>>;
|
||||
export function codexControlRequest(
|
||||
pluginConfig: unknown,
|
||||
method: CodexControlMethod,
|
||||
requestParams?: JsonValue,
|
||||
options?: { config?: AuthProfileOrderConfig },
|
||||
): Promise<JsonValue | undefined>;
|
||||
export async function codexControlRequest(
|
||||
pluginConfig: unknown,
|
||||
method: CodexControlMethod,
|
||||
requestParams?: unknown,
|
||||
options: { config?: AuthProfileOrderConfig } = {},
|
||||
) {
|
||||
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig });
|
||||
return await requestCodexAppServerJson({
|
||||
@@ -47,6 +60,7 @@ export async function codexControlRequest(
|
||||
requestParams,
|
||||
timeoutMs: runtime.requestTimeoutMs,
|
||||
startOptions: runtime.start,
|
||||
config: options.config,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,35 +68,56 @@ export function safeCodexControlRequest<M extends CodexControlRequestMethod>(
|
||||
pluginConfig: unknown,
|
||||
method: M,
|
||||
requestParams: CodexAppServerRequestParams<M>,
|
||||
options?: { config?: AuthProfileOrderConfig },
|
||||
): Promise<SafeValue<CodexAppServerRequestResult<M>>>;
|
||||
export function safeCodexControlRequest(
|
||||
pluginConfig: unknown,
|
||||
method: CodexControlMethod,
|
||||
requestParams?: JsonValue,
|
||||
options?: { config?: AuthProfileOrderConfig },
|
||||
): Promise<SafeValue<JsonValue | undefined>>;
|
||||
export async function safeCodexControlRequest(
|
||||
pluginConfig: unknown,
|
||||
method: CodexControlMethod,
|
||||
requestParams?: unknown,
|
||||
options: { config?: AuthProfileOrderConfig } = {},
|
||||
) {
|
||||
return await safeValue(
|
||||
async () => await codexControlRequest(pluginConfig, method, requestParams as JsonValue),
|
||||
async () =>
|
||||
await codexControlRequest(pluginConfig, method, requestParams as JsonValue, options),
|
||||
);
|
||||
}
|
||||
|
||||
async function safeCodexModelList(pluginConfig: unknown, limit: number) {
|
||||
async function safeCodexModelList(
|
||||
pluginConfig: unknown,
|
||||
limit: number,
|
||||
config?: AuthProfileOrderConfig,
|
||||
) {
|
||||
return await safeValue(
|
||||
async () => await listCodexAppServerModels(requestOptions(pluginConfig, limit)),
|
||||
async () => await listCodexAppServerModels(requestOptions(pluginConfig, limit, config)),
|
||||
);
|
||||
}
|
||||
|
||||
export async function readCodexStatusProbes(pluginConfig: unknown) {
|
||||
export async function readCodexStatusProbes(
|
||||
pluginConfig: unknown,
|
||||
config?: AuthProfileOrderConfig,
|
||||
) {
|
||||
const [models, account, limits, mcps, skills] = await Promise.all([
|
||||
safeCodexModelList(pluginConfig, 20),
|
||||
safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.account, { refreshToken: false }),
|
||||
safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.rateLimits, undefined),
|
||||
safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listMcpServers, { limit: 100 }),
|
||||
safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listSkills, {}),
|
||||
safeCodexModelList(pluginConfig, 20, config),
|
||||
safeCodexControlRequest(
|
||||
pluginConfig,
|
||||
CODEX_CONTROL_METHODS.account,
|
||||
{ refreshToken: false },
|
||||
{ config },
|
||||
),
|
||||
safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.rateLimits, undefined, { config }),
|
||||
safeCodexControlRequest(
|
||||
pluginConfig,
|
||||
CODEX_CONTROL_METHODS.listMcpServers,
|
||||
{ limit: 100 },
|
||||
{ config },
|
||||
),
|
||||
safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listSkills, {}, { config }),
|
||||
]);
|
||||
|
||||
return { models, account, limits, mcps, skills };
|
||||
|
||||
@@ -41,16 +41,23 @@ function createDeps(overrides: Partial<CodexCommandDeps> = {}): Partial<CodexCom
|
||||
codexControlRequest: vi.fn(),
|
||||
listCodexAppServerModels: vi.fn(),
|
||||
readCodexStatusProbes: vi.fn(),
|
||||
requestOptions: vi.fn((_pluginConfig: unknown, limit: number) => ({
|
||||
limit,
|
||||
timeoutMs: 1000,
|
||||
startOptions: {
|
||||
transport: "stdio",
|
||||
command: "codex",
|
||||
args: ["app-server", "--listen", "stdio://"],
|
||||
headers: {},
|
||||
} satisfies CodexAppServerStartOptions,
|
||||
})),
|
||||
requestOptions: vi.fn(
|
||||
(
|
||||
_pluginConfig: unknown,
|
||||
limit: number,
|
||||
config?: Parameters<NonNullable<CodexCommandDeps["requestOptions"]>>[2],
|
||||
) => ({
|
||||
limit,
|
||||
timeoutMs: 1000,
|
||||
startOptions: {
|
||||
transport: "stdio",
|
||||
command: "codex",
|
||||
args: ["app-server", "--listen", "stdio://"],
|
||||
headers: {},
|
||||
} satisfies CodexAppServerStartOptions,
|
||||
config,
|
||||
}),
|
||||
),
|
||||
safeCodexControlRequest: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
@@ -132,6 +139,7 @@ describe("codex command", () => {
|
||||
});
|
||||
|
||||
it("shows model ids from Codex app-server", async () => {
|
||||
const config = { auth: { order: { "openai-codex": ["openai-codex:work"] } } };
|
||||
const deps = createDeps({
|
||||
listCodexAppServerModels: vi.fn(async () => ({
|
||||
models: [
|
||||
@@ -145,9 +153,13 @@ describe("codex command", () => {
|
||||
})),
|
||||
});
|
||||
|
||||
await expect(handleCodexCommand(createContext("models"), { deps })).resolves.toEqual({
|
||||
await expect(
|
||||
handleCodexCommand(createContext("models", undefined, { config }), { deps }),
|
||||
).resolves.toEqual({
|
||||
text: "Codex models:\n- gpt-5.4",
|
||||
});
|
||||
expect(deps.requestOptions).toHaveBeenCalledWith(undefined, 100, config);
|
||||
expect(deps.listCodexAppServerModels).toHaveBeenCalledWith(expect.objectContaining({ config }));
|
||||
});
|
||||
|
||||
it("shows when Codex app-server model output is truncated", async () => {
|
||||
@@ -172,6 +184,7 @@ describe("codex command", () => {
|
||||
});
|
||||
|
||||
it("reports status unavailable when every Codex probe fails", async () => {
|
||||
const config = { auth: { order: { "openai-codex": ["openai-codex:work"] } } };
|
||||
const offline = { ok: false as const, error: "offline" };
|
||||
const deps = createDeps({
|
||||
readCodexStatusProbes: vi.fn(async () => ({
|
||||
@@ -183,7 +196,9 @@ describe("codex command", () => {
|
||||
})),
|
||||
});
|
||||
|
||||
await expect(handleCodexCommand(createContext("status"), { deps })).resolves.toEqual({
|
||||
await expect(
|
||||
handleCodexCommand(createContext("status", undefined, { config }), { deps }),
|
||||
).resolves.toEqual({
|
||||
text: [
|
||||
"Codex app-server: unavailable",
|
||||
"Models: offline",
|
||||
@@ -193,6 +208,7 @@ describe("codex command", () => {
|
||||
"Skills: offline",
|
||||
].join("\n"),
|
||||
});
|
||||
expect(deps.readCodexStatusProbes).toHaveBeenCalledWith(undefined, config);
|
||||
});
|
||||
|
||||
it("formats generated account/read responses", async () => {
|
||||
|
||||
Reference in New Issue
Block a user