mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:30:45 +00:00
fix(agents): keep OAuth auth read-through
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
loadAuthProfileStoreForSecretsRuntime,
|
||||
} from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { upsertAuthProfile } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
@@ -72,7 +76,7 @@ vi.mock("openclaw/plugin-sdk/agent-runtime", async (importOriginal) => {
|
||||
if (refreshed?.access) {
|
||||
oauthCredential = refreshed as typeof oauthCredential;
|
||||
params.store.profiles[params.profileId] = oauthCredential;
|
||||
if (params.agentDir) {
|
||||
if (params.agentDir || process.env.OPENCLAW_STATE_DIR) {
|
||||
actual.saveAuthProfileStore(params.store, params.agentDir);
|
||||
}
|
||||
}
|
||||
@@ -92,6 +96,7 @@ vi.mock("openclaw/plugin-sdk/agent-runtime", async (importOriginal) => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
oauthMocks.refreshOpenAICodexToken.mockReset();
|
||||
providerRuntimeMocks.formatProviderAuthProfileApiKeyWithPlugin.mockReset();
|
||||
providerRuntimeMocks.refreshProviderOAuthCredentialWithPlugin.mockClear();
|
||||
@@ -635,6 +640,132 @@ describe("bridgeCodexAppServerStartOptions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("refreshes inherited main Codex OAuth without cloning it into the child store", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
||||
const stateDir = path.join(root, "state");
|
||||
const childAgentDir = path.join(stateDir, "agents", "worker", "agent");
|
||||
const childAuthPath = path.join(childAgentDir, "auth-profiles.json");
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
|
||||
vi.stubEnv("OPENCLAW_AGENT_DIR", "");
|
||||
oauthMocks.refreshOpenAICodexToken.mockResolvedValueOnce({
|
||||
access: "main-refreshed-access-token",
|
||||
refresh: "main-refreshed-refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
accountId: "account-main-refreshed",
|
||||
});
|
||||
try {
|
||||
upsertAuthProfile({
|
||||
profileId: "openai-codex:work",
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "main-current-access-token",
|
||||
refresh: "main-refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
accountId: "account-main",
|
||||
email: "main-codex@example.test",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
refreshCodexAppServerAuthTokens({
|
||||
agentDir: childAgentDir,
|
||||
authProfileId: "openai-codex:work",
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
accessToken: "main-refreshed-access-token",
|
||||
chatgptAccountId: "account-main-refreshed",
|
||||
chatgptPlanType: null,
|
||||
});
|
||||
|
||||
expect(oauthMocks.refreshOpenAICodexToken).toHaveBeenCalledWith("main-refresh-token");
|
||||
await expect(fs.access(childAuthPath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect(loadAuthProfileStoreForSecretsRuntime().profiles["openai-codex:work"]).toMatchObject({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "main-refreshed-access-token",
|
||||
refresh: "main-refreshed-refresh-token",
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("force-refreshes the owner credential instead of a stale child OAuth clone", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
||||
const stateDir = path.join(root, "state");
|
||||
const childAgentDir = path.join(stateDir, "agents", "worker", "agent");
|
||||
const childAuthPath = path.join(childAgentDir, "auth-profiles.json");
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
|
||||
vi.stubEnv("OPENCLAW_AGENT_DIR", "");
|
||||
oauthMocks.refreshOpenAICodexToken.mockResolvedValueOnce({
|
||||
access: "main-refreshed-access-token",
|
||||
refresh: "main-refreshed-refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
accountId: "account-main-refreshed",
|
||||
});
|
||||
try {
|
||||
upsertAuthProfile({
|
||||
profileId: "openai-codex:work",
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "main-current-access-token",
|
||||
refresh: "main-owner-refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
accountId: "account-main",
|
||||
email: "main-codex@example.test",
|
||||
},
|
||||
});
|
||||
await fs.mkdir(childAgentDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
childAuthPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai-codex:work": {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "child-stale-access-token",
|
||||
refresh: "child-stale-refresh-token",
|
||||
expires: Date.now() - 60_000,
|
||||
accountId: "account-main",
|
||||
email: "main-codex@example.test",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
refreshCodexAppServerAuthTokens({
|
||||
agentDir: childAgentDir,
|
||||
authProfileId: "openai-codex:work",
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
accessToken: "main-refreshed-access-token",
|
||||
chatgptAccountId: "account-main-refreshed",
|
||||
chatgptPlanType: null,
|
||||
});
|
||||
|
||||
expect(oauthMocks.refreshOpenAICodexToken).toHaveBeenCalledWith("main-owner-refresh-token");
|
||||
expect(loadAuthProfileStoreForSecretsRuntime().profiles["openai-codex:work"]).toMatchObject({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "main-refreshed-access-token",
|
||||
refresh: "main-refreshed-refresh-token",
|
||||
});
|
||||
const child = JSON.parse(await fs.readFile(childAuthPath, "utf8")) as {
|
||||
profiles: Record<string, { access?: string; refresh?: string }>;
|
||||
};
|
||||
expect(child.profiles["openai-codex:work"]).toMatchObject({
|
||||
access: "child-stale-access-token",
|
||||
refresh: "child-stale-refresh-token",
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts a refreshed Codex OAuth credential when the stored provider is a legacy alias", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
||||
oauthMocks.refreshOpenAICodexToken.mockResolvedValueOnce({
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
loadAuthProfileStoreForSecretsRuntime,
|
||||
resolveProviderIdForAuth,
|
||||
resolveApiKeyForProfile,
|
||||
resolvePersistedAuthProfileOwnerAgentDir,
|
||||
saveAuthProfileStore,
|
||||
type AuthProfileCredential,
|
||||
type OAuthCredential,
|
||||
@@ -178,17 +179,26 @@ async function resolveOAuthCredentialForCodexAppServer(
|
||||
credential: OAuthCredential,
|
||||
params: { agentDir: string; forceRefresh: boolean },
|
||||
): Promise<OAuthCredential> {
|
||||
const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
|
||||
const ownerAgentDir = resolvePersistedAuthProfileOwnerAgentDir({
|
||||
agentDir: params.agentDir,
|
||||
profileId,
|
||||
});
|
||||
const store = ensureAuthProfileStore(ownerAgentDir, { allowKeychainPrompt: false });
|
||||
const ownerCredential = store.profiles[profileId];
|
||||
const credentialForOwner =
|
||||
ownerCredential?.type === "oauth" && isCodexAppServerAuthProvider(ownerCredential.provider)
|
||||
? ownerCredential
|
||||
: credential;
|
||||
if (params.forceRefresh) {
|
||||
store.profiles[profileId] = { ...credential, expires: 0 };
|
||||
saveAuthProfileStore(store, params.agentDir);
|
||||
store.profiles[profileId] = { ...credentialForOwner, expires: 0 };
|
||||
saveAuthProfileStore(store, ownerAgentDir);
|
||||
}
|
||||
const resolved = await resolveApiKeyForProfile({
|
||||
store,
|
||||
profileId,
|
||||
agentDir: params.agentDir,
|
||||
agentDir: ownerAgentDir,
|
||||
});
|
||||
const refreshed = loadAuthProfileStoreForSecretsRuntime(params.agentDir).profiles[profileId];
|
||||
const refreshed = loadAuthProfileStoreForSecretsRuntime(ownerAgentDir).profiles[profileId];
|
||||
const storedCredential = store.profiles[profileId];
|
||||
const candidate =
|
||||
refreshed?.type === "oauth" && isCodexAppServerAuthProvider(refreshed.provider)
|
||||
|
||||
@@ -80,30 +80,35 @@ function turnStartResult(turnId = "turn-auth-contract") {
|
||||
|
||||
function createCodexAuthProfileHarness(params: { startMethod: "thread/start" | "thread/resume" }) {
|
||||
const seenAuthProfileIds: Array<string | undefined> = [];
|
||||
const seenAgentDirs: Array<string | undefined> = [];
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
let notify: (notification: unknown) => Promise<void> = async () => undefined;
|
||||
__testing.setCodexAppServerClientFactoryForTests(async (_startOptions, authProfileId) => {
|
||||
seenAuthProfileIds.push(authProfileId);
|
||||
return {
|
||||
request: vi.fn(async (method: string, requestParams?: unknown) => {
|
||||
requests.push({ method, params: requestParams });
|
||||
if (method === params.startMethod) {
|
||||
return threadStartResult();
|
||||
}
|
||||
if (method === "turn/start") {
|
||||
return turnStartResult();
|
||||
}
|
||||
throw new Error(`unexpected method: ${method}`);
|
||||
}),
|
||||
addNotificationHandler: (handler: (notification: unknown) => Promise<void>) => {
|
||||
notify = handler;
|
||||
return () => undefined;
|
||||
},
|
||||
addRequestHandler: () => () => undefined,
|
||||
} as never;
|
||||
});
|
||||
__testing.setCodexAppServerClientFactoryForTests(
|
||||
async (_startOptions, authProfileId, agentDir) => {
|
||||
seenAuthProfileIds.push(authProfileId);
|
||||
seenAgentDirs.push(agentDir);
|
||||
return {
|
||||
request: vi.fn(async (method: string, requestParams?: unknown) => {
|
||||
requests.push({ method, params: requestParams });
|
||||
if (method === params.startMethod) {
|
||||
return threadStartResult();
|
||||
}
|
||||
if (method === "turn/start") {
|
||||
return turnStartResult();
|
||||
}
|
||||
throw new Error(`unexpected method: ${method}`);
|
||||
}),
|
||||
addNotificationHandler: (handler: (notification: unknown) => Promise<void>) => {
|
||||
notify = handler;
|
||||
return () => undefined;
|
||||
},
|
||||
addRequestHandler: () => () => undefined,
|
||||
} as never;
|
||||
},
|
||||
);
|
||||
return {
|
||||
seenAuthProfileIds,
|
||||
seenAgentDirs,
|
||||
async waitForMethod(method: string) {
|
||||
await vi.waitFor(() => expect(requests.some((entry) => entry.method === method)).toBe(true), {
|
||||
interval: 1,
|
||||
@@ -140,6 +145,7 @@ describe("Auth profile runtime contract - Codex app-server adapter", () => {
|
||||
const sessionFile = path.join(tmpDir, "session.jsonl");
|
||||
const params = createParams(sessionFile, tmpDir);
|
||||
params.authProfileId = AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProfileId;
|
||||
params.agentDir = tmpDir;
|
||||
|
||||
const run = runCodexAppServerAttempt(params);
|
||||
await vi.waitFor(
|
||||
@@ -149,6 +155,7 @@ describe("Auth profile runtime contract - Codex app-server adapter", () => {
|
||||
]),
|
||||
{ interval: 1 },
|
||||
);
|
||||
expect(harness.seenAgentDirs).toEqual([tmpDir]);
|
||||
await harness.waitForMethod("turn/start");
|
||||
await harness.completeTurn();
|
||||
await run;
|
||||
|
||||
@@ -4,14 +4,16 @@ import type { CodexAppServerStartOptions } from "./config.js";
|
||||
export type CodexAppServerClientFactory = (
|
||||
startOptions?: CodexAppServerStartOptions,
|
||||
authProfileId?: string,
|
||||
agentDir?: string,
|
||||
) => Promise<CodexAppServerClient>;
|
||||
|
||||
export const defaultCodexAppServerClientFactory: CodexAppServerClientFactory = (
|
||||
startOptions,
|
||||
authProfileId,
|
||||
agentDir,
|
||||
) =>
|
||||
import("./shared-client.js").then(({ getSharedCodexAppServerClient }) =>
|
||||
getSharedCodexAppServerClient({ startOptions, authProfileId }),
|
||||
getSharedCodexAppServerClient({ startOptions, authProfileId, agentDir }),
|
||||
);
|
||||
|
||||
export function createCodexAppServerClientFactoryTestHooks(
|
||||
|
||||
@@ -125,6 +125,7 @@ async function compactCodexNativeThread(
|
||||
const client = await clientFactory(
|
||||
appServer.start,
|
||||
requestedAuthProfileId ?? binding.authProfileId,
|
||||
params.agentDir,
|
||||
);
|
||||
const waiter = createCodexNativeCompactionWaiter(client, binding.threadId);
|
||||
let completion: CodexNativeCompactionCompletion;
|
||||
|
||||
@@ -355,6 +355,19 @@ describe("Codex app-server config", () => {
|
||||
expect(second).not.toContain("sk-second");
|
||||
});
|
||||
|
||||
it("derives distinct shared-client keys for distinct agent dirs", () => {
|
||||
const startOptions = {
|
||||
transport: "stdio" as const,
|
||||
command: "codex",
|
||||
args: ["app-server"],
|
||||
headers: {},
|
||||
};
|
||||
|
||||
expect(codexAppServerStartOptionsKey(startOptions, { agentDir: "/tmp/agent-a" })).not.toEqual(
|
||||
codexAppServerStartOptionsKey(startOptions, { agentDir: "/tmp/agent-b" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps runtime config keys aligned with manifest schema and UI hints", async () => {
|
||||
const manifest = JSON.parse(
|
||||
await fs.readFile(new URL("../../openclaw.plugin.json", import.meta.url), "utf8"),
|
||||
|
||||
@@ -294,7 +294,7 @@ export function resolveCodexComputerUseConfig(
|
||||
|
||||
export function codexAppServerStartOptionsKey(
|
||||
options: CodexAppServerStartOptions,
|
||||
params: { authProfileId?: string } = {},
|
||||
params: { authProfileId?: string; agentDir?: string } = {},
|
||||
): string {
|
||||
return JSON.stringify({
|
||||
transport: options.transport,
|
||||
@@ -311,6 +311,7 @@ export function codexAppServerStartOptionsKey(
|
||||
.map(([key, value]) => [key, hashSecretForKey(value, `env:${key}`)]),
|
||||
clearEnv: [...(options.clearEnv ?? [])].toSorted(),
|
||||
authProfileId: params.authProfileId ?? null,
|
||||
agentDir: params.agentDir ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export type CodexAppServerListModelsOptions = {
|
||||
timeoutMs?: number;
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
authProfileId?: string;
|
||||
agentDir?: string;
|
||||
sharedClient?: boolean;
|
||||
};
|
||||
|
||||
@@ -77,11 +78,13 @@ async function withCodexAppServerModelClient<T>(
|
||||
startOptions: options.startOptions,
|
||||
timeoutMs,
|
||||
authProfileId: options.authProfileId,
|
||||
agentDir: options.agentDir,
|
||||
})
|
||||
: await createIsolatedCodexAppServerClient({
|
||||
startOptions: options.startOptions,
|
||||
timeoutMs,
|
||||
authProfileId: options.authProfileId,
|
||||
agentDir: options.agentDir,
|
||||
});
|
||||
try {
|
||||
return await run({ client, timeoutMs });
|
||||
|
||||
@@ -145,7 +145,9 @@ function assistantMessage(text: string, timestamp: number) {
|
||||
|
||||
function createAppServerHarness(
|
||||
requestImpl: (method: string, params: unknown) => Promise<unknown>,
|
||||
options: { onStart?: (authProfileId: string | undefined) => void } = {},
|
||||
options: {
|
||||
onStart?: (authProfileId: string | undefined, agentDir: string | undefined) => void;
|
||||
} = {},
|
||||
) {
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
|
||||
@@ -154,17 +156,19 @@ function createAppServerHarness(
|
||||
return requestImpl(method, params);
|
||||
});
|
||||
|
||||
__testing.setCodexAppServerClientFactoryForTests(async (_startOptions, authProfileId) => {
|
||||
options.onStart?.(authProfileId);
|
||||
return {
|
||||
request,
|
||||
addNotificationHandler: (handler: typeof notify) => {
|
||||
notify = handler;
|
||||
return () => undefined;
|
||||
},
|
||||
addRequestHandler: () => () => undefined,
|
||||
} as never;
|
||||
});
|
||||
__testing.setCodexAppServerClientFactoryForTests(
|
||||
async (_startOptions, authProfileId, agentDir) => {
|
||||
options.onStart?.(authProfileId, agentDir);
|
||||
return {
|
||||
request,
|
||||
addNotificationHandler: (handler: typeof notify) => {
|
||||
notify = handler;
|
||||
return () => undefined;
|
||||
},
|
||||
addRequestHandler: () => () => undefined,
|
||||
} as never;
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
request,
|
||||
@@ -202,7 +206,9 @@ function createAppServerHarness(
|
||||
|
||||
function createStartedThreadHarness(
|
||||
requestImpl: (method: string, params: unknown) => Promise<unknown> = async () => undefined,
|
||||
options: { onStart?: (authProfileId: string | undefined) => void } = {},
|
||||
options: {
|
||||
onStart?: (authProfileId: string | undefined, agentDir: string | undefined) => void;
|
||||
} = {},
|
||||
) {
|
||||
return createAppServerHarness(async (method, params) => {
|
||||
const override = await requestImpl(method, params);
|
||||
@@ -1300,14 +1306,19 @@ describe("runCodexAppServerAttempt", () => {
|
||||
|
||||
it("passes the selected auth profile into app-server startup", async () => {
|
||||
const seenAuthProfileIds: Array<string | undefined> = [];
|
||||
const seenAgentDirs: Array<string | undefined> = [];
|
||||
const { requests, waitForMethod, completeTurn } = createStartedThreadHarness(undefined, {
|
||||
onStart: (authProfileId) => seenAuthProfileIds.push(authProfileId),
|
||||
onStart: (authProfileId, agentDir) => {
|
||||
seenAuthProfileIds.push(authProfileId);
|
||||
seenAgentDirs.push(agentDir);
|
||||
},
|
||||
});
|
||||
const params = createParams(
|
||||
path.join(tempDir, "session.jsonl"),
|
||||
path.join(tempDir, "workspace"),
|
||||
);
|
||||
params.authProfileId = "openai-codex:work";
|
||||
params.agentDir = path.join(tempDir, "agent");
|
||||
|
||||
const run = runCodexAppServerAttempt(params);
|
||||
await vi.waitFor(() => expect(seenAuthProfileIds).toEqual(["openai-codex:work"]), {
|
||||
@@ -1319,6 +1330,7 @@ describe("runCodexAppServerAttempt", () => {
|
||||
await run;
|
||||
|
||||
expect(seenAuthProfileIds).toEqual(["openai-codex:work"]);
|
||||
expect(seenAgentDirs).toEqual([path.join(tempDir, "agent")]);
|
||||
expect(requests.map((entry) => entry.method)).toContain("turn/start");
|
||||
});
|
||||
|
||||
@@ -1622,6 +1634,7 @@ describe("runCodexAppServerAttempt", () => {
|
||||
});
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
delete params.authProfileId;
|
||||
params.agentDir = path.join(tempDir, "agent");
|
||||
|
||||
const binding = await startOrResumeThread({
|
||||
client: {
|
||||
@@ -1660,6 +1673,7 @@ describe("runCodexAppServerAttempt", () => {
|
||||
dynamicToolsFingerprint: "[]",
|
||||
});
|
||||
const seenAuthProfileIds: Array<string | undefined> = [];
|
||||
const seenAgentDirs: Array<string | undefined> = [];
|
||||
const { requests, waitForMethod, completeTurn } = createAppServerHarness(
|
||||
async (method: string) => {
|
||||
if (method === "thread/resume") {
|
||||
@@ -1670,10 +1684,16 @@ describe("runCodexAppServerAttempt", () => {
|
||||
}
|
||||
throw new Error(`unexpected method: ${method}`);
|
||||
},
|
||||
{ onStart: (authProfileId) => seenAuthProfileIds.push(authProfileId) },
|
||||
{
|
||||
onStart: (authProfileId, agentDir) => {
|
||||
seenAuthProfileIds.push(authProfileId);
|
||||
seenAgentDirs.push(agentDir);
|
||||
},
|
||||
},
|
||||
);
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
delete params.authProfileId;
|
||||
params.agentDir = path.join(tempDir, "agent");
|
||||
|
||||
const run = runCodexAppServerAttempt(params);
|
||||
await vi.waitFor(() => expect(seenAuthProfileIds).toEqual(["openai-codex:bound"]), {
|
||||
@@ -1685,6 +1705,7 @@ describe("runCodexAppServerAttempt", () => {
|
||||
await run;
|
||||
|
||||
expect(seenAuthProfileIds).toEqual(["openai-codex:bound"]);
|
||||
expect(seenAgentDirs).toEqual([path.join(tempDir, "agent")]);
|
||||
expect(requests.map((entry) => entry.method)).toContain("turn/start");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -311,7 +311,7 @@ export async function runCodexAppServerAttempt(
|
||||
timeoutFloorMs: options.startupTimeoutFloorMs,
|
||||
signal: runAbortController.signal,
|
||||
operation: async () => {
|
||||
const startupClient = await clientFactory(appServer.start, startupAuthProfileId);
|
||||
const startupClient = await clientFactory(appServer.start, startupAuthProfileId, agentDir);
|
||||
await ensureCodexComputerUse({
|
||||
client: startupClient,
|
||||
pluginConfig: options.pluginConfig,
|
||||
|
||||
@@ -145,6 +145,33 @@ describe("shared Codex app-server client", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the selected agent dir for shared app-server auth bridging", async () => {
|
||||
const harness = createClientHarness();
|
||||
vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
|
||||
|
||||
const listPromise = listCodexAppServerModels({
|
||||
timeoutMs: 1000,
|
||||
authProfileId: "openai-codex:work",
|
||||
agentDir: "/tmp/openclaw-agent-nova",
|
||||
});
|
||||
await sendInitializeResult(harness, "openclaw/0.125.0 (macOS; test)");
|
||||
await sendEmptyModelList(harness);
|
||||
|
||||
await expect(listPromise).resolves.toEqual({ models: [] });
|
||||
expect(mocks.bridgeCodexAppServerStartOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentDir: "/tmp/openclaw-agent-nova",
|
||||
authProfileId: "openai-codex:work",
|
||||
}),
|
||||
);
|
||||
expect(mocks.applyCodexAppServerAuthProfile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentDir: "/tmp/openclaw-agent-nova",
|
||||
authProfileId: "openai-codex:work",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves the managed binary before bridging and spawning the shared client", async () => {
|
||||
const harness = createClientHarness();
|
||||
const startSpy = vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
|
||||
|
||||
@@ -29,9 +29,10 @@ export async function getSharedCodexAppServerClient(options?: {
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
timeoutMs?: number;
|
||||
authProfileId?: string;
|
||||
agentDir?: string;
|
||||
}): Promise<CodexAppServerClient> {
|
||||
const state = getSharedCodexAppServerClientState();
|
||||
const agentDir = resolveOpenClawAgentDir();
|
||||
const agentDir = options?.agentDir ?? resolveOpenClawAgentDir();
|
||||
const requestedStartOptions =
|
||||
options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
|
||||
const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions);
|
||||
@@ -42,6 +43,7 @@ export async function getSharedCodexAppServerClient(options?: {
|
||||
});
|
||||
const key = codexAppServerStartOptionsKey(startOptions, {
|
||||
authProfileId: options?.authProfileId,
|
||||
agentDir,
|
||||
});
|
||||
if (state.key && state.key !== key) {
|
||||
clearSharedCodexAppServerClient();
|
||||
@@ -87,8 +89,9 @@ export async function createIsolatedCodexAppServerClient(options?: {
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
timeoutMs?: number;
|
||||
authProfileId?: string;
|
||||
agentDir?: string;
|
||||
}): Promise<CodexAppServerClient> {
|
||||
const agentDir = resolveOpenClawAgentDir();
|
||||
const agentDir = options?.agentDir ?? resolveOpenClawAgentDir();
|
||||
const requestedStartOptions =
|
||||
options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
|
||||
const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions);
|
||||
|
||||
Reference in New Issue
Block a user