Skip to content

Commit 90419df

Browse files
steipeteclawsweeper-repair
andauthored
[codex] Make external CLI credential discovery explicit (#75209)
* refactor(auth): make external CLI discovery explicit * test(auth): update external cli discovery mocks * test(auth): cover scoped external cli auth mocks * [codex] Make external CLI credential discovery explicit --------- Co-authored-by: clawsweeper-repair <clawsweeper-repair@users.noreply.github.com>
1 parent bb3a0c9 commit 90419df

23 files changed

Lines changed: 365 additions & 82 deletions

docs/auth-credential-semantics.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ the target agent signs in separately and creates its own local profile.
8585
- Runtime-only credentials owned by external CLIs are discovered only when the
8686
provider, runtime, or auth profile is in scope for the current operation, or
8787
when a stored local profile for that external source already exists.
88+
- Auth-store callers should choose an explicit external-CLI discovery mode:
89+
`none` for persisted/plugin auth only, `existing` for refreshing already
90+
stored external CLI profiles, or `scoped` for a concrete provider/profile set.
8891
- Read-only/status paths pass `allowKeychainPrompt: false`; they use file-backed
8992
external CLI credentials only and do not read or reuse macOS Keychain results.
9093

extensions/qa-lab/src/model-selection.runtime.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ describe("qa model selection runtime", () => {
4545

4646
expect(resolveQaPreferredLiveModel()).toBe("openai/gpt-5.5");
4747
expect(defaultQaRuntimeModelForMode("live-frontier")).toBe("openai/gpt-5.5");
48+
expect(loadAuthProfileStoreForRuntime).toHaveBeenCalledWith(undefined, {
49+
readOnly: true,
50+
allowKeychainPrompt: false,
51+
externalCliProviderIds: ["openai-codex"],
52+
});
4853
});
4954

5055
it("keeps the OpenAI live default when stored OpenAI profiles are available", () => {

extensions/qa-lab/src/providers/live-frontier/model-selection.runtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export function resolveQaLiveFrontierPreferredModel() {
1414
const store = loadAuthProfileStoreForRuntime(undefined, {
1515
readOnly: true,
1616
allowKeychainPrompt: false,
17+
externalCliProviderIds: ["openai-codex"],
1718
});
1819
if (listProfilesForProvider(store, "openai").length > 0) {
1920
return undefined;

src/agents/auth-profiles.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ export type {
66
export type { AuthProfileEligibilityReasonCode } from "./auth-profiles/order.js";
77
export { resolveAuthProfileDisplayLabel } from "./auth-profiles/display.js";
88
export { formatAuthDoctorHint } from "./auth-profiles/doctor.js";
9+
export {
10+
externalCliDiscoveryExisting,
11+
externalCliDiscoveryForConfigStatus,
12+
externalCliDiscoveryForProviderAuth,
13+
externalCliDiscoveryForProviders,
14+
externalCliDiscoveryNone,
15+
externalCliDiscoveryScoped,
16+
type ExternalCliAuthDiscovery,
17+
} from "./auth-profiles/external-cli-discovery.js";
918
export { resolveApiKeyForProfile } from "./auth-profiles/oauth.js";
1019
export { resolveAuthProfileEligibility, resolveAuthProfileOrder } from "./auth-profiles/order.js";
1120
export {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import type { OpenClawConfig } from "../../config/types.openclaw.js";
2+
import {
3+
resolveExternalCliAuthScopeFromConfig,
4+
type ExternalCliAuthScope,
5+
} from "./external-cli-scope.js";
6+
7+
export type ExternalCliAuthDiscovery =
8+
| {
9+
mode: "none";
10+
allowKeychainPrompt?: false;
11+
config?: OpenClawConfig;
12+
}
13+
| {
14+
mode: "existing";
15+
allowKeychainPrompt?: boolean;
16+
config?: OpenClawConfig;
17+
}
18+
| {
19+
mode: "scoped";
20+
allowKeychainPrompt?: boolean;
21+
config?: OpenClawConfig;
22+
providerIds?: Iterable<string>;
23+
profileIds?: Iterable<string>;
24+
};
25+
26+
type ProviderAuthDiscoveryParams = {
27+
cfg?: OpenClawConfig;
28+
provider: string;
29+
profileId?: string;
30+
preferredProfile?: string;
31+
allowKeychainPrompt?: boolean;
32+
};
33+
34+
type ConfigStatusDiscoveryParams = {
35+
cfg: OpenClawConfig;
36+
allowKeychainPrompt?: false;
37+
};
38+
39+
type ProviderSetDiscoveryParams = {
40+
cfg?: OpenClawConfig;
41+
providers: Iterable<string>;
42+
allowKeychainPrompt?: false;
43+
};
44+
45+
function normalizeStringList(values: Iterable<string | undefined>): string[] {
46+
return [...values]
47+
.map((value) => value?.trim())
48+
.filter((value): value is string => Boolean(value));
49+
}
50+
51+
export function externalCliDiscoveryNone(params?: {
52+
config?: OpenClawConfig;
53+
}): ExternalCliAuthDiscovery {
54+
return {
55+
mode: "none",
56+
allowKeychainPrompt: false,
57+
...(params?.config ? { config: params.config } : {}),
58+
};
59+
}
60+
61+
export function externalCliDiscoveryExisting(params?: {
62+
config?: OpenClawConfig;
63+
allowKeychainPrompt?: boolean;
64+
}): ExternalCliAuthDiscovery {
65+
return {
66+
mode: "existing",
67+
...(params?.allowKeychainPrompt !== undefined
68+
? { allowKeychainPrompt: params.allowKeychainPrompt }
69+
: {}),
70+
...(params?.config ? { config: params.config } : {}),
71+
};
72+
}
73+
74+
export function externalCliDiscoveryScoped(params: {
75+
config?: OpenClawConfig;
76+
providerIds?: Iterable<string>;
77+
profileIds?: Iterable<string>;
78+
allowKeychainPrompt?: boolean;
79+
}): ExternalCliAuthDiscovery {
80+
return {
81+
mode: "scoped",
82+
...(params.allowKeychainPrompt !== undefined
83+
? { allowKeychainPrompt: params.allowKeychainPrompt }
84+
: {}),
85+
...(params.config ? { config: params.config } : {}),
86+
...(params.providerIds ? { providerIds: params.providerIds } : {}),
87+
...(params.profileIds ? { profileIds: params.profileIds } : {}),
88+
};
89+
}
90+
91+
export function externalCliDiscoveryForProviderAuth(
92+
params: ProviderAuthDiscoveryParams,
93+
): ExternalCliAuthDiscovery {
94+
const profileIds = normalizeStringList([params.profileId, params.preferredProfile]);
95+
return externalCliDiscoveryScoped({
96+
config: params.cfg,
97+
allowKeychainPrompt: params.allowKeychainPrompt ?? false,
98+
providerIds: [params.provider],
99+
...(profileIds.length > 0 ? { profileIds } : {}),
100+
});
101+
}
102+
103+
export function externalCliDiscoveryForConfigStatus(
104+
params: ConfigStatusDiscoveryParams,
105+
): ExternalCliAuthDiscovery {
106+
const scope = resolveExternalCliAuthScopeFromConfig(params.cfg);
107+
return externalCliDiscoveryFromScope({
108+
cfg: params.cfg,
109+
scope,
110+
allowKeychainPrompt: params.allowKeychainPrompt ?? false,
111+
});
112+
}
113+
114+
export function externalCliDiscoveryForProviders(
115+
params: ProviderSetDiscoveryParams,
116+
): ExternalCliAuthDiscovery {
117+
const providers = normalizeStringList(params.providers);
118+
if (providers.length === 0) {
119+
return externalCliDiscoveryNone({ config: params.cfg });
120+
}
121+
return externalCliDiscoveryScoped({
122+
config: params.cfg,
123+
allowKeychainPrompt: params.allowKeychainPrompt ?? false,
124+
providerIds: providers,
125+
});
126+
}
127+
128+
function externalCliDiscoveryFromScope(params: {
129+
cfg: OpenClawConfig;
130+
scope: ExternalCliAuthScope | undefined;
131+
allowKeychainPrompt: false;
132+
}): ExternalCliAuthDiscovery {
133+
if (!params.scope) {
134+
return externalCliDiscoveryNone({ config: params.cfg });
135+
}
136+
return externalCliDiscoveryScoped({
137+
config: params.cfg,
138+
allowKeychainPrompt: params.allowKeychainPrompt,
139+
providerIds: params.scope.providerIds,
140+
profileIds: params.scope.profileIds,
141+
});
142+
}

src/agents/auth-profiles/store.ts

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
log,
1111
} from "./constants.js";
1212
import { overlayExternalAuthProfiles, shouldPersistExternalAuthProfile } from "./external-auth.js";
13+
import type { ExternalCliAuthDiscovery } from "./external-cli-discovery.js";
1314
import { isSafeToAdoptMainStoreOAuthIdentity } from "./oauth-shared.js";
1415
import {
1516
ensureAuthStoreFile,
@@ -38,6 +39,7 @@ import type { AuthProfileStore } from "./types.js";
3839
type LoadAuthProfileStoreOptions = {
3940
allowKeychainPrompt?: boolean;
4041
config?: OpenClawConfig;
42+
externalCli?: ExternalCliAuthDiscovery;
4143
readOnly?: boolean;
4244
syncExternalCli?: boolean;
4345
externalCliProviderIds?: Iterable<string>;
@@ -49,6 +51,13 @@ type SaveAuthProfileStoreOptions = {
4951
syncExternalCli?: boolean;
5052
};
5153

54+
type ResolvedExternalCliOverlayOptions = {
55+
allowKeychainPrompt?: boolean;
56+
config?: OpenClawConfig;
57+
externalCliProviderIds?: Iterable<string>;
58+
externalCliProfileIds?: Iterable<string>;
59+
};
60+
5261
const loadedAuthStoreCache = new Map<
5362
string,
5463
{
@@ -183,6 +192,51 @@ function writeCachedAuthProfileStore(params: {
183192
});
184193
}
185194

195+
function resolveExternalCliOverlayOptions(
196+
options: LoadAuthProfileStoreOptions | undefined,
197+
): ResolvedExternalCliOverlayOptions {
198+
const discovery = options?.externalCli;
199+
if (!discovery) {
200+
return {
201+
...(options?.allowKeychainPrompt !== undefined
202+
? { allowKeychainPrompt: options.allowKeychainPrompt }
203+
: {}),
204+
...(options?.config ? { config: options.config } : {}),
205+
...(options?.externalCliProviderIds
206+
? { externalCliProviderIds: options.externalCliProviderIds }
207+
: {}),
208+
...(options?.externalCliProfileIds
209+
? { externalCliProfileIds: options.externalCliProfileIds }
210+
: {}),
211+
};
212+
}
213+
if (discovery.mode === "none") {
214+
const config = discovery.config ?? options?.config;
215+
return {
216+
allowKeychainPrompt: false,
217+
...(config ? { config } : {}),
218+
externalCliProviderIds: [],
219+
externalCliProfileIds: [],
220+
};
221+
}
222+
if (discovery.mode === "existing") {
223+
const allowKeychainPrompt = discovery.allowKeychainPrompt ?? options?.allowKeychainPrompt;
224+
const config = discovery.config ?? options?.config;
225+
return {
226+
...(allowKeychainPrompt !== undefined ? { allowKeychainPrompt } : {}),
227+
...(config ? { config } : {}),
228+
};
229+
}
230+
const allowKeychainPrompt = discovery.allowKeychainPrompt ?? options?.allowKeychainPrompt;
231+
const config = discovery.config ?? options?.config;
232+
return {
233+
...(allowKeychainPrompt !== undefined ? { allowKeychainPrompt } : {}),
234+
...(config ? { config } : {}),
235+
...(discovery.providerIds ? { externalCliProviderIds: discovery.providerIds } : {}),
236+
...(discovery.profileIds ? { externalCliProfileIds: discovery.profileIds } : {}),
237+
};
238+
}
239+
186240
function shouldKeepProfileInLocalStore(params: {
187241
store: AuthProfileStore;
188242
profileId: string;
@@ -384,23 +438,18 @@ export function loadAuthProfileStoreForRuntime(
384438
const store = loadAuthProfileStoreForAgent(agentDir, options);
385439
const authPath = resolveAuthStorePath(agentDir);
386440
const mainAuthPath = resolveAuthStorePath();
441+
const externalCli = resolveExternalCliOverlayOptions(options);
387442
if (!agentDir || authPath === mainAuthPath) {
388443
return overlayExternalAuthProfiles(store, {
389444
agentDir,
390-
allowKeychainPrompt: options?.allowKeychainPrompt,
391-
config: options?.config,
392-
externalCliProviderIds: options?.externalCliProviderIds,
393-
externalCliProfileIds: options?.externalCliProfileIds,
445+
...externalCli,
394446
});
395447
}
396448

397449
const mainStore = loadAuthProfileStoreForAgent(undefined, options);
398450
return overlayExternalAuthProfiles(mergeAuthProfileStores(mainStore, store), {
399451
agentDir,
400-
allowKeychainPrompt: options?.allowKeychainPrompt,
401-
config: options?.config,
402-
externalCliProviderIds: options?.externalCliProviderIds,
403-
externalCliProfileIds: options?.externalCliProfileIds,
452+
...externalCli,
404453
});
405454
}
406455

@@ -426,18 +475,17 @@ export function ensureAuthProfileStore(
426475
options?: {
427476
allowKeychainPrompt?: boolean;
428477
config?: OpenClawConfig;
478+
externalCli?: ExternalCliAuthDiscovery;
429479
externalCliProviderIds?: Iterable<string>;
430480
externalCliProfileIds?: Iterable<string>;
431481
},
432482
): AuthProfileStore {
483+
const externalCli = resolveExternalCliOverlayOptions(options);
433484
return overlayExternalAuthProfiles(
434485
ensureAuthProfileStoreWithoutExternalProfiles(agentDir, options),
435486
{
436487
agentDir,
437-
allowKeychainPrompt: options?.allowKeychainPrompt,
438-
config: options?.config,
439-
externalCliProviderIds: options?.externalCliProviderIds,
440-
externalCliProfileIds: options?.externalCliProfileIds,
488+
...externalCli,
441489
},
442490
);
443491
}

src/agents/cli-runner/prepare.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
1212
import { annotateInterSessionPromptText } from "../../sessions/input-provenance.js";
1313
import { resolveOpenClawAgentDir } from "../agent-paths.js";
1414
import { resolveSessionAgentIds } from "../agent-scope.js";
15+
import { externalCliDiscoveryForProviderAuth } from "../auth-profiles/external-cli-discovery.js";
1516
import { loadAuthProfileStoreForRuntime } from "../auth-profiles/store.js";
1617
import type { AuthProfileCredential } from "../auth-profiles/types.js";
1718
import {
@@ -115,7 +116,10 @@ export async function prepareCliRunContext(
115116
if (effectiveAuthProfileId) {
116117
const authStore = loadAuthProfileStoreForRuntime(agentDir, {
117118
readOnly: true,
118-
allowKeychainPrompt: false,
119+
externalCli: externalCliDiscoveryForProviderAuth({
120+
provider: params.provider,
121+
profileId: effectiveAuthProfileId,
122+
}),
119123
});
120124
authCredential = authStore.profiles[effectiveAuthProfileId];
121125
}

src/agents/codex-native-web-search-core.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { OpenClawConfig } from "../config/types.openclaw.js";
22
import { isRecord } from "../utils.js";
3+
import { externalCliDiscoveryForProviderAuth } from "./auth-profiles/external-cli-discovery.js";
34
import { listProfilesForProvider } from "./auth-profiles/profile-list.js";
45
import { ensureAuthProfileStore } from "./auth-profiles/store.js";
56
import {
@@ -56,7 +57,15 @@ export function hasAvailableCodexAuth(params: {
5657
if (params.agentDir) {
5758
try {
5859
if (
59-
listProfilesForProvider(ensureAuthProfileStore(params.agentDir), "openai-codex").length > 0
60+
listProfilesForProvider(
61+
ensureAuthProfileStore(params.agentDir, {
62+
externalCli: externalCliDiscoveryForProviderAuth({
63+
cfg: params.config,
64+
provider: "openai-codex",
65+
}),
66+
}),
67+
"openai-codex",
68+
).length > 0
6069
) {
6170
return true;
6271
}

src/agents/model-auth-label.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { SessionEntry } from "../config/sessions.js";
22
import type { OpenClawConfig } from "../config/types.openclaw.js";
33
import {
4+
externalCliDiscoveryForProviderAuth,
45
ensureAuthProfileStore,
56
loadAuthProfileStoreWithoutExternalProfiles,
67
resolveAuthProfileDisplayLabel,
@@ -28,7 +29,11 @@ export function resolveModelAuthLabel(params: {
2829
params.includeExternalProfiles === false
2930
? loadAuthProfileStoreWithoutExternalProfiles(params.agentDir)
3031
: ensureAuthProfileStore(params.agentDir, {
31-
allowKeychainPrompt: false,
32+
externalCli: externalCliDiscoveryForProviderAuth({
33+
cfg: params.cfg,
34+
provider: providerKey,
35+
preferredProfile: params.sessionEntry?.authProfileOverride,
36+
}),
3237
});
3338
const profileOverride = params.sessionEntry?.authProfileOverride?.trim();
3439
const order = resolveAuthProfileOrder({

0 commit comments

Comments
 (0)