mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-17 21:10:54 +00:00
refactor: move provider auth-choice helpers into plugins
This commit is contained in:
@@ -1,5 +1 @@
|
||||
export {
|
||||
resolveProviderPluginChoice,
|
||||
runProviderModelSelectedHook,
|
||||
} from "../plugins/provider-wizard.js";
|
||||
export { resolvePluginProviders } from "../plugins/providers.js";
|
||||
export * from "../plugins/provider-auth-choice.runtime.js";
|
||||
|
||||
@@ -13,7 +13,7 @@ const resolveProviderPluginChoice = vi.hoisted(() =>
|
||||
vi.fn<() => { provider: ProviderPlugin; method: ProviderAuthMethod } | null>(),
|
||||
);
|
||||
const runProviderModelSelectedHook = vi.hoisted(() => vi.fn(async () => {}));
|
||||
vi.mock("./auth-choice.apply.plugin-provider.runtime.js", () => ({
|
||||
vi.mock("../plugins/provider-auth-choice.runtime.js", () => ({
|
||||
resolvePluginProviders,
|
||||
resolveProviderPluginChoice,
|
||||
runProviderModelSelectedHook,
|
||||
@@ -49,20 +49,17 @@ vi.mock("../plugins/provider-auth-helpers.js", () => ({
|
||||
}));
|
||||
|
||||
const isRemoteEnvironment = vi.hoisted(() => vi.fn(() => false));
|
||||
vi.mock("./oauth-env.js", () => ({
|
||||
const openUrl = vi.hoisted(() => vi.fn(async () => {}));
|
||||
vi.mock("../plugins/setup-browser.js", () => ({
|
||||
isRemoteEnvironment,
|
||||
openUrl,
|
||||
}));
|
||||
|
||||
const createVpsAwareOAuthHandlers = vi.hoisted(() => vi.fn());
|
||||
vi.mock("./oauth-flow.js", () => ({
|
||||
vi.mock("../plugins/provider-oauth-flow.js", () => ({
|
||||
createVpsAwareOAuthHandlers,
|
||||
}));
|
||||
|
||||
const openUrl = vi.hoisted(() => vi.fn(async () => {}));
|
||||
vi.mock("./onboard-helpers.js", () => ({
|
||||
openUrl,
|
||||
}));
|
||||
|
||||
function buildProvider(): ProviderPlugin {
|
||||
return {
|
||||
id: "ollama",
|
||||
|
||||
@@ -1,295 +1 @@
|
||||
import { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
|
||||
import {
|
||||
resolveDefaultAgentId,
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { upsertAuthProfile } from "../agents/auth-profiles.js";
|
||||
import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js";
|
||||
import { enablePluginInConfig } from "../plugins/enable.js";
|
||||
import { applyAuthProfileConfig } from "../plugins/provider-auth-helpers.js";
|
||||
import type { ProviderAuthMethod, ProviderAuthOptionBag } from "../plugins/types.js";
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import { isRemoteEnvironment } from "./oauth-env.js";
|
||||
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
|
||||
import { openUrl } from "./onboard-helpers.js";
|
||||
import type { OnboardOptions } from "./onboard-types.js";
|
||||
import {
|
||||
applyDefaultModel,
|
||||
mergeConfigPatch,
|
||||
pickAuthMethod,
|
||||
resolveProviderMatch,
|
||||
} from "./provider-auth-helpers.js";
|
||||
|
||||
export type PluginProviderAuthChoiceOptions = {
|
||||
authChoice: string;
|
||||
pluginId: string;
|
||||
providerId: string;
|
||||
methodId?: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
function restoreConfiguredPrimaryModel(
|
||||
nextConfig: ApplyAuthChoiceParams["config"],
|
||||
originalConfig: ApplyAuthChoiceParams["config"],
|
||||
): ApplyAuthChoiceParams["config"] {
|
||||
const originalModel = originalConfig.agents?.defaults?.model;
|
||||
const nextAgents = nextConfig.agents;
|
||||
const nextDefaults = nextAgents?.defaults;
|
||||
if (!nextDefaults) {
|
||||
return nextConfig;
|
||||
}
|
||||
if (originalModel !== undefined) {
|
||||
return {
|
||||
...nextConfig,
|
||||
agents: {
|
||||
...nextAgents,
|
||||
defaults: {
|
||||
...nextDefaults,
|
||||
model: originalModel,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const { model: _model, ...restDefaults } = nextDefaults;
|
||||
return {
|
||||
...nextConfig,
|
||||
agents: {
|
||||
...nextAgents,
|
||||
defaults: restDefaults,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function loadPluginProviderRuntime() {
|
||||
return import("./auth-choice.apply.plugin-provider.runtime.js");
|
||||
}
|
||||
|
||||
export async function runProviderPluginAuthMethod(params: {
|
||||
config: ApplyAuthChoiceParams["config"];
|
||||
runtime: ApplyAuthChoiceParams["runtime"];
|
||||
prompter: ApplyAuthChoiceParams["prompter"];
|
||||
method: ProviderAuthMethod;
|
||||
agentDir?: string;
|
||||
agentId?: string;
|
||||
workspaceDir?: string;
|
||||
emitNotes?: boolean;
|
||||
secretInputMode?: OnboardOptions["secretInputMode"];
|
||||
allowSecretRefPrompt?: boolean;
|
||||
opts?: Partial<OnboardOptions>;
|
||||
}): Promise<{ config: ApplyAuthChoiceParams["config"]; defaultModel?: string }> {
|
||||
const agentId = params.agentId ?? resolveDefaultAgentId(params.config);
|
||||
const defaultAgentId = resolveDefaultAgentId(params.config);
|
||||
const agentDir =
|
||||
params.agentDir ??
|
||||
(agentId === defaultAgentId
|
||||
? resolveOpenClawAgentDir()
|
||||
: resolveAgentDir(params.config, agentId));
|
||||
const workspaceDir =
|
||||
params.workspaceDir ??
|
||||
resolveAgentWorkspaceDir(params.config, agentId) ??
|
||||
resolveDefaultAgentWorkspaceDir();
|
||||
|
||||
const isRemote = isRemoteEnvironment();
|
||||
const result = await params.method.run({
|
||||
config: params.config,
|
||||
agentDir,
|
||||
workspaceDir,
|
||||
prompter: params.prompter,
|
||||
runtime: params.runtime,
|
||||
opts: params.opts as ProviderAuthOptionBag | undefined,
|
||||
secretInputMode: params.secretInputMode,
|
||||
allowSecretRefPrompt: params.allowSecretRefPrompt,
|
||||
isRemote,
|
||||
openUrl: async (url) => {
|
||||
await openUrl(url);
|
||||
},
|
||||
oauth: {
|
||||
createVpsAwareHandlers: (opts) => createVpsAwareOAuthHandlers(opts),
|
||||
},
|
||||
});
|
||||
|
||||
let nextConfig = params.config;
|
||||
if (result.configPatch) {
|
||||
nextConfig = mergeConfigPatch(nextConfig, result.configPatch);
|
||||
}
|
||||
|
||||
for (const profile of result.profiles) {
|
||||
upsertAuthProfile({
|
||||
profileId: profile.profileId,
|
||||
credential: profile.credential,
|
||||
agentDir,
|
||||
});
|
||||
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: profile.profileId,
|
||||
provider: profile.credential.provider,
|
||||
mode: profile.credential.type === "token" ? "token" : profile.credential.type,
|
||||
...("email" in profile.credential && profile.credential.email
|
||||
? { email: profile.credential.email }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
if (params.emitNotes !== false && result.notes && result.notes.length > 0) {
|
||||
await params.prompter.note(result.notes.join("\n"), "Provider notes");
|
||||
}
|
||||
|
||||
return {
|
||||
config: nextConfig,
|
||||
defaultModel: result.defaultModel,
|
||||
};
|
||||
}
|
||||
|
||||
export async function applyAuthChoiceLoadedPluginProvider(
|
||||
params: ApplyAuthChoiceParams,
|
||||
): Promise<ApplyAuthChoiceResult | null> {
|
||||
const agentId = params.agentId ?? resolveDefaultAgentId(params.config);
|
||||
const workspaceDir =
|
||||
resolveAgentWorkspaceDir(params.config, agentId) ?? resolveDefaultAgentWorkspaceDir();
|
||||
const { resolvePluginProviders, resolveProviderPluginChoice, runProviderModelSelectedHook } =
|
||||
await loadPluginProviderRuntime();
|
||||
const providers = resolvePluginProviders({
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
});
|
||||
const resolved = resolveProviderPluginChoice({
|
||||
providers,
|
||||
choice: params.authChoice,
|
||||
});
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const applied = await runProviderPluginAuthMethod({
|
||||
config: params.config,
|
||||
runtime: params.runtime,
|
||||
prompter: params.prompter,
|
||||
method: resolved.method,
|
||||
agentDir: params.agentDir,
|
||||
agentId: params.agentId,
|
||||
workspaceDir,
|
||||
secretInputMode: params.opts?.secretInputMode,
|
||||
allowSecretRefPrompt: false,
|
||||
opts: params.opts as ProviderAuthOptionBag | undefined,
|
||||
});
|
||||
|
||||
let nextConfig = applied.config;
|
||||
let agentModelOverride: string | undefined;
|
||||
if (applied.defaultModel) {
|
||||
if (params.setDefaultModel) {
|
||||
nextConfig = applyDefaultModel(nextConfig, applied.defaultModel);
|
||||
await runProviderModelSelectedHook({
|
||||
config: nextConfig,
|
||||
model: applied.defaultModel,
|
||||
prompter: params.prompter,
|
||||
agentDir: params.agentDir,
|
||||
workspaceDir,
|
||||
});
|
||||
await params.prompter.note(
|
||||
`Default model set to ${applied.defaultModel}`,
|
||||
"Model configured",
|
||||
);
|
||||
return { config: nextConfig };
|
||||
}
|
||||
nextConfig = restoreConfiguredPrimaryModel(nextConfig, params.config);
|
||||
agentModelOverride = applied.defaultModel;
|
||||
}
|
||||
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
export async function applyAuthChoicePluginProvider(
|
||||
params: ApplyAuthChoiceParams,
|
||||
options: PluginProviderAuthChoiceOptions,
|
||||
): Promise<ApplyAuthChoiceResult | null> {
|
||||
if (params.authChoice !== options.authChoice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enableResult = enablePluginInConfig(params.config, options.pluginId);
|
||||
let nextConfig = enableResult.config;
|
||||
if (!enableResult.enabled) {
|
||||
await params.prompter.note(
|
||||
`${options.label} plugin is disabled (${enableResult.reason ?? "blocked"}).`,
|
||||
options.label,
|
||||
);
|
||||
return { config: nextConfig };
|
||||
}
|
||||
|
||||
const agentId = params.agentId ?? resolveDefaultAgentId(nextConfig);
|
||||
const defaultAgentId = resolveDefaultAgentId(nextConfig);
|
||||
const agentDir =
|
||||
params.agentDir ??
|
||||
(agentId === defaultAgentId ? resolveOpenClawAgentDir() : resolveAgentDir(nextConfig, agentId));
|
||||
const workspaceDir =
|
||||
resolveAgentWorkspaceDir(nextConfig, agentId) ?? resolveDefaultAgentWorkspaceDir();
|
||||
|
||||
const { resolvePluginProviders, runProviderModelSelectedHook } =
|
||||
await loadPluginProviderRuntime();
|
||||
const providers = resolvePluginProviders({
|
||||
config: nextConfig,
|
||||
workspaceDir,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
});
|
||||
const provider = resolveProviderMatch(providers, options.providerId);
|
||||
if (!provider) {
|
||||
await params.prompter.note(
|
||||
`${options.label} auth plugin is not available. Enable it and re-run onboarding.`,
|
||||
options.label,
|
||||
);
|
||||
return { config: nextConfig };
|
||||
}
|
||||
|
||||
const method = pickAuthMethod(provider, options.methodId) ?? provider.auth[0];
|
||||
if (!method) {
|
||||
await params.prompter.note(`${options.label} auth method missing.`, options.label);
|
||||
return { config: nextConfig };
|
||||
}
|
||||
|
||||
const applied = await runProviderPluginAuthMethod({
|
||||
config: nextConfig,
|
||||
runtime: params.runtime,
|
||||
prompter: params.prompter,
|
||||
method,
|
||||
agentDir,
|
||||
agentId,
|
||||
workspaceDir,
|
||||
secretInputMode: params.opts?.secretInputMode,
|
||||
allowSecretRefPrompt: false,
|
||||
opts: params.opts as ProviderAuthOptionBag | undefined,
|
||||
});
|
||||
nextConfig = applied.config;
|
||||
|
||||
let agentModelOverride: string | undefined;
|
||||
if (applied.defaultModel) {
|
||||
if (params.setDefaultModel) {
|
||||
nextConfig = applyDefaultModel(nextConfig, applied.defaultModel);
|
||||
await runProviderModelSelectedHook({
|
||||
config: nextConfig,
|
||||
model: applied.defaultModel,
|
||||
prompter: params.prompter,
|
||||
agentDir,
|
||||
workspaceDir,
|
||||
});
|
||||
await params.prompter.note(
|
||||
`Default model set to ${applied.defaultModel}`,
|
||||
"Model configured",
|
||||
);
|
||||
} else {
|
||||
nextConfig = restoreConfiguredPrimaryModel(nextConfig, params.config);
|
||||
}
|
||||
if (!params.setDefaultModel && params.agentId) {
|
||||
agentModelOverride = applied.defaultModel;
|
||||
await params.prompter.note(
|
||||
`Default model set to ${applied.defaultModel} for agent "${params.agentId}".`,
|
||||
"Model configured",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
export * from "../plugins/provider-auth-choice.js";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { applyAuthChoiceLoadedPluginProvider } from "../plugins/provider-auth-choice.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import { normalizeLegacyOnboardAuthChoice } from "./auth-choice-legacy.js";
|
||||
import { applyAuthChoiceApiProviders } from "./auth-choice.apply.api-providers.js";
|
||||
import { normalizeApiKeyTokenProviderAuthChoice } from "./auth-choice.apply.api-providers.js";
|
||||
import { applyAuthChoiceOAuth } from "./auth-choice.apply.oauth.js";
|
||||
import { applyAuthChoiceLoadedPluginProvider } from "./auth-choice.apply.plugin-provider.js";
|
||||
import type { AuthChoice, OnboardOptions } from "./onboard-types.js";
|
||||
|
||||
export type ApplyAuthChoiceParams = {
|
||||
|
||||
@@ -1,47 +1 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveManifestProviderAuthChoice } from "../plugins/provider-auth-choices.js";
|
||||
import { normalizeLegacyOnboardAuthChoice } from "./auth-choice-legacy.js";
|
||||
import type { AuthChoice } from "./onboard-types.js";
|
||||
|
||||
const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
||||
chutes: "chutes",
|
||||
"litellm-api-key": "litellm",
|
||||
"custom-api-key": "custom",
|
||||
};
|
||||
|
||||
export async function resolvePreferredProviderForAuthChoice(params: {
|
||||
choice: AuthChoice;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<string | undefined> {
|
||||
const choice = normalizeLegacyOnboardAuthChoice(params.choice) ?? params.choice;
|
||||
const manifestResolved = resolveManifestProviderAuthChoice(choice, params);
|
||||
if (manifestResolved) {
|
||||
return manifestResolved.providerId;
|
||||
}
|
||||
const [{ resolveProviderPluginChoice }, { resolvePluginProviders }] = await Promise.all([
|
||||
import("../plugins/provider-wizard.js"),
|
||||
import("../plugins/providers.js"),
|
||||
]);
|
||||
const providers = resolvePluginProviders({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
});
|
||||
const pluginResolved = resolveProviderPluginChoice({
|
||||
providers,
|
||||
choice,
|
||||
});
|
||||
if (pluginResolved) {
|
||||
return pluginResolved.provider.id;
|
||||
}
|
||||
|
||||
const preferred = PREFERRED_PROVIDER_BY_AUTH_CHOICE[choice];
|
||||
if (preferred) {
|
||||
return preferred;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
export * from "../plugins/provider-auth-choice-preference.js";
|
||||
|
||||
@@ -4,4 +4,4 @@ export {
|
||||
runProviderModelSelectedHook,
|
||||
} from "../plugins/provider-wizard.js";
|
||||
export { resolvePluginProviders } from "../plugins/providers.js";
|
||||
export { runProviderPluginAuthMethod } from "./auth-choice.apply.plugin-provider.js";
|
||||
export { runProviderPluginAuthMethod } from "../plugins/provider-auth-choice.js";
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { applyNonInteractivePluginProviderChoice } from "./auth-choice.plugin-providers.js";
|
||||
|
||||
const resolvePreferredProviderForAuthChoice = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
vi.mock("../../auth-choice.preferred-provider.js", () => ({
|
||||
vi.mock("../../../plugins/provider-auth-choice-preference.js", () => ({
|
||||
resolvePreferredProviderForAuthChoice,
|
||||
}));
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ import type { ApiKeyCredential } from "../../../agents/auth-profiles/types.js";
|
||||
import { resolveDefaultAgentWorkspaceDir } from "../../../agents/workspace.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { enablePluginInConfig } from "../../../plugins/enable.js";
|
||||
import { resolvePreferredProviderForAuthChoice } from "../../../plugins/provider-auth-choice-preference.js";
|
||||
import type {
|
||||
ProviderAuthOptionBag,
|
||||
ProviderNonInteractiveApiKeyCredentialParams,
|
||||
ProviderResolveNonInteractiveApiKeyParams,
|
||||
} from "../../../plugins/types.js";
|
||||
import type { RuntimeEnv } from "../../../runtime.js";
|
||||
import { resolvePreferredProviderForAuthChoice } from "../../auth-choice.preferred-provider.js";
|
||||
import type { OnboardOptions } from "../../onboard-types.js";
|
||||
|
||||
const PROVIDER_PLUGIN_CHOICE_PREFIX = "provider-plugin:";
|
||||
|
||||
@@ -1,82 +1 @@
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ProviderAuthMethod, ProviderPlugin } from "../plugins/types.js";
|
||||
|
||||
export function resolveProviderMatch(
|
||||
providers: ProviderPlugin[],
|
||||
rawProvider?: string,
|
||||
): ProviderPlugin | null {
|
||||
const raw = rawProvider?.trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const normalized = normalizeProviderId(raw);
|
||||
return (
|
||||
providers.find((provider) => normalizeProviderId(provider.id) === normalized) ??
|
||||
providers.find(
|
||||
(provider) =>
|
||||
provider.aliases?.some((alias) => normalizeProviderId(alias) === normalized) ?? false,
|
||||
) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export function pickAuthMethod(
|
||||
provider: ProviderPlugin,
|
||||
rawMethod?: string,
|
||||
): ProviderAuthMethod | null {
|
||||
const raw = rawMethod?.trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const normalized = raw.toLowerCase();
|
||||
return (
|
||||
provider.auth.find((method) => method.id.toLowerCase() === normalized) ??
|
||||
provider.auth.find((method) => method.label.toLowerCase() === normalized) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
export function mergeConfigPatch<T>(base: T, patch: unknown): T {
|
||||
if (!isPlainRecord(base) || !isPlainRecord(patch)) {
|
||||
return patch as T;
|
||||
}
|
||||
|
||||
const next: Record<string, unknown> = { ...base };
|
||||
for (const [key, value] of Object.entries(patch)) {
|
||||
const existing = next[key];
|
||||
if (isPlainRecord(existing) && isPlainRecord(value)) {
|
||||
next[key] = mergeConfigPatch(existing, value);
|
||||
} else {
|
||||
next[key] = value;
|
||||
}
|
||||
}
|
||||
return next as T;
|
||||
}
|
||||
|
||||
export function applyDefaultModel(cfg: OpenClawConfig, model: string): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[model] = models[model] ?? {};
|
||||
|
||||
const existingModel = cfg.agents?.defaults?.model;
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
model: {
|
||||
...(existingModel && typeof existingModel === "object" && "fallbacks" in existingModel
|
||||
? { fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks }
|
||||
: undefined),
|
||||
primary: model,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
export * from "../plugins/provider-auth-choice-helpers.js";
|
||||
|
||||
@@ -1,92 +1 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { vi } from "vitest";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { makeTempWorkspace } from "../test-helpers/workspace.js";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
|
||||
export const noopAsync = async () => {};
|
||||
export const noop = () => {};
|
||||
|
||||
export function createExitThrowingRuntime(): RuntimeEnv {
|
||||
return {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createWizardPrompter(
|
||||
overrides: Partial<WizardPrompter>,
|
||||
options?: { defaultSelect?: string },
|
||||
): WizardPrompter {
|
||||
return {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note: vi.fn(noopAsync),
|
||||
select: vi.fn(async () => (options?.defaultSelect ?? "") as never),
|
||||
multiselect: vi.fn(async () => []),
|
||||
text: vi.fn(async () => "") as unknown as WizardPrompter["text"],
|
||||
confirm: vi.fn(async () => false),
|
||||
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export async function setupAuthTestEnv(
|
||||
prefix = "openclaw-auth-",
|
||||
options?: { agentSubdir?: string },
|
||||
): Promise<{
|
||||
stateDir: string;
|
||||
agentDir: string;
|
||||
}> {
|
||||
const stateDir = await makeTempWorkspace(prefix);
|
||||
const agentDir = path.join(stateDir, options?.agentSubdir ?? "agent");
|
||||
process.env.OPENCLAW_STATE_DIR = stateDir;
|
||||
process.env.OPENCLAW_AGENT_DIR = agentDir;
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
return { stateDir, agentDir };
|
||||
}
|
||||
|
||||
export type AuthTestLifecycle = {
|
||||
setStateDir: (stateDir: string) => void;
|
||||
cleanup: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function createAuthTestLifecycle(envKeys: string[]): AuthTestLifecycle {
|
||||
const envSnapshot = captureEnv(envKeys);
|
||||
let stateDir: string | null = null;
|
||||
return {
|
||||
setStateDir(nextStateDir: string) {
|
||||
stateDir = nextStateDir;
|
||||
},
|
||||
async cleanup() {
|
||||
if (stateDir) {
|
||||
await fs.rm(stateDir, { recursive: true, force: true });
|
||||
stateDir = null;
|
||||
}
|
||||
envSnapshot.restore();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function requireOpenClawAgentDir(): string {
|
||||
const agentDir = process.env.OPENCLAW_AGENT_DIR;
|
||||
if (!agentDir) {
|
||||
throw new Error("OPENCLAW_AGENT_DIR not set");
|
||||
}
|
||||
return agentDir;
|
||||
}
|
||||
|
||||
export function authProfilePathForAgent(agentDir: string): string {
|
||||
return path.join(agentDir, "auth-profiles.json");
|
||||
}
|
||||
|
||||
export async function readAuthProfilesForAgent<T>(agentDir: string): Promise<T> {
|
||||
const raw = await fs.readFile(authProfilePathForAgent(agentDir), "utf8");
|
||||
return JSON.parse(raw) as T;
|
||||
}
|
||||
export * from "../../test/helpers/auth-wizard.js";
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { clearRuntimeAuthProfileStoreSnapshots } from "../../agents/auth-profiles/store.js";
|
||||
import { applyAuthChoiceLoadedPluginProvider } from "../../commands/auth-choice.apply.plugin-provider.js";
|
||||
import {
|
||||
createAuthTestLifecycle,
|
||||
createExitThrowingRuntime,
|
||||
@@ -8,18 +6,20 @@ import {
|
||||
readAuthProfilesForAgent,
|
||||
requireOpenClawAgentDir,
|
||||
setupAuthTestEnv,
|
||||
} from "../../commands/test-wizard-helpers.js";
|
||||
} from "../../../test/helpers/auth-wizard.js";
|
||||
import { clearRuntimeAuthProfileStoreSnapshots } from "../../agents/auth-profiles/store.js";
|
||||
import { applyAuthChoiceLoadedPluginProvider } from "../../plugins/provider-auth-choice.js";
|
||||
import { createCapturedPluginRegistration } from "../../test-utils/plugin-registration.js";
|
||||
import { buildProviderPluginMethodChoice } from "../provider-wizard.js";
|
||||
import type { OpenClawPluginApi, ProviderPlugin } from "../types.js";
|
||||
import { requireProviderContractProvider, uniqueProviderContractProviders } from "./registry.js";
|
||||
|
||||
type ResolvePluginProviders =
|
||||
typeof import("../../commands/auth-choice.apply.plugin-provider.runtime.js").resolvePluginProviders;
|
||||
typeof import("../../plugins/provider-auth-choice.runtime.js").resolvePluginProviders;
|
||||
type ResolveProviderPluginChoice =
|
||||
typeof import("../../commands/auth-choice.apply.plugin-provider.runtime.js").resolveProviderPluginChoice;
|
||||
typeof import("../../plugins/provider-auth-choice.runtime.js").resolveProviderPluginChoice;
|
||||
type RunProviderModelSelectedHook =
|
||||
typeof import("../../commands/auth-choice.apply.plugin-provider.runtime.js").runProviderModelSelectedHook;
|
||||
typeof import("../../plugins/provider-auth-choice.runtime.js").runProviderModelSelectedHook;
|
||||
|
||||
const loginQwenPortalOAuthMock = vi.hoisted(() => vi.fn());
|
||||
const githubCopilotLoginCommandMock = vi.hoisted(() => vi.fn());
|
||||
@@ -38,7 +38,7 @@ vi.mock("../../providers/github-copilot-auth.js", () => ({
|
||||
githubCopilotLoginCommand: githubCopilotLoginCommandMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../commands/auth-choice.apply.plugin-provider.runtime.js", () => ({
|
||||
vi.mock("../../plugins/provider-auth-choice.runtime.js", () => ({
|
||||
resolvePluginProviders: resolvePluginProvidersMock,
|
||||
resolveProviderPluginChoice: resolveProviderPluginChoiceMock,
|
||||
runProviderModelSelectedHook: runProviderModelSelectedHookMock,
|
||||
@@ -54,7 +54,7 @@ vi.mock("../../plugins/providers.js", async () => {
|
||||
});
|
||||
|
||||
const { resolvePreferredProviderForAuthChoice } =
|
||||
await import("../../commands/auth-choice.preferred-provider.js");
|
||||
await import("../../plugins/provider-auth-choice-preference.js");
|
||||
|
||||
type StoredAuthProfile = {
|
||||
type?: string;
|
||||
|
||||
@@ -14,19 +14,19 @@ import type {
|
||||
import type { OpenClawPluginApi, ProviderPlugin } from "../types.js";
|
||||
|
||||
type LoginOpenAICodexOAuth =
|
||||
(typeof import("../../commands/openai-codex-oauth.js"))["loginOpenAICodexOAuth"];
|
||||
(typeof import("../../plugins/provider-openai-codex-oauth.js"))["loginOpenAICodexOAuth"];
|
||||
type LoginQwenPortalOAuth =
|
||||
(typeof import("../../../extensions/qwen-portal-auth/oauth.js"))["loginQwenPortalOAuth"];
|
||||
type GithubCopilotLoginCommand =
|
||||
(typeof import("../../providers/github-copilot-auth.js"))["githubCopilotLoginCommand"];
|
||||
type CreateVpsAwareHandlers =
|
||||
(typeof import("../../commands/oauth-flow.js"))["createVpsAwareOAuthHandlers"];
|
||||
(typeof import("../../plugins/provider-oauth-flow.js"))["createVpsAwareOAuthHandlers"];
|
||||
|
||||
const loginOpenAICodexOAuthMock = vi.hoisted(() => vi.fn<LoginOpenAICodexOAuth>());
|
||||
const loginQwenPortalOAuthMock = vi.hoisted(() => vi.fn<LoginQwenPortalOAuth>());
|
||||
const githubCopilotLoginCommandMock = vi.hoisted(() => vi.fn<GithubCopilotLoginCommand>());
|
||||
|
||||
vi.mock("../../commands/openai-codex-oauth.js", () => ({
|
||||
vi.mock("../../plugins/provider-openai-codex-oauth.js", () => ({
|
||||
loginOpenAICodexOAuth: loginOpenAICodexOAuthMock,
|
||||
}));
|
||||
|
||||
|
||||
82
src/plugins/provider-auth-choice-helpers.ts
Normal file
82
src/plugins/provider-auth-choice-helpers.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ProviderAuthMethod, ProviderPlugin } from "./types.js";
|
||||
|
||||
export function resolveProviderMatch(
|
||||
providers: ProviderPlugin[],
|
||||
rawProvider?: string,
|
||||
): ProviderPlugin | null {
|
||||
const raw = rawProvider?.trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const normalized = normalizeProviderId(raw);
|
||||
return (
|
||||
providers.find((provider) => normalizeProviderId(provider.id) === normalized) ??
|
||||
providers.find(
|
||||
(provider) =>
|
||||
provider.aliases?.some((alias) => normalizeProviderId(alias) === normalized) ?? false,
|
||||
) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export function pickAuthMethod(
|
||||
provider: ProviderPlugin,
|
||||
rawMethod?: string,
|
||||
): ProviderAuthMethod | null {
|
||||
const raw = rawMethod?.trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const normalized = raw.toLowerCase();
|
||||
return (
|
||||
provider.auth.find((method) => method.id.toLowerCase() === normalized) ??
|
||||
provider.auth.find((method) => method.label.toLowerCase() === normalized) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
export function mergeConfigPatch<T>(base: T, patch: unknown): T {
|
||||
if (!isPlainRecord(base) || !isPlainRecord(patch)) {
|
||||
return patch as T;
|
||||
}
|
||||
|
||||
const next: Record<string, unknown> = { ...base };
|
||||
for (const [key, value] of Object.entries(patch)) {
|
||||
const existing = next[key];
|
||||
if (isPlainRecord(existing) && isPlainRecord(value)) {
|
||||
next[key] = mergeConfigPatch(existing, value);
|
||||
} else {
|
||||
next[key] = value;
|
||||
}
|
||||
}
|
||||
return next as T;
|
||||
}
|
||||
|
||||
export function applyDefaultModel(cfg: OpenClawConfig, model: string): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[model] = models[model] ?? {};
|
||||
|
||||
const existingModel = cfg.agents?.defaults?.model;
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
model: {
|
||||
...(existingModel && typeof existingModel === "object" && "fallbacks" in existingModel
|
||||
? { fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks }
|
||||
: undefined),
|
||||
primary: model,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
53
src/plugins/provider-auth-choice-preference.ts
Normal file
53
src/plugins/provider-auth-choice-preference.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveManifestProviderAuthChoice } from "./provider-auth-choices.js";
|
||||
|
||||
const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<string, string>> = {
|
||||
chutes: "chutes",
|
||||
"litellm-api-key": "litellm",
|
||||
"custom-api-key": "custom",
|
||||
};
|
||||
|
||||
function normalizeLegacyAuthChoice(choice: string): string {
|
||||
if (choice === "oauth") {
|
||||
return "setup-token";
|
||||
}
|
||||
if (choice === "claude-cli") {
|
||||
return "setup-token";
|
||||
}
|
||||
if (choice === "codex-cli") {
|
||||
return "openai-codex";
|
||||
}
|
||||
return choice;
|
||||
}
|
||||
|
||||
export async function resolvePreferredProviderForAuthChoice(params: {
|
||||
choice: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<string | undefined> {
|
||||
const choice = normalizeLegacyAuthChoice(params.choice) ?? params.choice;
|
||||
const manifestResolved = resolveManifestProviderAuthChoice(choice, params);
|
||||
if (manifestResolved) {
|
||||
return manifestResolved.providerId;
|
||||
}
|
||||
|
||||
const { resolveProviderPluginChoice, resolvePluginProviders } =
|
||||
await import("./provider-auth-choice.runtime.js");
|
||||
const providers = resolvePluginProviders({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
});
|
||||
const pluginResolved = resolveProviderPluginChoice({
|
||||
providers,
|
||||
choice,
|
||||
});
|
||||
if (pluginResolved) {
|
||||
return pluginResolved.provider.id;
|
||||
}
|
||||
|
||||
return PREFERRED_PROVIDER_BY_AUTH_CHOICE[choice];
|
||||
}
|
||||
2
src/plugins/provider-auth-choice.runtime.ts
Normal file
2
src/plugins/provider-auth-choice.runtime.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { resolveProviderPluginChoice, runProviderModelSelectedHook } from "./provider-wizard.js";
|
||||
export { resolvePluginProviders } from "./providers.js";
|
||||
309
src/plugins/provider-auth-choice.ts
Normal file
309
src/plugins/provider-auth-choice.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
|
||||
import {
|
||||
resolveDefaultAgentId,
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { upsertAuthProfile } from "../agents/auth-profiles.js";
|
||||
import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import { enablePluginInConfig } from "./enable.js";
|
||||
import {
|
||||
applyDefaultModel,
|
||||
mergeConfigPatch,
|
||||
pickAuthMethod,
|
||||
resolveProviderMatch,
|
||||
} from "./provider-auth-choice-helpers.js";
|
||||
import { applyAuthProfileConfig } from "./provider-auth-helpers.js";
|
||||
import { createVpsAwareOAuthHandlers } from "./provider-oauth-flow.js";
|
||||
import { isRemoteEnvironment, openUrl } from "./setup-browser.js";
|
||||
import type { ProviderAuthMethod, ProviderAuthOptionBag, ProviderPlugin } from "./types.js";
|
||||
|
||||
export type ApplyProviderAuthChoiceParams = {
|
||||
authChoice: string;
|
||||
config: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
runtime: RuntimeEnv;
|
||||
agentDir?: string;
|
||||
setDefaultModel: boolean;
|
||||
agentId?: string;
|
||||
opts?: Partial<ProviderAuthOptionBag>;
|
||||
};
|
||||
|
||||
export type ApplyProviderAuthChoiceResult = {
|
||||
config: OpenClawConfig;
|
||||
agentModelOverride?: string;
|
||||
};
|
||||
|
||||
export type PluginProviderAuthChoiceOptions = {
|
||||
authChoice: string;
|
||||
pluginId: string;
|
||||
providerId: string;
|
||||
methodId?: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
function restoreConfiguredPrimaryModel(
|
||||
nextConfig: OpenClawConfig,
|
||||
originalConfig: OpenClawConfig,
|
||||
): OpenClawConfig {
|
||||
const originalModel = originalConfig.agents?.defaults?.model;
|
||||
const nextAgents = nextConfig.agents;
|
||||
const nextDefaults = nextAgents?.defaults;
|
||||
if (!nextDefaults) {
|
||||
return nextConfig;
|
||||
}
|
||||
if (originalModel !== undefined) {
|
||||
return {
|
||||
...nextConfig,
|
||||
agents: {
|
||||
...nextAgents,
|
||||
defaults: {
|
||||
...nextDefaults,
|
||||
model: originalModel,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const { model: _model, ...restDefaults } = nextDefaults;
|
||||
return {
|
||||
...nextConfig,
|
||||
agents: {
|
||||
...nextAgents,
|
||||
defaults: restDefaults,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function loadPluginProviderRuntime() {
|
||||
return import("./provider-auth-choice.runtime.js");
|
||||
}
|
||||
|
||||
export async function runProviderPluginAuthMethod(params: {
|
||||
config: OpenClawConfig;
|
||||
runtime: RuntimeEnv;
|
||||
prompter: WizardPrompter;
|
||||
method: ProviderAuthMethod;
|
||||
agentDir?: string;
|
||||
agentId?: string;
|
||||
workspaceDir?: string;
|
||||
emitNotes?: boolean;
|
||||
secretInputMode?: ProviderAuthOptionBag["secretInputMode"];
|
||||
allowSecretRefPrompt?: boolean;
|
||||
opts?: Partial<ProviderAuthOptionBag>;
|
||||
}): Promise<{ config: OpenClawConfig; defaultModel?: string }> {
|
||||
const agentId = params.agentId ?? resolveDefaultAgentId(params.config);
|
||||
const defaultAgentId = resolveDefaultAgentId(params.config);
|
||||
const agentDir =
|
||||
params.agentDir ??
|
||||
(agentId === defaultAgentId
|
||||
? resolveOpenClawAgentDir()
|
||||
: resolveAgentDir(params.config, agentId));
|
||||
const workspaceDir =
|
||||
params.workspaceDir ??
|
||||
resolveAgentWorkspaceDir(params.config, agentId) ??
|
||||
resolveDefaultAgentWorkspaceDir();
|
||||
|
||||
const result = await params.method.run({
|
||||
config: params.config,
|
||||
agentDir,
|
||||
workspaceDir,
|
||||
prompter: params.prompter,
|
||||
runtime: params.runtime,
|
||||
opts: params.opts,
|
||||
secretInputMode: params.secretInputMode,
|
||||
allowSecretRefPrompt: params.allowSecretRefPrompt,
|
||||
isRemote: isRemoteEnvironment(),
|
||||
openUrl: async (url) => {
|
||||
await openUrl(url);
|
||||
},
|
||||
oauth: {
|
||||
createVpsAwareHandlers: (opts) => createVpsAwareOAuthHandlers(opts),
|
||||
},
|
||||
});
|
||||
|
||||
let nextConfig = params.config;
|
||||
if (result.configPatch) {
|
||||
nextConfig = mergeConfigPatch(nextConfig, result.configPatch);
|
||||
}
|
||||
|
||||
for (const profile of result.profiles) {
|
||||
upsertAuthProfile({
|
||||
profileId: profile.profileId,
|
||||
credential: profile.credential,
|
||||
agentDir,
|
||||
});
|
||||
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: profile.profileId,
|
||||
provider: profile.credential.provider,
|
||||
mode: profile.credential.type === "token" ? "token" : profile.credential.type,
|
||||
...("email" in profile.credential && profile.credential.email
|
||||
? { email: profile.credential.email }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
if (params.emitNotes !== false && result.notes && result.notes.length > 0) {
|
||||
await params.prompter.note(result.notes.join("\n"), "Provider notes");
|
||||
}
|
||||
|
||||
return {
|
||||
config: nextConfig,
|
||||
defaultModel: result.defaultModel,
|
||||
};
|
||||
}
|
||||
|
||||
export async function applyAuthChoiceLoadedPluginProvider(
|
||||
params: ApplyProviderAuthChoiceParams,
|
||||
): Promise<ApplyProviderAuthChoiceResult | null> {
|
||||
const agentId = params.agentId ?? resolveDefaultAgentId(params.config);
|
||||
const workspaceDir =
|
||||
resolveAgentWorkspaceDir(params.config, agentId) ?? resolveDefaultAgentWorkspaceDir();
|
||||
const { resolvePluginProviders, resolveProviderPluginChoice, runProviderModelSelectedHook } =
|
||||
await loadPluginProviderRuntime();
|
||||
const providers = resolvePluginProviders({
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
});
|
||||
const resolved = resolveProviderPluginChoice({
|
||||
providers,
|
||||
choice: params.authChoice,
|
||||
});
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const applied = await runProviderPluginAuthMethod({
|
||||
config: params.config,
|
||||
runtime: params.runtime,
|
||||
prompter: params.prompter,
|
||||
method: resolved.method,
|
||||
agentDir: params.agentDir,
|
||||
agentId: params.agentId,
|
||||
workspaceDir,
|
||||
secretInputMode: params.opts?.secretInputMode,
|
||||
allowSecretRefPrompt: false,
|
||||
opts: params.opts,
|
||||
});
|
||||
|
||||
let nextConfig = applied.config;
|
||||
let agentModelOverride: string | undefined;
|
||||
if (applied.defaultModel) {
|
||||
if (params.setDefaultModel) {
|
||||
nextConfig = applyDefaultModel(nextConfig, applied.defaultModel);
|
||||
await runProviderModelSelectedHook({
|
||||
config: nextConfig,
|
||||
model: applied.defaultModel,
|
||||
prompter: params.prompter,
|
||||
agentDir: params.agentDir,
|
||||
workspaceDir,
|
||||
});
|
||||
await params.prompter.note(
|
||||
`Default model set to ${applied.defaultModel}`,
|
||||
"Model configured",
|
||||
);
|
||||
return { config: nextConfig };
|
||||
}
|
||||
nextConfig = restoreConfiguredPrimaryModel(nextConfig, params.config);
|
||||
agentModelOverride = applied.defaultModel;
|
||||
}
|
||||
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
export async function applyAuthChoicePluginProvider(
|
||||
params: ApplyProviderAuthChoiceParams,
|
||||
options: PluginProviderAuthChoiceOptions,
|
||||
): Promise<ApplyProviderAuthChoiceResult | null> {
|
||||
if (params.authChoice !== options.authChoice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enableResult = enablePluginInConfig(params.config, options.pluginId);
|
||||
let nextConfig = enableResult.config;
|
||||
if (!enableResult.enabled) {
|
||||
await params.prompter.note(
|
||||
`${options.label} plugin is disabled (${enableResult.reason ?? "blocked"}).`,
|
||||
options.label,
|
||||
);
|
||||
return { config: nextConfig };
|
||||
}
|
||||
|
||||
const agentId = params.agentId ?? resolveDefaultAgentId(nextConfig);
|
||||
const defaultAgentId = resolveDefaultAgentId(nextConfig);
|
||||
const agentDir =
|
||||
params.agentDir ??
|
||||
(agentId === defaultAgentId ? resolveOpenClawAgentDir() : resolveAgentDir(nextConfig, agentId));
|
||||
const workspaceDir =
|
||||
resolveAgentWorkspaceDir(nextConfig, agentId) ?? resolveDefaultAgentWorkspaceDir();
|
||||
|
||||
const { resolvePluginProviders, runProviderModelSelectedHook } =
|
||||
await loadPluginProviderRuntime();
|
||||
const providers = resolvePluginProviders({
|
||||
config: nextConfig,
|
||||
workspaceDir,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
});
|
||||
const provider = resolveProviderMatch(providers, options.providerId);
|
||||
if (!provider) {
|
||||
await params.prompter.note(
|
||||
`${options.label} auth plugin is not available. Enable it and re-run onboarding.`,
|
||||
options.label,
|
||||
);
|
||||
return { config: nextConfig };
|
||||
}
|
||||
|
||||
const method = pickAuthMethod(provider, options.methodId) ?? provider.auth[0];
|
||||
if (!method) {
|
||||
await params.prompter.note(`${options.label} auth method missing.`, options.label);
|
||||
return { config: nextConfig };
|
||||
}
|
||||
|
||||
const applied = await runProviderPluginAuthMethod({
|
||||
config: nextConfig,
|
||||
runtime: params.runtime,
|
||||
prompter: params.prompter,
|
||||
method,
|
||||
agentDir,
|
||||
agentId,
|
||||
workspaceDir,
|
||||
secretInputMode: params.opts?.secretInputMode,
|
||||
allowSecretRefPrompt: false,
|
||||
opts: params.opts,
|
||||
});
|
||||
|
||||
nextConfig = applied.config;
|
||||
if (applied.defaultModel) {
|
||||
if (params.setDefaultModel) {
|
||||
nextConfig = applyDefaultModel(nextConfig, applied.defaultModel);
|
||||
await runProviderModelSelectedHook({
|
||||
config: nextConfig,
|
||||
model: applied.defaultModel,
|
||||
prompter: params.prompter,
|
||||
agentDir,
|
||||
workspaceDir,
|
||||
});
|
||||
await params.prompter.note(
|
||||
`Default model set to ${applied.defaultModel}`,
|
||||
"Model configured",
|
||||
);
|
||||
return { config: nextConfig };
|
||||
}
|
||||
if (params.agentId) {
|
||||
await params.prompter.note(
|
||||
`Default model set to ${applied.defaultModel} for agent "${params.agentId}".`,
|
||||
"Model configured",
|
||||
);
|
||||
}
|
||||
nextConfig = restoreConfiguredPrimaryModel(nextConfig, params.config);
|
||||
return { config: nextConfig, agentModelOverride: applied.defaultModel };
|
||||
}
|
||||
|
||||
return { config: nextConfig };
|
||||
}
|
||||
92
test/helpers/auth-wizard.ts
Normal file
92
test/helpers/auth-wizard.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { vi } from "vitest";
|
||||
import type { RuntimeEnv } from "../../src/runtime.js";
|
||||
import { makeTempWorkspace } from "../../src/test-helpers/workspace.js";
|
||||
import { captureEnv } from "../../src/test-utils/env.js";
|
||||
import type { WizardPrompter } from "../../src/wizard/prompts.js";
|
||||
|
||||
export const noopAsync = async () => {};
|
||||
export const noop = () => {};
|
||||
|
||||
export function createExitThrowingRuntime(): RuntimeEnv {
|
||||
return {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createWizardPrompter(
|
||||
overrides: Partial<WizardPrompter>,
|
||||
options?: { defaultSelect?: string },
|
||||
): WizardPrompter {
|
||||
return {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note: vi.fn(noopAsync),
|
||||
select: vi.fn(async () => (options?.defaultSelect ?? "") as never),
|
||||
multiselect: vi.fn(async () => []),
|
||||
text: vi.fn(async () => "") as unknown as WizardPrompter["text"],
|
||||
confirm: vi.fn(async () => false),
|
||||
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export async function setupAuthTestEnv(
|
||||
prefix = "openclaw-auth-",
|
||||
options?: { agentSubdir?: string },
|
||||
): Promise<{
|
||||
stateDir: string;
|
||||
agentDir: string;
|
||||
}> {
|
||||
const stateDir = await makeTempWorkspace(prefix);
|
||||
const agentDir = path.join(stateDir, options?.agentSubdir ?? "agent");
|
||||
process.env.OPENCLAW_STATE_DIR = stateDir;
|
||||
process.env.OPENCLAW_AGENT_DIR = agentDir;
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
return { stateDir, agentDir };
|
||||
}
|
||||
|
||||
export type AuthTestLifecycle = {
|
||||
setStateDir: (stateDir: string) => void;
|
||||
cleanup: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function createAuthTestLifecycle(envKeys: string[]): AuthTestLifecycle {
|
||||
const envSnapshot = captureEnv(envKeys);
|
||||
let stateDir: string | null = null;
|
||||
return {
|
||||
setStateDir(nextStateDir: string) {
|
||||
stateDir = nextStateDir;
|
||||
},
|
||||
async cleanup() {
|
||||
if (stateDir) {
|
||||
await fs.rm(stateDir, { recursive: true, force: true });
|
||||
stateDir = null;
|
||||
}
|
||||
envSnapshot.restore();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function requireOpenClawAgentDir(): string {
|
||||
const agentDir = process.env.OPENCLAW_AGENT_DIR;
|
||||
if (!agentDir) {
|
||||
throw new Error("OPENCLAW_AGENT_DIR not set");
|
||||
}
|
||||
return agentDir;
|
||||
}
|
||||
|
||||
export function authProfilePathForAgent(agentDir: string): string {
|
||||
return path.join(agentDir, "auth-profiles.json");
|
||||
}
|
||||
|
||||
export async function readAuthProfilesForAgent<T>(agentDir: string): Promise<T> {
|
||||
const raw = await fs.readFile(authProfilePathForAgent(agentDir), "utf8");
|
||||
return JSON.parse(raw) as T;
|
||||
}
|
||||
Reference in New Issue
Block a user