mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:20:43 +00:00
refactor(auth): route codex runtimes through canonical oauth
This commit is contained in:
committed by
Peter Steinberger
parent
f98e98ab66
commit
859eb06662
100
extensions/codex/src/app-server/auth-bridge.test.ts
Normal file
100
extensions/codex/src/app-server/auth-bridge.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
ensureAuthProfileStore: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
|
||||
ensureAuthProfileStore: mocks.ensureAuthProfileStore,
|
||||
}));
|
||||
|
||||
let bridgeCodexAppServerStartOptions: typeof import("./auth-bridge.js").bridgeCodexAppServerStartOptions;
|
||||
|
||||
describe("bridgeCodexAppServerStartOptions", () => {
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
({ bridgeCodexAppServerStartOptions } = await import("./auth-bridge.js"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
mocks.ensureAuthProfileStore.mockReset();
|
||||
await Promise.all(
|
||||
tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
it("bridges canonical OpenClaw oauth into an isolated CODEX_HOME", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
||||
tempDirs.push(agentDir);
|
||||
mocks.ensureAuthProfileStore.mockReturnValue({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai-codex:default": {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
accountId: "acct-123",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await bridgeCodexAppServerStartOptions({
|
||||
startOptions: {
|
||||
command: "codex",
|
||||
args: ["app-server"],
|
||||
headers: { authorization: "Bearer dev-token" },
|
||||
env: { EXISTING: "1" },
|
||||
clearEnv: ["FOO"],
|
||||
},
|
||||
agentDir,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
env: {
|
||||
EXISTING: "1",
|
||||
CODEX_HOME: expect.stringContaining(path.join(agentDir, "harness-auth", "codex")),
|
||||
},
|
||||
clearEnv: expect.arrayContaining(["FOO", "OPENAI_API_KEY"]),
|
||||
});
|
||||
|
||||
const authFile = JSON.parse(
|
||||
await fs.readFile(path.join(result.env?.CODEX_HOME ?? "", "auth.json"), "utf8"),
|
||||
);
|
||||
expect(authFile).toEqual({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
access_token: "access-token",
|
||||
refresh_token: "refresh-token",
|
||||
account_id: "acct-123",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves start options unchanged when canonical oauth is unavailable", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
||||
tempDirs.push(agentDir);
|
||||
const startOptions = {
|
||||
command: "codex",
|
||||
args: ["app-server"],
|
||||
headers: { authorization: "Bearer dev-token" },
|
||||
};
|
||||
mocks.ensureAuthProfileStore.mockReturnValue({
|
||||
version: 1,
|
||||
profiles: {},
|
||||
});
|
||||
|
||||
await expect(
|
||||
bridgeCodexAppServerStartOptions({
|
||||
startOptions,
|
||||
agentDir,
|
||||
authProfileId: "openai-codex:missing",
|
||||
}),
|
||||
).resolves.toEqual(startOptions);
|
||||
});
|
||||
});
|
||||
76
extensions/codex/src/app-server/auth-bridge.ts
Normal file
76
extensions/codex/src/app-server/auth-bridge.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { ensureAuthProfileStore, type OAuthCredential } from "openclaw/plugin-sdk/provider-auth";
|
||||
import type { CodexAppServerStartOptions } from "./config.js";
|
||||
|
||||
const DEFAULT_CODEX_AUTH_PROFILE_ID = "openai-codex:default";
|
||||
const CODEX_AUTH_ENV_CLEAR_KEYS = ["OPENAI_API_KEY"] as const;
|
||||
|
||||
function isBridgeableCodexOAuthCredential(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" &&
|
||||
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: {
|
||||
access_token: credential.access,
|
||||
refresh_token: credential.refresh,
|
||||
...(credential.accountId ? { account_id: credential.accountId } : {}),
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
export async function bridgeCodexAppServerStartOptions(params: {
|
||||
startOptions: CodexAppServerStartOptions;
|
||||
agentDir: string;
|
||||
authProfileId?: string;
|
||||
}): Promise<CodexAppServerStartOptions> {
|
||||
const profileId = params.authProfileId?.trim() || DEFAULT_CODEX_AUTH_PROFILE_ID;
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const credential = store.profiles[profileId];
|
||||
if (!isBridgeableCodexOAuthCredential(credential)) {
|
||||
return params.startOptions;
|
||||
}
|
||||
|
||||
const codexHome = resolveCodexBridgeHome(params.agentDir, profileId);
|
||||
await fs.mkdir(codexHome, { recursive: true });
|
||||
await fs.writeFile(path.join(codexHome, "auth.json"), buildCodexAuthFile(credential));
|
||||
|
||||
return {
|
||||
...params.startOptions,
|
||||
env: {
|
||||
...params.startOptions.env,
|
||||
CODEX_HOME: codexHome,
|
||||
},
|
||||
clearEnv: Array.from(
|
||||
new Set([...(params.startOptions.clearEnv ?? []), ...CODEX_AUTH_ENV_CLEAR_KEYS]),
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -105,6 +105,38 @@ describe("maybeCompactCodexAppServerSession", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reuses the bound auth profile for native compaction", async () => {
|
||||
const fake = createFakeCodexClient();
|
||||
let seenAuthProfileId: string | undefined;
|
||||
__testing.setCodexAppServerClientFactoryForTests(async (_startOptions, authProfileId) => {
|
||||
seenAuthProfileId = authProfileId;
|
||||
return fake.client;
|
||||
});
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
await writeCodexAppServerBinding(sessionFile, {
|
||||
threadId: "thread-1",
|
||||
cwd: tempDir,
|
||||
authProfileId: "openai-codex:work",
|
||||
});
|
||||
|
||||
const pendingResult = maybeCompactCodexAppServerSession({
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
sessionFile,
|
||||
workspaceDir: tempDir,
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(fake.request).toHaveBeenCalledWith("thread/compact/start", { threadId: "thread-1" });
|
||||
});
|
||||
fake.emit({
|
||||
method: "thread/compacted",
|
||||
params: { threadId: "thread-1", turnId: "turn-1" },
|
||||
});
|
||||
await pendingResult;
|
||||
|
||||
expect(seenAuthProfileId).toBe("openai-codex:work");
|
||||
});
|
||||
});
|
||||
|
||||
function createFakeCodexClient(): {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getSharedCodexAppServerClient } from "./shared-client.js";
|
||||
|
||||
type CodexAppServerClientFactory = (
|
||||
startOptions?: CodexAppServerStartOptions,
|
||||
authProfileId?: string,
|
||||
) => Promise<CodexAppServerClient>;
|
||||
type CodexNativeCompactionCompletion = {
|
||||
signal: "thread/compacted" | "item/completed";
|
||||
@@ -25,8 +26,8 @@ type CodexNativeCompactionWaiter = {
|
||||
|
||||
const DEFAULT_CODEX_COMPACTION_WAIT_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
|
||||
let clientFactory: CodexAppServerClientFactory = (startOptions) =>
|
||||
getSharedCodexAppServerClient({ startOptions });
|
||||
let clientFactory: CodexAppServerClientFactory = (startOptions, authProfileId) =>
|
||||
getSharedCodexAppServerClient({ startOptions, authProfileId });
|
||||
|
||||
export async function maybeCompactCodexAppServerSession(
|
||||
params: CompactEmbeddedPiSessionParams,
|
||||
@@ -38,7 +39,7 @@ export async function maybeCompactCodexAppServerSession(
|
||||
return { ok: false, compacted: false, reason: "no codex app-server thread binding" };
|
||||
}
|
||||
|
||||
const client = await clientFactory(appServer.start);
|
||||
const client = await clientFactory(appServer.start, binding.authProfileId);
|
||||
const waiter = createCodexNativeCompactionWaiter(client, binding.threadId);
|
||||
let completion: CodexNativeCompactionCompletion;
|
||||
try {
|
||||
@@ -212,6 +213,7 @@ export const __testing = {
|
||||
clientFactory = factory;
|
||||
},
|
||||
resetCodexAppServerClientFactoryForTests(): void {
|
||||
clientFactory = (startOptions) => getSharedCodexAppServerClient({ startOptions });
|
||||
clientFactory = (startOptions, authProfileId) =>
|
||||
getSharedCodexAppServerClient({ startOptions, authProfileId });
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -12,6 +12,8 @@ export type CodexAppServerStartOptions = {
|
||||
url?: string;
|
||||
authToken?: string;
|
||||
headers: Record<string, string>;
|
||||
env?: Record<string, string>;
|
||||
clearEnv?: string[];
|
||||
};
|
||||
|
||||
export type CodexAppServerRuntimeOptions = {
|
||||
@@ -158,6 +160,8 @@ export function codexAppServerStartOptionsKey(options: CodexAppServerStartOption
|
||||
headers: Object.entries(options.headers).toSorted(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
),
|
||||
env: Object.entries(options.env ?? {}).toSorted(([left], [right]) => left.localeCompare(right)),
|
||||
clearEnv: [...(options.clearEnv ?? [])].toSorted(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export type CodexAppServerListModelsOptions = {
|
||||
includeHidden?: boolean;
|
||||
timeoutMs?: number;
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
authProfileId?: string;
|
||||
sharedClient?: boolean;
|
||||
};
|
||||
|
||||
@@ -40,10 +41,12 @@ export async function listCodexAppServerModels(
|
||||
? await getSharedCodexAppServerClient({
|
||||
startOptions: options.startOptions,
|
||||
timeoutMs,
|
||||
authProfileId: options.authProfileId,
|
||||
})
|
||||
: await createIsolatedCodexAppServerClient({
|
||||
startOptions: options.startOptions,
|
||||
timeoutMs,
|
||||
authProfileId: options.authProfileId,
|
||||
});
|
||||
try {
|
||||
const response = await client.request<JsonObject>(
|
||||
|
||||
@@ -8,6 +8,7 @@ export async function requestCodexAppServerJson<T = JsonValue | undefined>(param
|
||||
requestParams?: JsonValue;
|
||||
timeoutMs?: number;
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
authProfileId?: string;
|
||||
}): Promise<T> {
|
||||
const timeoutMs = params.timeoutMs ?? 60_000;
|
||||
return await withTimeout(
|
||||
@@ -15,6 +16,7 @@ export async function requestCodexAppServerJson<T = JsonValue | undefined>(param
|
||||
const client = await getSharedCodexAppServerClient({
|
||||
startOptions: params.startOptions,
|
||||
timeoutMs,
|
||||
authProfileId: params.authProfileId,
|
||||
});
|
||||
return await client.request<T>(params.method, params.requestParams, { timeoutMs });
|
||||
})(),
|
||||
|
||||
@@ -313,6 +313,53 @@ describe("runCodexAppServerAttempt", () => {
|
||||
expect(queueAgentHarnessMessage("session-1", "after timeout")).toBe(false);
|
||||
});
|
||||
|
||||
it("passes the selected auth profile into app-server startup", async () => {
|
||||
const seenAuthProfileIds: Array<string | undefined> = [];
|
||||
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
|
||||
__testing.setCodexAppServerClientFactoryForTests(async (_startOptions, authProfileId) => {
|
||||
seenAuthProfileIds.push(authProfileId);
|
||||
return {
|
||||
request: async (method: string) => {
|
||||
if (method === "thread/start") {
|
||||
return {
|
||||
thread: { id: "thread-1" },
|
||||
model: "gpt-5.4-codex",
|
||||
modelProvider: "openai",
|
||||
};
|
||||
}
|
||||
if (method === "turn/start") {
|
||||
return { turn: { id: "turn-1", status: "inProgress" } };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
addNotificationHandler: (handler: typeof notify) => {
|
||||
notify = handler;
|
||||
return () => undefined;
|
||||
},
|
||||
addRequestHandler: () => () => undefined,
|
||||
} as never;
|
||||
});
|
||||
const params = createParams(
|
||||
path.join(tempDir, "session.jsonl"),
|
||||
path.join(tempDir, "workspace"),
|
||||
);
|
||||
params.authProfileId = "openai-codex:work";
|
||||
|
||||
const run = runCodexAppServerAttempt(params);
|
||||
await vi.waitFor(() => expect(seenAuthProfileIds).toEqual(["openai-codex:work"]));
|
||||
await notify({
|
||||
method: "turn/completed",
|
||||
params: {
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
turn: { id: "turn-1", status: "completed" },
|
||||
},
|
||||
});
|
||||
await run;
|
||||
|
||||
expect(seenAuthProfileIds).toEqual(["openai-codex:work"]);
|
||||
});
|
||||
|
||||
it("times out turn start before the active run handle is installed", async () => {
|
||||
const request = vi.fn(
|
||||
async (method: string, _params?: unknown, options?: { timeoutMs?: number }) => {
|
||||
|
||||
@@ -37,10 +37,11 @@ import { mirrorCodexAppServerTranscript } from "./transcript-mirror.js";
|
||||
|
||||
type CodexAppServerClientFactory = (
|
||||
startOptions?: CodexAppServerStartOptions,
|
||||
authProfileId?: string,
|
||||
) => Promise<CodexAppServerClient>;
|
||||
|
||||
let clientFactory: CodexAppServerClientFactory = (startOptions) =>
|
||||
getSharedCodexAppServerClient({ startOptions });
|
||||
let clientFactory: CodexAppServerClientFactory = (startOptions, authProfileId) =>
|
||||
getSharedCodexAppServerClient({ startOptions, authProfileId });
|
||||
|
||||
export async function runCodexAppServerAttempt(
|
||||
params: EmbeddedRunAttemptParams,
|
||||
@@ -101,7 +102,7 @@ export async function runCodexAppServerAttempt(
|
||||
timeoutMs: params.timeoutMs,
|
||||
signal: runAbortController.signal,
|
||||
operation: async () => {
|
||||
const startupClient = await clientFactory(appServer.start);
|
||||
const startupClient = await clientFactory(appServer.start, params.authProfileId);
|
||||
const startupThread = await startOrResumeThread({
|
||||
client: startupClient,
|
||||
params,
|
||||
@@ -487,6 +488,7 @@ export const __testing = {
|
||||
clientFactory = factory;
|
||||
},
|
||||
resetCodexAppServerClientFactoryForTests(): void {
|
||||
clientFactory = (startOptions) => getSharedCodexAppServerClient({ startOptions });
|
||||
clientFactory = (startOptions, authProfileId) =>
|
||||
getSharedCodexAppServerClient({ startOptions, authProfileId });
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -6,6 +6,7 @@ export type CodexAppServerThreadBinding = {
|
||||
threadId: string;
|
||||
sessionFile: string;
|
||||
cwd: string;
|
||||
authProfileId?: string;
|
||||
model?: string;
|
||||
modelProvider?: string;
|
||||
dynamicToolsFingerprint?: string;
|
||||
@@ -41,6 +42,7 @@ export async function readCodexAppServerBinding(
|
||||
threadId: parsed.threadId,
|
||||
sessionFile,
|
||||
cwd: typeof parsed.cwd === "string" ? parsed.cwd : "",
|
||||
authProfileId: typeof parsed.authProfileId === "string" ? parsed.authProfileId : undefined,
|
||||
model: typeof parsed.model === "string" ? parsed.model : undefined,
|
||||
modelProvider: typeof parsed.modelProvider === "string" ? parsed.modelProvider : undefined,
|
||||
dynamicToolsFingerprint:
|
||||
@@ -71,6 +73,7 @@ export async function writeCodexAppServerBinding(
|
||||
sessionFile,
|
||||
threadId: binding.threadId,
|
||||
cwd: binding.cwd,
|
||||
authProfileId: binding.authProfileId,
|
||||
model: binding.model,
|
||||
modelProvider: binding.modelProvider,
|
||||
dynamicToolsFingerprint: binding.dynamicToolsFingerprint,
|
||||
|
||||
@@ -1,14 +1,35 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { CodexAppServerClient, MIN_CODEX_APP_SERVER_VERSION } from "./client.js";
|
||||
import { listCodexAppServerModels } from "./models.js";
|
||||
import { resetSharedCodexAppServerClientForTests } from "./shared-client.js";
|
||||
import { createClientHarness } from "./test-support.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
bridgeCodexAppServerStartOptions: vi.fn(async ({ startOptions }) => startOptions),
|
||||
resolveOpenClawAgentDir: vi.fn(() => "/tmp/openclaw-agent"),
|
||||
}));
|
||||
|
||||
vi.mock("./auth-bridge.js", () => ({
|
||||
bridgeCodexAppServerStartOptions: mocks.bridgeCodexAppServerStartOptions,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
|
||||
resolveOpenClawAgentDir: mocks.resolveOpenClawAgentDir,
|
||||
}));
|
||||
|
||||
let listCodexAppServerModels: typeof import("./models.js").listCodexAppServerModels;
|
||||
let resetSharedCodexAppServerClientForTests: typeof import("./shared-client.js").resetSharedCodexAppServerClientForTests;
|
||||
|
||||
describe("shared Codex app-server client", () => {
|
||||
beforeAll(async () => {
|
||||
({ listCodexAppServerModels } = await import("./models.js"));
|
||||
({ resetSharedCodexAppServerClientForTests } = await import("./shared-client.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetSharedCodexAppServerClientForTests();
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
mocks.bridgeCodexAppServerStartOptions.mockClear();
|
||||
mocks.resolveOpenClawAgentDir.mockClear();
|
||||
});
|
||||
|
||||
it("closes the shared app-server when the version gate fails", async () => {
|
||||
@@ -18,6 +39,7 @@ describe("shared Codex app-server client", () => {
|
||||
// Model discovery uses the shared-client path, which owns child teardown
|
||||
// when initialize discovers an unsupported app-server.
|
||||
const listPromise = listCodexAppServerModels({ timeoutMs: 1000 });
|
||||
await vi.waitFor(() => expect(harness.writes.length).toBeGreaterThanOrEqual(1));
|
||||
const initialize = JSON.parse(harness.writes[0] ?? "{}") as { id?: number };
|
||||
harness.send({
|
||||
id: initialize.id,
|
||||
@@ -45,6 +67,7 @@ describe("shared Codex app-server client", () => {
|
||||
expect(first.process.kill).toHaveBeenCalledTimes(1);
|
||||
|
||||
const secondList = listCodexAppServerModels({ timeoutMs: 1000 });
|
||||
await vi.waitFor(() => expect(second.writes.length).toBeGreaterThanOrEqual(1));
|
||||
const initialize = JSON.parse(second.writes[0] ?? "{}") as { id?: number };
|
||||
second.send({
|
||||
id: initialize.id,
|
||||
@@ -57,4 +80,30 @@ describe("shared Codex app-server client", () => {
|
||||
await expect(secondList).resolves.toEqual({ models: [] });
|
||||
expect(startSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("passes the selected auth profile through the bridge helper", async () => {
|
||||
const harness = createClientHarness();
|
||||
vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
|
||||
|
||||
const listPromise = listCodexAppServerModels({
|
||||
timeoutMs: 1000,
|
||||
authProfileId: "openai-codex:work",
|
||||
});
|
||||
await vi.waitFor(() => expect(harness.writes.length).toBeGreaterThanOrEqual(1));
|
||||
const initialize = JSON.parse(harness.writes[0] ?? "{}") as { id?: number };
|
||||
harness.send({
|
||||
id: initialize.id,
|
||||
result: { userAgent: "openclaw/0.118.0 (macOS; test)" },
|
||||
});
|
||||
await vi.waitFor(() => expect(harness.writes.length).toBeGreaterThanOrEqual(3));
|
||||
const modelList = JSON.parse(harness.writes[2] ?? "{}") as { id?: number };
|
||||
harness.send({ id: modelList.id, result: { data: [] } });
|
||||
|
||||
await expect(listPromise).resolves.toEqual({ models: [] });
|
||||
expect(mocks.bridgeCodexAppServerStartOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
authProfileId: "openai-codex:work",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { resolveOpenClawAgentDir } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { bridgeCodexAppServerStartOptions } from "./auth-bridge.js";
|
||||
import { CodexAppServerClient } from "./client.js";
|
||||
import {
|
||||
codexAppServerStartOptionsKey,
|
||||
@@ -25,9 +27,14 @@ function getSharedCodexAppServerClientState(): SharedCodexAppServerClientState {
|
||||
export async function getSharedCodexAppServerClient(options?: {
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
timeoutMs?: number;
|
||||
authProfileId?: string;
|
||||
}): Promise<CodexAppServerClient> {
|
||||
const state = getSharedCodexAppServerClientState();
|
||||
const startOptions = options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
|
||||
const startOptions = await bridgeCodexAppServerStartOptions({
|
||||
startOptions: options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start,
|
||||
agentDir: resolveOpenClawAgentDir(),
|
||||
authProfileId: options?.authProfileId,
|
||||
});
|
||||
const key = codexAppServerStartOptionsKey(startOptions);
|
||||
if (state.key && state.key !== key) {
|
||||
clearSharedCodexAppServerClient();
|
||||
@@ -62,8 +69,13 @@ export async function getSharedCodexAppServerClient(options?: {
|
||||
export async function createIsolatedCodexAppServerClient(options?: {
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
timeoutMs?: number;
|
||||
authProfileId?: string;
|
||||
}): Promise<CodexAppServerClient> {
|
||||
const startOptions = options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
|
||||
const startOptions = await bridgeCodexAppServerStartOptions({
|
||||
startOptions: options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start,
|
||||
agentDir: resolveOpenClawAgentDir(),
|
||||
authProfileId: options?.authProfileId,
|
||||
});
|
||||
const client = CodexAppServerClient.start(startOptions);
|
||||
const initialize = client.initialize();
|
||||
try {
|
||||
|
||||
@@ -53,6 +53,7 @@ export async function startOrResumeThread(params: {
|
||||
await writeCodexAppServerBinding(params.params.sessionFile, {
|
||||
threadId: response.thread.id,
|
||||
cwd: params.cwd,
|
||||
authProfileId: params.params.authProfileId,
|
||||
model: params.params.modelId,
|
||||
modelProvider: response.modelProvider ?? normalizeModelProvider(params.params.provider),
|
||||
dynamicToolsFingerprint,
|
||||
@@ -62,6 +63,7 @@ export async function startOrResumeThread(params: {
|
||||
...binding,
|
||||
threadId: response.thread.id,
|
||||
cwd: params.cwd,
|
||||
authProfileId: params.params.authProfileId,
|
||||
model: params.params.modelId,
|
||||
modelProvider: response.modelProvider ?? normalizeModelProvider(params.params.provider),
|
||||
dynamicToolsFingerprint,
|
||||
@@ -93,6 +95,7 @@ export async function startOrResumeThread(params: {
|
||||
await writeCodexAppServerBinding(params.params.sessionFile, {
|
||||
threadId: response.thread.id,
|
||||
cwd: params.cwd,
|
||||
authProfileId: params.params.authProfileId,
|
||||
model: response.model ?? params.params.modelId,
|
||||
modelProvider: response.modelProvider ?? normalizeModelProvider(params.params.provider),
|
||||
dynamicToolsFingerprint,
|
||||
@@ -103,6 +106,7 @@ export async function startOrResumeThread(params: {
|
||||
threadId: response.thread.id,
|
||||
sessionFile: params.params.sessionFile,
|
||||
cwd: params.cwd,
|
||||
authProfileId: params.params.authProfileId,
|
||||
model: response.model ?? params.params.modelId,
|
||||
modelProvider: response.modelProvider ?? normalizeModelProvider(params.params.provider),
|
||||
dynamicToolsFingerprint,
|
||||
|
||||
@@ -3,8 +3,15 @@ import type { CodexAppServerStartOptions } from "./config.js";
|
||||
import type { CodexAppServerTransport } from "./transport.js";
|
||||
|
||||
export function createStdioTransport(options: CodexAppServerStartOptions): CodexAppServerTransport {
|
||||
const env = {
|
||||
...process.env,
|
||||
...options.env,
|
||||
};
|
||||
for (const key of options.clearEnv ?? []) {
|
||||
delete env[key];
|
||||
}
|
||||
return spawn(options.command, options.args, {
|
||||
env: process.env,
|
||||
env,
|
||||
detached: process.platform !== "win32",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user