Skip to content

Commit 8bd24ad

Browse files
clawsweeper[bot]rubencuTakhoffman
authored
fix(codex): preserve plugin tool auth profiles (#83845)
Summary: - This PR threads a Codex-only `toolAuthProfileStore` through embedded runner attempt params, uses it for Code ... struction, forwards auth profiles into plugin-only tools, and adds regression tests plus a changelog entry. - Reproducibility: yes. The linked source PR includes a concrete before-fix negative control and after-fix gat ... urrent-main source inspection shows Codex dynamic tools still receive only the scoped transport auth store. Automerge notes: - PR branch already contained follow-up commit before automerge: test(codex): align dynamic tool auth test helper - PR branch already contained follow-up commit before automerge: fix(codex): expose tool auth to installed harnesses - PR branch already contained follow-up commit before automerge: test(codex): narrow auth store assertions - PR branch already contained follow-up commit before automerge: fix(codex): preserve plugin tool auth profiles Validation: - ClawSweeper review passed for head c226f54. - Required merge gates passed before the squash merge. Prepared head SHA: c226f54 Review: #83845 (comment) Co-authored-by: Rubén Cuevas <hi@rubencu.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent 3ee0342 commit 8bd24ad

8 files changed

Lines changed: 157 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
5757

5858
### Fixes
5959

60+
- Codex app-server: preserve plugin tool auth profiles when Codex owns model transport so OpenClaw dynamic tools can resolve their provider credentials. (#83603) Thanks @rubencu.
6061
- Memory/search: scan the JS-side fallback vector path (used when the sqlite-vec index is unavailable or has a mismatched dimension) in bounded rowid batches and yield to the event loop between batches so large chunk tables can no longer pin the Node.js main thread for multi-second windows. Also keeps the SQL prepared statement rooted in a local so node:sqlite cannot finalize it mid-scan under heap pressure. Fixes #81172. Thanks @dev23xyz-oss.
6162
- Memory Wiki: preserve fs-safe diagnostics when bridge source page writes fail for non-symlink filesystem safety reasons, so directory collisions are reported with the underlying error code. (#83776) Thanks @TurboTheTurtle.
6263
- Telegram: keep forum topics from blocking sibling topic traffic by routing inbound serialization, media/text buffers, and account API queues on topic-aware lanes. (#83829)

extensions/codex/src/app-server/run-attempt.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,69 @@ describe("runCodexAppServerAttempt", () => {
11421142
);
11431143
});
11441144

1145+
it("uses the tool auth profile store for Codex dynamic tool construction", async () => {
1146+
const sessionFile = path.join(tempDir, "session.jsonl");
1147+
const workspaceDir = path.join(tempDir, "workspace");
1148+
const params = createParams(sessionFile, workspaceDir);
1149+
const transportAuthProfileStore = {
1150+
version: 1,
1151+
profiles: {
1152+
"openai-codex:work": {
1153+
provider: "openai-codex",
1154+
type: "oauth",
1155+
access: "transport-token",
1156+
refresh: "transport-refresh",
1157+
expires: Date.now() + 60_000,
1158+
},
1159+
},
1160+
} satisfies EmbeddedRunAttemptParams["authProfileStore"];
1161+
const toolAuthProfileStore = {
1162+
version: 1,
1163+
profiles: {
1164+
"openai-codex:work": {
1165+
provider: "openai-codex",
1166+
type: "oauth",
1167+
access: "transport-token",
1168+
refresh: "transport-refresh",
1169+
expires: Date.now() + 60_000,
1170+
},
1171+
"xai:work": {
1172+
provider: "xai",
1173+
type: "oauth",
1174+
access: "xai-token",
1175+
refresh: "xai-refresh",
1176+
expires: Date.now() + 60_000,
1177+
},
1178+
},
1179+
} satisfies EmbeddedRunAttemptParams["authProfileStore"];
1180+
params.disableTools = false;
1181+
params.authProfileStore = transportAuthProfileStore;
1182+
params.toolAuthProfileStore = toolAuthProfileStore;
1183+
params.runtimePlan = createCodexRuntimePlanFixture();
1184+
const factoryOptions: unknown[] = [];
1185+
testing.setOpenClawCodingToolsFactoryForTests((options) => {
1186+
factoryOptions.push(options);
1187+
return [];
1188+
});
1189+
1190+
await testing.buildDynamicTools({
1191+
params,
1192+
resolvedWorkspace: workspaceDir,
1193+
effectiveWorkspace: workspaceDir,
1194+
sandboxSessionKey: params.sessionKey!,
1195+
sandbox: null as never,
1196+
runAbortController: new AbortController(),
1197+
sessionAgentId: "main",
1198+
pluginConfig: {},
1199+
onYieldDetected: () => undefined,
1200+
});
1201+
1202+
expect(factoryOptions).toHaveLength(1);
1203+
expect((factoryOptions[0] as { authProfileStore?: unknown }).authProfileStore).toBe(
1204+
toolAuthProfileStore,
1205+
);
1206+
});
1207+
11451208
it("keeps canonical OpenAI Codex runs on OpenAI dynamic tool policy", async () => {
11461209
const sessionFile = path.join(tempDir, "session.jsonl");
11471210
const workspaceDir = path.join(tempDir, "workspace");

extensions/codex/src/app-server/run-attempt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3285,7 +3285,7 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
32853285
resolvedWorkspace: input.resolvedWorkspace,
32863286
}),
32873287
config: params.config,
3288-
authProfileStore: params.authProfileStore,
3288+
authProfileStore: params.toolAuthProfileStore ?? params.authProfileStore,
32893289
abortSignal: input.runAbortController.signal,
32903290
emitBeforeToolCallDiagnostics: false,
32913291
modelProvider: params.model.provider,

src/agents/pi-embedded-runner/run.overflow-compaction.test.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
322322
expect(pluginParams.runtimePlan).toBe(runtimePlan);
323323
const authProfileStore = expectRecordFields(pluginParams.authProfileStore, {});
324324
expect(authProfileStore.profiles).toEqual({});
325+
expect(
326+
(pluginParams as { toolAuthProfileStore?: unknown }).toolAuthProfileStore,
327+
).toBeUndefined();
325328
});
326329

327330
it("forwards optional attempt params and the runtime plan into one attempt call", async () => {
@@ -441,6 +444,13 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
441444
provider: "anthropic",
442445
key: "sk-ant",
443446
},
447+
"xai:work": {
448+
type: "oauth" as const,
449+
provider: "xai",
450+
access: "xai-access",
451+
refresh: "xai-refresh",
452+
expires: Date.now() + 60_000,
453+
},
444454
},
445455
};
446456
mockedEnsureAuthProfileStoreWithoutExternalProfiles.mockReturnValueOnce(codexAuthStore);
@@ -487,14 +497,16 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
487497
const harnessParams = mockCallArg(pluginRunAttempt) as {
488498
runtimePlan?: unknown;
489499
authProfileStore?: { profiles?: Record<string, unknown> };
500+
toolAuthProfileStore?: unknown;
490501
};
491502
expect(harnessParams?.runtimePlan).toBe(runtimePlan);
492-
const authProfileStore = expectRecordFields(harnessParams.authProfileStore, {});
493-
const authProfiles = expectRecordFields(authProfileStore.profiles, {});
503+
const forwardedAuthStore = expectRecordFields(harnessParams.authProfileStore, {});
504+
const authProfiles = expectRecordFields(forwardedAuthStore.profiles, {});
494505
expect(Object.keys(authProfiles)).toEqual(["openai-codex:work"]);
495506
expectRecordFields(authProfiles["openai-codex:work"], {
496507
provider: "openai-codex",
497508
});
509+
expect(harnessParams.toolAuthProfileStore).toBe(codexAuthStore);
498510
});
499511

500512
it("forwards OpenAI Codex auth profiles when openai/* is forced through codex", async () => {
@@ -713,6 +725,13 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
713725
refresh: "refresh-token",
714726
expires: Date.now() + 60_000,
715727
},
728+
"xai:work": {
729+
type: "oauth" as const,
730+
provider: "xai",
731+
access: "xai-token",
732+
refresh: "xai-refresh",
733+
expires: Date.now() + 60_000,
734+
},
716735
},
717736
};
718737
clearAgentHarnesses();
@@ -825,6 +844,17 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
825844
forwardedAuthProfileId: "openai-codex:default",
826845
},
827846
});
847+
const harnessParams = mockCallArg(pluginRunAttempt) as {
848+
authProfileStore?: { profiles?: Record<string, unknown> };
849+
toolAuthProfileStore?: unknown;
850+
};
851+
const forwardedAuthStore = expectRecordFields(harnessParams.authProfileStore, {});
852+
const authProfiles = expectRecordFields(forwardedAuthStore.profiles, {});
853+
expect(Object.keys(authProfiles)).toEqual(["openai-codex:default"]);
854+
expectRecordFields(authProfiles["openai-codex:default"], {
855+
provider: "openai-codex",
856+
});
857+
expect(harnessParams.toolAuthProfileStore).toBe(codexAuthStore);
828858
});
829859

830860
it("refreshes bootstrapped Codex OAuth credentials when rotating profiles", async () => {

src/agents/pi-embedded-runner/run.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,6 +1444,9 @@ export async function runEmbeddedPiAgent(
14441444
initialReplayState: accumulatedReplayState,
14451445
authStorage,
14461446
authProfileStore: runAttemptAuthProfileStore,
1447+
// Codex builds OpenClaw tools inside its harness. Keep transport
1448+
// auth scoped while letting tool construction see plugin creds.
1449+
toolAuthProfileStore: agentHarness.id === "codex" ? attemptAuthProfileStore : undefined,
14471450
modelRegistry,
14481451
agentId: workspaceResolution.agentId,
14491452
legacyBeforeAgentStartResult,

src/agents/pi-embedded-runner/run/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ export type EmbeddedRunAttemptParams = EmbeddedRunAttemptBase & {
5858
authStorage: AuthStorage;
5959
/** Auth profile store already resolved during startup for this attempt. */
6060
authProfileStore: AuthProfileStore;
61+
/**
62+
* Full auth profile store for OpenClaw tool availability.
63+
* Plugin-owned harnesses may scope `authProfileStore` to model transport credentials.
64+
*/
65+
toolAuthProfileStore?: AuthProfileStore;
6166
modelRegistry: ModelRegistry;
6267
thinkLevel: ThinkLevel;
6368
legacyBeforeAgentStartResult?: PluginHookBeforeAgentStartResult;

src/agents/pi-tools.create-openclaw-coding-tools.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
resetGlobalHookRunner,
1313
} from "../plugins/hook-runner-global.js";
1414
import { createMockPluginRegistry } from "../plugins/hooks.test-helpers.js";
15+
import type { AuthProfileStore } from "./auth-profiles/types.js";
1516
import "./test-helpers/fast-bash-tools.js";
1617
import "./test-helpers/fast-coding-tools.js";
1718
import "./test-helpers/fast-openclaw-tools.js";
@@ -522,6 +523,56 @@ describe("createOpenClawCodingTools", () => {
522523
}
523524
});
524525

526+
it("forwards auth profiles to plugin-only tool construction", () => {
527+
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
528+
createOpenClawToolsMock.mockClear();
529+
const resolvePluginToolsSpy = vi
530+
.spyOn(openClawPluginTools, "resolveOpenClawPluginToolsForOptions")
531+
.mockReturnValue([]);
532+
const authProfileStore = {
533+
version: 1,
534+
order: { xai: ["xai-oauth"] },
535+
profiles: {
536+
"xai-oauth": {
537+
type: "oauth",
538+
provider: "xai",
539+
access: "xai-oauth-access-token", // pragma: allowlist secret
540+
refresh: "xai-oauth-refresh-token", // pragma: allowlist secret
541+
expires: Date.now() + 60_000,
542+
},
543+
},
544+
} satisfies AuthProfileStore;
545+
546+
try {
547+
createOpenClawCodingTools({
548+
config: {
549+
auth: {
550+
order: {
551+
xai: ["xai-oauth"],
552+
},
553+
},
554+
},
555+
authProfileStore,
556+
includeCoreTools: false,
557+
runtimeToolAllowlist: ["x_search"],
558+
toolConstructionPlan: {
559+
includeBaseCodingTools: false,
560+
includeShellTools: false,
561+
includeChannelTools: false,
562+
includeOpenClawTools: false,
563+
includePluginTools: true,
564+
},
565+
});
566+
567+
expect(createOpenClawToolsMock).not.toHaveBeenCalled();
568+
expect(resolvePluginToolsSpy).toHaveBeenCalledTimes(1);
569+
const pluginToolOptions = resolvePluginToolsSpy.mock.calls[0]?.[0].options;
570+
expect(pluginToolOptions?.authProfileStore).toBe(authProfileStore);
571+
} finally {
572+
resolvePluginToolsSpy.mockRestore();
573+
}
574+
});
575+
525576
it("uses tools.alsoAllow for optional plugin discovery without widening to all plugins", () => {
526577
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
527578
createOpenClawToolsMock.mockClear();

src/agents/pi-tools.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,7 @@ export function createOpenClawCodingTools(options?: {
866866
disableMessageTool: options?.disableMessageTool,
867867
requesterAgentIdOverride: agentId,
868868
allowGatewaySubagentBinding: options?.allowGatewaySubagentBinding,
869+
authProfileStore: options?.authProfileStore,
869870
},
870871
resolvedConfig: options?.config,
871872
});

0 commit comments

Comments
 (0)