mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 04:40:43 +00:00
[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>
This commit is contained in:
committed by
GitHub
parent
bb3a0c9545
commit
90419df663
@@ -85,6 +85,9 @@ the target agent signs in separately and creates its own local profile.
|
||||
- Runtime-only credentials owned by external CLIs are discovered only when the
|
||||
provider, runtime, or auth profile is in scope for the current operation, or
|
||||
when a stored local profile for that external source already exists.
|
||||
- Auth-store callers should choose an explicit external-CLI discovery mode:
|
||||
`none` for persisted/plugin auth only, `existing` for refreshing already
|
||||
stored external CLI profiles, or `scoped` for a concrete provider/profile set.
|
||||
- Read-only/status paths pass `allowKeychainPrompt: false`; they use file-backed
|
||||
external CLI credentials only and do not read or reuse macOS Keychain results.
|
||||
|
||||
|
||||
@@ -45,6 +45,11 @@ describe("qa model selection runtime", () => {
|
||||
|
||||
expect(resolveQaPreferredLiveModel()).toBe("openai/gpt-5.5");
|
||||
expect(defaultQaRuntimeModelForMode("live-frontier")).toBe("openai/gpt-5.5");
|
||||
expect(loadAuthProfileStoreForRuntime).toHaveBeenCalledWith(undefined, {
|
||||
readOnly: true,
|
||||
allowKeychainPrompt: false,
|
||||
externalCliProviderIds: ["openai-codex"],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the OpenAI live default when stored OpenAI profiles are available", () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ export function resolveQaLiveFrontierPreferredModel() {
|
||||
const store = loadAuthProfileStoreForRuntime(undefined, {
|
||||
readOnly: true,
|
||||
allowKeychainPrompt: false,
|
||||
externalCliProviderIds: ["openai-codex"],
|
||||
});
|
||||
if (listProfilesForProvider(store, "openai").length > 0) {
|
||||
return undefined;
|
||||
|
||||
@@ -6,6 +6,15 @@ export type {
|
||||
export type { AuthProfileEligibilityReasonCode } from "./auth-profiles/order.js";
|
||||
export { resolveAuthProfileDisplayLabel } from "./auth-profiles/display.js";
|
||||
export { formatAuthDoctorHint } from "./auth-profiles/doctor.js";
|
||||
export {
|
||||
externalCliDiscoveryExisting,
|
||||
externalCliDiscoveryForConfigStatus,
|
||||
externalCliDiscoveryForProviderAuth,
|
||||
externalCliDiscoveryForProviders,
|
||||
externalCliDiscoveryNone,
|
||||
externalCliDiscoveryScoped,
|
||||
type ExternalCliAuthDiscovery,
|
||||
} from "./auth-profiles/external-cli-discovery.js";
|
||||
export { resolveApiKeyForProfile } from "./auth-profiles/oauth.js";
|
||||
export { resolveAuthProfileEligibility, resolveAuthProfileOrder } from "./auth-profiles/order.js";
|
||||
export {
|
||||
|
||||
142
src/agents/auth-profiles/external-cli-discovery.ts
Normal file
142
src/agents/auth-profiles/external-cli-discovery.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import {
|
||||
resolveExternalCliAuthScopeFromConfig,
|
||||
type ExternalCliAuthScope,
|
||||
} from "./external-cli-scope.js";
|
||||
|
||||
export type ExternalCliAuthDiscovery =
|
||||
| {
|
||||
mode: "none";
|
||||
allowKeychainPrompt?: false;
|
||||
config?: OpenClawConfig;
|
||||
}
|
||||
| {
|
||||
mode: "existing";
|
||||
allowKeychainPrompt?: boolean;
|
||||
config?: OpenClawConfig;
|
||||
}
|
||||
| {
|
||||
mode: "scoped";
|
||||
allowKeychainPrompt?: boolean;
|
||||
config?: OpenClawConfig;
|
||||
providerIds?: Iterable<string>;
|
||||
profileIds?: Iterable<string>;
|
||||
};
|
||||
|
||||
type ProviderAuthDiscoveryParams = {
|
||||
cfg?: OpenClawConfig;
|
||||
provider: string;
|
||||
profileId?: string;
|
||||
preferredProfile?: string;
|
||||
allowKeychainPrompt?: boolean;
|
||||
};
|
||||
|
||||
type ConfigStatusDiscoveryParams = {
|
||||
cfg: OpenClawConfig;
|
||||
allowKeychainPrompt?: false;
|
||||
};
|
||||
|
||||
type ProviderSetDiscoveryParams = {
|
||||
cfg?: OpenClawConfig;
|
||||
providers: Iterable<string>;
|
||||
allowKeychainPrompt?: false;
|
||||
};
|
||||
|
||||
function normalizeStringList(values: Iterable<string | undefined>): string[] {
|
||||
return [...values]
|
||||
.map((value) => value?.trim())
|
||||
.filter((value): value is string => Boolean(value));
|
||||
}
|
||||
|
||||
export function externalCliDiscoveryNone(params?: {
|
||||
config?: OpenClawConfig;
|
||||
}): ExternalCliAuthDiscovery {
|
||||
return {
|
||||
mode: "none",
|
||||
allowKeychainPrompt: false,
|
||||
...(params?.config ? { config: params.config } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function externalCliDiscoveryExisting(params?: {
|
||||
config?: OpenClawConfig;
|
||||
allowKeychainPrompt?: boolean;
|
||||
}): ExternalCliAuthDiscovery {
|
||||
return {
|
||||
mode: "existing",
|
||||
...(params?.allowKeychainPrompt !== undefined
|
||||
? { allowKeychainPrompt: params.allowKeychainPrompt }
|
||||
: {}),
|
||||
...(params?.config ? { config: params.config } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function externalCliDiscoveryScoped(params: {
|
||||
config?: OpenClawConfig;
|
||||
providerIds?: Iterable<string>;
|
||||
profileIds?: Iterable<string>;
|
||||
allowKeychainPrompt?: boolean;
|
||||
}): ExternalCliAuthDiscovery {
|
||||
return {
|
||||
mode: "scoped",
|
||||
...(params.allowKeychainPrompt !== undefined
|
||||
? { allowKeychainPrompt: params.allowKeychainPrompt }
|
||||
: {}),
|
||||
...(params.config ? { config: params.config } : {}),
|
||||
...(params.providerIds ? { providerIds: params.providerIds } : {}),
|
||||
...(params.profileIds ? { profileIds: params.profileIds } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function externalCliDiscoveryForProviderAuth(
|
||||
params: ProviderAuthDiscoveryParams,
|
||||
): ExternalCliAuthDiscovery {
|
||||
const profileIds = normalizeStringList([params.profileId, params.preferredProfile]);
|
||||
return externalCliDiscoveryScoped({
|
||||
config: params.cfg,
|
||||
allowKeychainPrompt: params.allowKeychainPrompt ?? false,
|
||||
providerIds: [params.provider],
|
||||
...(profileIds.length > 0 ? { profileIds } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function externalCliDiscoveryForConfigStatus(
|
||||
params: ConfigStatusDiscoveryParams,
|
||||
): ExternalCliAuthDiscovery {
|
||||
const scope = resolveExternalCliAuthScopeFromConfig(params.cfg);
|
||||
return externalCliDiscoveryFromScope({
|
||||
cfg: params.cfg,
|
||||
scope,
|
||||
allowKeychainPrompt: params.allowKeychainPrompt ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
export function externalCliDiscoveryForProviders(
|
||||
params: ProviderSetDiscoveryParams,
|
||||
): ExternalCliAuthDiscovery {
|
||||
const providers = normalizeStringList(params.providers);
|
||||
if (providers.length === 0) {
|
||||
return externalCliDiscoveryNone({ config: params.cfg });
|
||||
}
|
||||
return externalCliDiscoveryScoped({
|
||||
config: params.cfg,
|
||||
allowKeychainPrompt: params.allowKeychainPrompt ?? false,
|
||||
providerIds: providers,
|
||||
});
|
||||
}
|
||||
|
||||
function externalCliDiscoveryFromScope(params: {
|
||||
cfg: OpenClawConfig;
|
||||
scope: ExternalCliAuthScope | undefined;
|
||||
allowKeychainPrompt: false;
|
||||
}): ExternalCliAuthDiscovery {
|
||||
if (!params.scope) {
|
||||
return externalCliDiscoveryNone({ config: params.cfg });
|
||||
}
|
||||
return externalCliDiscoveryScoped({
|
||||
config: params.cfg,
|
||||
allowKeychainPrompt: params.allowKeychainPrompt,
|
||||
providerIds: params.scope.providerIds,
|
||||
profileIds: params.scope.profileIds,
|
||||
});
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
log,
|
||||
} from "./constants.js";
|
||||
import { overlayExternalAuthProfiles, shouldPersistExternalAuthProfile } from "./external-auth.js";
|
||||
import type { ExternalCliAuthDiscovery } from "./external-cli-discovery.js";
|
||||
import { isSafeToAdoptMainStoreOAuthIdentity } from "./oauth-shared.js";
|
||||
import {
|
||||
ensureAuthStoreFile,
|
||||
@@ -38,6 +39,7 @@ import type { AuthProfileStore } from "./types.js";
|
||||
type LoadAuthProfileStoreOptions = {
|
||||
allowKeychainPrompt?: boolean;
|
||||
config?: OpenClawConfig;
|
||||
externalCli?: ExternalCliAuthDiscovery;
|
||||
readOnly?: boolean;
|
||||
syncExternalCli?: boolean;
|
||||
externalCliProviderIds?: Iterable<string>;
|
||||
@@ -49,6 +51,13 @@ type SaveAuthProfileStoreOptions = {
|
||||
syncExternalCli?: boolean;
|
||||
};
|
||||
|
||||
type ResolvedExternalCliOverlayOptions = {
|
||||
allowKeychainPrompt?: boolean;
|
||||
config?: OpenClawConfig;
|
||||
externalCliProviderIds?: Iterable<string>;
|
||||
externalCliProfileIds?: Iterable<string>;
|
||||
};
|
||||
|
||||
const loadedAuthStoreCache = new Map<
|
||||
string,
|
||||
{
|
||||
@@ -183,6 +192,51 @@ function writeCachedAuthProfileStore(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveExternalCliOverlayOptions(
|
||||
options: LoadAuthProfileStoreOptions | undefined,
|
||||
): ResolvedExternalCliOverlayOptions {
|
||||
const discovery = options?.externalCli;
|
||||
if (!discovery) {
|
||||
return {
|
||||
...(options?.allowKeychainPrompt !== undefined
|
||||
? { allowKeychainPrompt: options.allowKeychainPrompt }
|
||||
: {}),
|
||||
...(options?.config ? { config: options.config } : {}),
|
||||
...(options?.externalCliProviderIds
|
||||
? { externalCliProviderIds: options.externalCliProviderIds }
|
||||
: {}),
|
||||
...(options?.externalCliProfileIds
|
||||
? { externalCliProfileIds: options.externalCliProfileIds }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
if (discovery.mode === "none") {
|
||||
const config = discovery.config ?? options?.config;
|
||||
return {
|
||||
allowKeychainPrompt: false,
|
||||
...(config ? { config } : {}),
|
||||
externalCliProviderIds: [],
|
||||
externalCliProfileIds: [],
|
||||
};
|
||||
}
|
||||
if (discovery.mode === "existing") {
|
||||
const allowKeychainPrompt = discovery.allowKeychainPrompt ?? options?.allowKeychainPrompt;
|
||||
const config = discovery.config ?? options?.config;
|
||||
return {
|
||||
...(allowKeychainPrompt !== undefined ? { allowKeychainPrompt } : {}),
|
||||
...(config ? { config } : {}),
|
||||
};
|
||||
}
|
||||
const allowKeychainPrompt = discovery.allowKeychainPrompt ?? options?.allowKeychainPrompt;
|
||||
const config = discovery.config ?? options?.config;
|
||||
return {
|
||||
...(allowKeychainPrompt !== undefined ? { allowKeychainPrompt } : {}),
|
||||
...(config ? { config } : {}),
|
||||
...(discovery.providerIds ? { externalCliProviderIds: discovery.providerIds } : {}),
|
||||
...(discovery.profileIds ? { externalCliProfileIds: discovery.profileIds } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function shouldKeepProfileInLocalStore(params: {
|
||||
store: AuthProfileStore;
|
||||
profileId: string;
|
||||
@@ -384,23 +438,18 @@ export function loadAuthProfileStoreForRuntime(
|
||||
const store = loadAuthProfileStoreForAgent(agentDir, options);
|
||||
const authPath = resolveAuthStorePath(agentDir);
|
||||
const mainAuthPath = resolveAuthStorePath();
|
||||
const externalCli = resolveExternalCliOverlayOptions(options);
|
||||
if (!agentDir || authPath === mainAuthPath) {
|
||||
return overlayExternalAuthProfiles(store, {
|
||||
agentDir,
|
||||
allowKeychainPrompt: options?.allowKeychainPrompt,
|
||||
config: options?.config,
|
||||
externalCliProviderIds: options?.externalCliProviderIds,
|
||||
externalCliProfileIds: options?.externalCliProfileIds,
|
||||
...externalCli,
|
||||
});
|
||||
}
|
||||
|
||||
const mainStore = loadAuthProfileStoreForAgent(undefined, options);
|
||||
return overlayExternalAuthProfiles(mergeAuthProfileStores(mainStore, store), {
|
||||
agentDir,
|
||||
allowKeychainPrompt: options?.allowKeychainPrompt,
|
||||
config: options?.config,
|
||||
externalCliProviderIds: options?.externalCliProviderIds,
|
||||
externalCliProfileIds: options?.externalCliProfileIds,
|
||||
...externalCli,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -426,18 +475,17 @@ export function ensureAuthProfileStore(
|
||||
options?: {
|
||||
allowKeychainPrompt?: boolean;
|
||||
config?: OpenClawConfig;
|
||||
externalCli?: ExternalCliAuthDiscovery;
|
||||
externalCliProviderIds?: Iterable<string>;
|
||||
externalCliProfileIds?: Iterable<string>;
|
||||
},
|
||||
): AuthProfileStore {
|
||||
const externalCli = resolveExternalCliOverlayOptions(options);
|
||||
return overlayExternalAuthProfiles(
|
||||
ensureAuthProfileStoreWithoutExternalProfiles(agentDir, options),
|
||||
{
|
||||
agentDir,
|
||||
allowKeychainPrompt: options?.allowKeychainPrompt,
|
||||
config: options?.config,
|
||||
externalCliProviderIds: options?.externalCliProviderIds,
|
||||
externalCliProfileIds: options?.externalCliProfileIds,
|
||||
...externalCli,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import { annotateInterSessionPromptText } from "../../sessions/input-provenance.js";
|
||||
import { resolveOpenClawAgentDir } from "../agent-paths.js";
|
||||
import { resolveSessionAgentIds } from "../agent-scope.js";
|
||||
import { externalCliDiscoveryForProviderAuth } from "../auth-profiles/external-cli-discovery.js";
|
||||
import { loadAuthProfileStoreForRuntime } from "../auth-profiles/store.js";
|
||||
import type { AuthProfileCredential } from "../auth-profiles/types.js";
|
||||
import {
|
||||
@@ -115,7 +116,10 @@ export async function prepareCliRunContext(
|
||||
if (effectiveAuthProfileId) {
|
||||
const authStore = loadAuthProfileStoreForRuntime(agentDir, {
|
||||
readOnly: true,
|
||||
allowKeychainPrompt: false,
|
||||
externalCli: externalCliDiscoveryForProviderAuth({
|
||||
provider: params.provider,
|
||||
profileId: effectiveAuthProfileId,
|
||||
}),
|
||||
});
|
||||
authCredential = authStore.profiles[effectiveAuthProfileId];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { externalCliDiscoveryForProviderAuth } from "./auth-profiles/external-cli-discovery.js";
|
||||
import { listProfilesForProvider } from "./auth-profiles/profile-list.js";
|
||||
import { ensureAuthProfileStore } from "./auth-profiles/store.js";
|
||||
import {
|
||||
@@ -56,7 +57,15 @@ export function hasAvailableCodexAuth(params: {
|
||||
if (params.agentDir) {
|
||||
try {
|
||||
if (
|
||||
listProfilesForProvider(ensureAuthProfileStore(params.agentDir), "openai-codex").length > 0
|
||||
listProfilesForProvider(
|
||||
ensureAuthProfileStore(params.agentDir, {
|
||||
externalCli: externalCliDiscoveryForProviderAuth({
|
||||
cfg: params.config,
|
||||
provider: "openai-codex",
|
||||
}),
|
||||
}),
|
||||
"openai-codex",
|
||||
).length > 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
externalCliDiscoveryForProviderAuth,
|
||||
ensureAuthProfileStore,
|
||||
loadAuthProfileStoreWithoutExternalProfiles,
|
||||
resolveAuthProfileDisplayLabel,
|
||||
@@ -28,7 +29,11 @@ export function resolveModelAuthLabel(params: {
|
||||
params.includeExternalProfiles === false
|
||||
? loadAuthProfileStoreWithoutExternalProfiles(params.agentDir)
|
||||
: ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
externalCli: externalCliDiscoveryForProviderAuth({
|
||||
cfg: params.cfg,
|
||||
provider: providerKey,
|
||||
preferredProfile: params.sessionEntry?.authProfileOverride,
|
||||
}),
|
||||
});
|
||||
const profileOverride = params.sessionEntry?.authProfileOverride?.trim();
|
||||
const order = resolveAuthProfileOrder({
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
|
||||
import {
|
||||
type AuthProfileStore,
|
||||
externalCliDiscoveryForProviderAuth,
|
||||
ensureAuthProfileStore,
|
||||
listProfilesForProvider,
|
||||
resolveApiKeyForProfile,
|
||||
@@ -496,14 +497,8 @@ function resolveScopedAuthProfileStore(params: {
|
||||
profileId?: string;
|
||||
preferredProfile?: string;
|
||||
}): AuthProfileStore {
|
||||
const profileIds = [params.profileId, params.preferredProfile]
|
||||
.map((value) => value?.trim())
|
||||
.filter((value): value is string => Boolean(value));
|
||||
return ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
config: params.cfg,
|
||||
externalCliProviderIds: [params.provider],
|
||||
...(profileIds.length > 0 ? { externalCliProfileIds: profileIds } : {}),
|
||||
externalCli: externalCliDiscoveryForProviderAuth(params),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { sanitizeForLog } from "../terminal/ansi.js";
|
||||
import { externalCliDiscoveryForProviders } from "./auth-profiles/external-cli-discovery.js";
|
||||
import { hasAnyAuthProfileStoreSource } from "./auth-profiles/source-check.js";
|
||||
import type { AuthProfileStore } from "./auth-profiles/types.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
||||
@@ -383,7 +384,10 @@ function resolveFallbackSoonestCooldownExpiry(params: {
|
||||
// cooldowns through a separate store instance while the fallback loop runs.
|
||||
const refreshedStore = params.authRuntime.loadAuthProfileStoreForRuntime(params.agentDir, {
|
||||
readOnly: true,
|
||||
allowKeychainPrompt: false,
|
||||
externalCli: externalCliDiscoveryForProviders({
|
||||
cfg: params.cfg,
|
||||
providers: params.candidates.map((candidate) => candidate.provider),
|
||||
}),
|
||||
});
|
||||
let soonest: number | null = null;
|
||||
for (const candidate of params.candidates) {
|
||||
@@ -772,7 +776,12 @@ export async function runWithModelFallback<T>(params: {
|
||||
? await loadModelFallbackAuthRuntime()
|
||||
: null;
|
||||
const authStore = authRuntime
|
||||
? authRuntime.ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false })
|
||||
? authRuntime.ensureAuthProfileStore(params.agentDir, {
|
||||
externalCli: externalCliDiscoveryForProviders({
|
||||
cfg: params.cfg,
|
||||
providers: candidates.map((candidate) => candidate.provider),
|
||||
}),
|
||||
})
|
||||
: null;
|
||||
const attempts: FallbackAttempt[] = [];
|
||||
let lastError: unknown;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
externalCliDiscoveryForProviderAuth,
|
||||
ensureAuthProfileStore,
|
||||
listProfilesForProvider,
|
||||
type AuthProfileStore,
|
||||
@@ -29,7 +30,7 @@ export function hasAuthForModelProvider(params: {
|
||||
const store =
|
||||
params.store ??
|
||||
ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
externalCli: externalCliDiscoveryForProviderAuth({ cfg: params.cfg, provider }),
|
||||
});
|
||||
if (listProfilesForProvider(store, provider).length > 0) {
|
||||
return true;
|
||||
@@ -43,9 +44,6 @@ export function createProviderAuthChecker(params: {
|
||||
agentDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): (provider: string) => boolean {
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const authCache = new Map<string, boolean>();
|
||||
return (provider: string) => {
|
||||
const key = normalizeProviderId(provider);
|
||||
@@ -59,7 +57,6 @@ export function createProviderAuthChecker(params: {
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentDir: params.agentDir,
|
||||
env: params.env,
|
||||
store,
|
||||
});
|
||||
authCache.set(key, value);
|
||||
return value;
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
markAuthProfileGood,
|
||||
markAuthProfileUsed,
|
||||
} from "../auth-profiles.js";
|
||||
import { externalCliDiscoveryForProviderAuth } from "../auth-profiles/external-cli-discovery.js";
|
||||
import {
|
||||
resolveSessionKeyForRequest,
|
||||
resolveStoredSessionKeyForSessionId,
|
||||
@@ -509,7 +510,11 @@ export async function runEmbeddedPiAgent(
|
||||
const authStore = pluginHarnessOwnsTransport
|
||||
? createEmptyAuthProfileStore()
|
||||
: ensureAuthProfileStore(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
externalCli: externalCliDiscoveryForProviderAuth({
|
||||
cfg: params.config,
|
||||
provider,
|
||||
preferredProfile: params.authProfileId,
|
||||
}),
|
||||
});
|
||||
const requestedProfileId = params.authProfileId?.trim();
|
||||
const resolvePluginHarnessPreferredProfileId = (): string | undefined => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { AgentModelConfig } from "../../config/types.agents-shared.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import {
|
||||
externalCliDiscoveryForProviderAuth,
|
||||
ensureAuthProfileStore,
|
||||
hasAnyAuthProfileStoreSource,
|
||||
listProfilesForProvider,
|
||||
@@ -46,7 +47,7 @@ export function hasAuthForProvider(params: { provider: string; agentDir?: string
|
||||
return false;
|
||||
}
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
externalCli: externalCliDiscoveryForProviderAuth({ provider: params.provider }),
|
||||
});
|
||||
return listProfilesForProvider(store, params.provider).length > 0;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ vi.mock("../../agents/auth-profiles.js", () => ({
|
||||
clearRuntimeAuthProfileStoreSnapshots: () => {
|
||||
authProfilesStoreMock.profiles = {};
|
||||
},
|
||||
externalCliDiscoveryForProviderAuth: () => ({
|
||||
mode: "scoped",
|
||||
allowKeychainPrompt: false,
|
||||
}),
|
||||
ensureAuthProfileStore: () => ({
|
||||
version: 1,
|
||||
profiles: authProfilesStoreMock.profiles,
|
||||
@@ -501,6 +505,8 @@ describe("/model chat UX", () => {
|
||||
try {
|
||||
await withEnvAsync(
|
||||
{
|
||||
ANTHROPIC_API_KEY: undefined,
|
||||
ANTHROPIC_OAUTH_TOKEN: undefined,
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
WORKSPACE_MODEL_CREDENTIALS: credentialPath,
|
||||
|
||||
@@ -30,6 +30,10 @@ const ensureAuthProfileStore = vi.hoisted(() =>
|
||||
const listProfilesForProvider = vi.hoisted(() => vi.fn(() => []));
|
||||
const upsertAuthProfile = vi.hoisted(() => vi.fn());
|
||||
vi.mock("../agents/auth-profiles.js", () => ({
|
||||
externalCliDiscoveryForProviderAuth: () => ({
|
||||
mode: "scoped",
|
||||
allowKeychainPrompt: false,
|
||||
}),
|
||||
ensureAuthProfileStore,
|
||||
listProfilesForProvider,
|
||||
upsertAuthProfile,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { resolveAgentDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||
import {
|
||||
type AuthProfileStore,
|
||||
externalCliDiscoveryForProviderAuth,
|
||||
ensureAuthProfileStore,
|
||||
resolveAuthStatePathForDisplay,
|
||||
setAuthProfileOrder,
|
||||
@@ -48,9 +49,9 @@ export async function modelsAuthOrderGetCommand(
|
||||
opts: { provider: string; agent?: string; json?: boolean },
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const { agentId, agentDir, provider } = await resolveAuthOrderContext(opts, runtime);
|
||||
const { cfg, agentId, agentDir, provider } = await resolveAuthOrderContext(opts, runtime);
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
externalCli: externalCliDiscoveryForProviderAuth({ cfg, provider }),
|
||||
});
|
||||
const order = describeOrder(store, provider);
|
||||
|
||||
@@ -94,10 +95,10 @@ export async function modelsAuthOrderSetCommand(
|
||||
opts: { provider: string; agent?: string; order: string[] },
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const { agentId, agentDir, provider } = await resolveAuthOrderContext(opts, runtime);
|
||||
const { cfg, agentId, agentDir, provider } = await resolveAuthOrderContext(opts, runtime);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
externalCli: externalCliDiscoveryForProviderAuth({ cfg, provider }),
|
||||
});
|
||||
const providerKey = provider;
|
||||
const requested = normalizeStringEntries(opts.order ?? []);
|
||||
|
||||
@@ -367,7 +367,13 @@ describe("modelsAuthLoginCommand", () => {
|
||||
|
||||
await modelsAuthLoginCommand({ provider: "openai-codex" }, runtime);
|
||||
|
||||
expect(mocks.loadAuthProfileStoreForRuntime).toHaveBeenCalledWith("/tmp/openclaw/agents/main");
|
||||
expect(mocks.loadAuthProfileStoreForRuntime).toHaveBeenCalledWith("/tmp/openclaw/agents/main", {
|
||||
externalCli: {
|
||||
mode: "scoped",
|
||||
allowKeychainPrompt: false,
|
||||
providerIds: ["openai-codex"],
|
||||
},
|
||||
});
|
||||
expect(mocks.clearAuthProfileCooldown).toHaveBeenCalledWith({
|
||||
store: fakeStore,
|
||||
profileId: "openai-codex:user@example.com",
|
||||
@@ -418,7 +424,16 @@ describe("modelsAuthLoginCommand", () => {
|
||||
|
||||
expect(mocks.resolveDefaultAgentId).not.toHaveBeenCalled();
|
||||
expect(mocks.resolveAgentDir).toHaveBeenCalledWith(originalConfig, "coder");
|
||||
expect(mocks.loadAuthProfileStoreForRuntime).toHaveBeenCalledWith("/tmp/openclaw/agents/coder");
|
||||
expect(mocks.loadAuthProfileStoreForRuntime).toHaveBeenCalledWith(
|
||||
"/tmp/openclaw/agents/coder",
|
||||
{
|
||||
externalCli: {
|
||||
mode: "scoped",
|
||||
allowKeychainPrompt: false,
|
||||
providerIds: ["openai-codex"],
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(runProviderAuth).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentDir: "/tmp/openclaw/agents/coder",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { externalCliDiscoveryForProviderAuth } from "../../agents/auth-profiles.js";
|
||||
import { listProfilesForProvider, upsertAuthProfile } from "../../agents/auth-profiles/profiles.js";
|
||||
import { loadAuthProfileStoreForRuntime } from "../../agents/auth-profiles/store.js";
|
||||
import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js";
|
||||
@@ -559,7 +560,9 @@ type LoginOptions = {
|
||||
*/
|
||||
async function clearStaleProfileLockouts(provider: string, agentDir: string): Promise<void> {
|
||||
try {
|
||||
const store = loadAuthProfileStoreForRuntime(agentDir);
|
||||
const store = loadAuthProfileStoreForRuntime(agentDir, {
|
||||
externalCli: externalCliDiscoveryForProviderAuth({ provider }),
|
||||
});
|
||||
const profileIds = listProfilesForProvider(store, provider);
|
||||
for (const profileId of profileIds) {
|
||||
await clearAuthProfileCooldown({ store, profileId, agentDir });
|
||||
|
||||
@@ -74,6 +74,10 @@ vi.mock("./shared.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/auth-profiles.js", () => ({
|
||||
externalCliDiscoveryScoped: (params: Record<string, unknown> = {}) => ({
|
||||
mode: "scoped",
|
||||
...params,
|
||||
}),
|
||||
ensureAuthProfileStore: (agentDir?: string) =>
|
||||
agentDir === "/tmp/coder-agent" && mockAgentStore ? mockAgentStore : mockStore,
|
||||
listProfilesForProvider: (store: AuthProfileStore, provider: string) =>
|
||||
@@ -400,27 +404,29 @@ describe("buildProbeTargets reason codes", () => {
|
||||
order: {},
|
||||
};
|
||||
|
||||
const defaultPlan = await buildProbeTargets({
|
||||
cfg: {} as OpenClawConfig,
|
||||
providers: ["anthropic"],
|
||||
modelCandidates: ["anthropic/claude-sonnet-4-6"],
|
||||
options: {
|
||||
timeoutMs: 5_000,
|
||||
concurrency: 1,
|
||||
maxTokens: 16,
|
||||
},
|
||||
});
|
||||
const agentPlan = await buildProbeTargets({
|
||||
cfg: {} as OpenClawConfig,
|
||||
agentDir: "/tmp/coder-agent",
|
||||
providers: ["anthropic"],
|
||||
modelCandidates: ["anthropic/claude-sonnet-4-6"],
|
||||
options: {
|
||||
timeoutMs: 5_000,
|
||||
concurrency: 1,
|
||||
maxTokens: 16,
|
||||
},
|
||||
});
|
||||
const { defaultPlan, agentPlan } = await withClearedAnthropicEnv(async () => ({
|
||||
defaultPlan: await buildProbeTargets({
|
||||
cfg: {} as OpenClawConfig,
|
||||
providers: ["anthropic"],
|
||||
modelCandidates: ["anthropic/claude-sonnet-4-6"],
|
||||
options: {
|
||||
timeoutMs: 5_000,
|
||||
concurrency: 1,
|
||||
maxTokens: 16,
|
||||
},
|
||||
}),
|
||||
agentPlan: await buildProbeTargets({
|
||||
cfg: {} as OpenClawConfig,
|
||||
agentDir: "/tmp/coder-agent",
|
||||
providers: ["anthropic"],
|
||||
modelCandidates: ["anthropic/claude-sonnet-4-6"],
|
||||
options: {
|
||||
timeoutMs: 5_000,
|
||||
concurrency: 1,
|
||||
maxTokens: 16,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
expect(defaultPlan.targets).toEqual([]);
|
||||
expect(agentPlan.results).toEqual([]);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/ag
|
||||
import {
|
||||
type AuthProfileCredential,
|
||||
type AuthProfileEligibilityReasonCode,
|
||||
externalCliDiscoveryScoped,
|
||||
ensureAuthProfileStore,
|
||||
listProfilesForProvider,
|
||||
resolveAuthProfileDisplayLabel,
|
||||
@@ -256,7 +257,14 @@ export async function buildProbeTargets(params: {
|
||||
options: AuthProbeOptions;
|
||||
}): Promise<{ targets: AuthProbeTarget[]; results: AuthProbeResult[] }> {
|
||||
const { cfg, agentDir, providers, modelCandidates, options, workspaceDir } = params;
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
externalCli: externalCliDiscoveryScoped({
|
||||
config: cfg,
|
||||
allowKeychainPrompt: false,
|
||||
providerIds: providers,
|
||||
profileIds: options.profileIds,
|
||||
}),
|
||||
});
|
||||
const providerFilter = options.provider?.trim();
|
||||
const providerFilterKey = providerFilter ? normalizeProviderId(providerFilter) : null;
|
||||
const profileFilter = new Set((options.profileIds ?? []).map((id) => id.trim()).filter(Boolean));
|
||||
|
||||
@@ -24,9 +24,15 @@ vi.mock("../../agents/agent-paths.js", () => ({
|
||||
resolveOpenClawAgentDir: mocks.resolveOpenClawAgentDir,
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/auth-profiles.js", () => ({
|
||||
ensureAuthProfileStore: mocks.ensureAuthProfileStore,
|
||||
}));
|
||||
vi.mock("../../agents/auth-profiles.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../agents/auth-profiles.js")>(
|
||||
"../../agents/auth-profiles.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
ensureAuthProfileStore: mocks.ensureAuthProfileStore,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../agents/auth-health.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../agents/auth-health.js")>(
|
||||
@@ -219,28 +225,31 @@ describe("models.authStatus", () => {
|
||||
expect(mocks.ensureAuthProfileStore).toHaveBeenCalledWith(
|
||||
"/tmp/agent",
|
||||
expect.objectContaining({
|
||||
allowKeychainPrompt: false,
|
||||
config: expect.any(Object),
|
||||
externalCliProviderIds: expect.arrayContaining(["opencode-go"]),
|
||||
externalCliProfileIds: ["opencode-go:default"],
|
||||
externalCli: expect.objectContaining({
|
||||
mode: "scoped",
|
||||
allowKeychainPrompt: false,
|
||||
config: expect.any(Object),
|
||||
providerIds: expect.arrayContaining(["opencode-go"]),
|
||||
profileIds: ["opencode-go:default"],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const [, options] = mocks.ensureAuthProfileStore.mock.calls[0] ?? [];
|
||||
expect((options as { externalCliProviderIds?: string[] }).externalCliProviderIds).not.toContain(
|
||||
"claude-cli",
|
||||
);
|
||||
const externalCli = (options as { externalCli?: { providerIds?: string[] } }).externalCli;
|
||||
expect(externalCli?.providerIds).not.toContain("claude-cli");
|
||||
});
|
||||
|
||||
it("keeps the auth store overlay unscoped when config has no provider signal", async () => {
|
||||
it("disables external CLI auth overlays when config has no provider signal", async () => {
|
||||
await handler(createOptions());
|
||||
|
||||
expect(mocks.ensureAuthProfileStore).toHaveBeenCalledWith(
|
||||
"/tmp/agent",
|
||||
expect.objectContaining({
|
||||
allowKeychainPrompt: false,
|
||||
config: expect.any(Object),
|
||||
externalCliProviderIds: undefined,
|
||||
externalCliProfileIds: undefined,
|
||||
externalCli: expect.objectContaining({
|
||||
mode: "none",
|
||||
allowKeychainPrompt: false,
|
||||
config: expect.any(Object),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
buildAuthHealthSummary,
|
||||
formatRemainingShort,
|
||||
} from "../../agents/auth-health.js";
|
||||
import { ensureAuthProfileStore } from "../../agents/auth-profiles.js";
|
||||
import { resolveExternalCliAuthScopeFromConfig } from "../../agents/auth-profiles/external-cli-scope.js";
|
||||
import {
|
||||
ensureAuthProfileStore,
|
||||
externalCliDiscoveryForConfigStatus,
|
||||
} from "../../agents/auth-profiles.js";
|
||||
import { normalizeProviderId } from "../../agents/provider-id.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { isSecretRef } from "../../config/types.secrets.js";
|
||||
@@ -293,12 +295,8 @@ export const modelsAuthStatusHandlers: GatewayRequestHandlers = {
|
||||
try {
|
||||
const cfg = context.getRuntimeConfig();
|
||||
const agentDir = resolveOpenClawAgentDir();
|
||||
const externalCliAuthScope = resolveExternalCliAuthScopeFromConfig(cfg);
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
config: cfg,
|
||||
externalCliProviderIds: externalCliAuthScope?.providerIds,
|
||||
externalCliProfileIds: externalCliAuthScope?.profileIds,
|
||||
externalCli: externalCliDiscoveryForConfigStatus({ cfg }),
|
||||
});
|
||||
const configured = resolveConfiguredProviders(cfg);
|
||||
const authHealth: AuthHealthSummary = buildAuthHealthSummary({
|
||||
|
||||
Reference in New Issue
Block a user