Onboarding: scope non-interactive API keys by agent
This commit is contained in:
parent
673b3f186c
commit
f8f55fb0d7
@ -44,6 +44,7 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => {
|
||||
});
|
||||
|
||||
it("disables profile fallback for GigaChat personal OAuth onboarding", async () => {
|
||||
const agentDir = "/tmp/openclaw-agents/work/agent";
|
||||
const nextConfig = { agents: { defaults: {} } } as OpenClawConfig;
|
||||
const resolveApiKey = vi.fn(async () => ({
|
||||
key: "gigachat-oauth-credentials",
|
||||
@ -60,6 +61,7 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => {
|
||||
baseConfig: nextConfig,
|
||||
opts: {} as never,
|
||||
runtime: { error: vi.fn(), exit: vi.fn(), log: vi.fn() } as never,
|
||||
agentDir,
|
||||
apiKeyStorageOptions: undefined,
|
||||
resolveApiKey,
|
||||
maybeSetResolvedApiKey,
|
||||
@ -70,13 +72,14 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => {
|
||||
provider: "gigachat",
|
||||
flagName: "--gigachat-api-key",
|
||||
envVar: "GIGACHAT_CREDENTIALS",
|
||||
agentDir,
|
||||
allowProfile: false,
|
||||
}),
|
||||
);
|
||||
expect(maybeSetResolvedApiKey).toHaveBeenCalledOnce();
|
||||
expect(setGigachatApiKey).toHaveBeenCalledWith(
|
||||
"gigachat-oauth-credentials",
|
||||
undefined,
|
||||
agentDir,
|
||||
undefined,
|
||||
{
|
||||
authMode: "oauth",
|
||||
@ -87,6 +90,7 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => {
|
||||
});
|
||||
|
||||
it("accepts the generic --token input for GigaChat non-interactive OAuth", async () => {
|
||||
const agentDir = "/tmp/openclaw-agents/work/agent";
|
||||
const nextConfig = { agents: { defaults: {} } } as OpenClawConfig;
|
||||
const resolveApiKey = vi.fn(async () => ({
|
||||
key: "gigachat-token-credentials",
|
||||
@ -103,6 +107,7 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => {
|
||||
baseConfig: nextConfig,
|
||||
opts: { token: "gigachat-token-credentials" } as never,
|
||||
runtime: { error: vi.fn(), exit: vi.fn(), log: vi.fn() } as never,
|
||||
agentDir,
|
||||
apiKeyStorageOptions: undefined,
|
||||
resolveApiKey,
|
||||
maybeSetResolvedApiKey,
|
||||
@ -114,12 +119,13 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => {
|
||||
flagValue: "gigachat-token-credentials",
|
||||
flagName: "--gigachat-api-key",
|
||||
envVar: "GIGACHAT_CREDENTIALS",
|
||||
agentDir,
|
||||
allowProfile: false,
|
||||
}),
|
||||
);
|
||||
expect(setGigachatApiKey).toHaveBeenCalledWith(
|
||||
"gigachat-token-credentials",
|
||||
undefined,
|
||||
agentDir,
|
||||
undefined,
|
||||
{
|
||||
authMode: "oauth",
|
||||
@ -130,6 +136,7 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => {
|
||||
});
|
||||
|
||||
it("rejects Basic-shaped GIGACHAT_CREDENTIALS in the OAuth onboarding path", async () => {
|
||||
const agentDir = "/tmp/openclaw-agents/work/agent";
|
||||
const nextConfig = { agents: { defaults: {} } } as OpenClawConfig;
|
||||
const runtime: RuntimeEnv = {
|
||||
error: vi.fn(),
|
||||
@ -148,6 +155,7 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => {
|
||||
baseConfig: nextConfig,
|
||||
opts: {} as never,
|
||||
runtime,
|
||||
agentDir,
|
||||
apiKeyStorageOptions: undefined,
|
||||
resolveApiKey,
|
||||
maybeSetResolvedApiKey,
|
||||
@ -163,6 +171,7 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => {
|
||||
});
|
||||
|
||||
it("resets the GigaChat provider base URL when replacing a Basic profile with OAuth", async () => {
|
||||
const agentDir = "/tmp/openclaw-agents/work/agent";
|
||||
const nextConfig = { agents: { defaults: {} } } as OpenClawConfig;
|
||||
const basicProfile: ApiKeyCredential = {
|
||||
type: "api_key",
|
||||
@ -203,11 +212,13 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => {
|
||||
} as OpenClawConfig,
|
||||
opts: { token: "gigachat-oauth-credentials" } as never,
|
||||
runtime: { error: vi.fn(), exit: vi.fn(), log: vi.fn() } as never,
|
||||
agentDir,
|
||||
apiKeyStorageOptions: undefined,
|
||||
resolveApiKey,
|
||||
maybeSetResolvedApiKey,
|
||||
});
|
||||
|
||||
expect(loadAuthProfileStoreForSecretsRuntime).toHaveBeenCalledWith(agentDir);
|
||||
expect(applyGigachatConfig).toHaveBeenCalledWith(expect.any(Object), {
|
||||
baseUrl: GIGACHAT_BASE_URL,
|
||||
});
|
||||
|
||||
@ -19,8 +19,8 @@ type ResolvedNonInteractiveApiKey = {
|
||||
source: "profile" | "env" | "flag";
|
||||
};
|
||||
|
||||
function hadStoredGigachatBasicProfile(): boolean {
|
||||
const profile = loadAuthProfileStoreForSecretsRuntime().profiles["gigachat:default"];
|
||||
function hadStoredGigachatBasicProfile(agentDir?: string): boolean {
|
||||
const profile = loadAuthProfileStoreForSecretsRuntime(agentDir).profiles["gigachat:default"];
|
||||
return (
|
||||
profile?.type === "api_key" &&
|
||||
profile.provider === "gigachat" &&
|
||||
@ -33,6 +33,7 @@ async function applyGigachatNonInteractiveApiKeyChoice(params: {
|
||||
baseConfig: OpenClawConfig;
|
||||
opts: OnboardOptions;
|
||||
runtime: RuntimeEnv;
|
||||
agentDir?: string;
|
||||
apiKeyStorageOptions?: ApiKeyStorageOptions;
|
||||
resolveApiKey: (input: {
|
||||
provider: string;
|
||||
@ -41,6 +42,7 @@ async function applyGigachatNonInteractiveApiKeyChoice(params: {
|
||||
flagName: `--${string}`;
|
||||
envVar: string;
|
||||
runtime: RuntimeEnv;
|
||||
agentDir?: string;
|
||||
allowProfile?: boolean;
|
||||
}) => Promise<ResolvedNonInteractiveApiKey | null>;
|
||||
maybeSetResolvedApiKey: (
|
||||
@ -48,7 +50,7 @@ async function applyGigachatNonInteractiveApiKeyChoice(params: {
|
||||
setter: (value: SecretInput) => Promise<void> | void,
|
||||
) => Promise<boolean>;
|
||||
}): Promise<OpenClawConfig | null> {
|
||||
const resetGigachatBaseUrl = hadStoredGigachatBasicProfile();
|
||||
const resetGigachatBaseUrl = hadStoredGigachatBasicProfile(params.agentDir);
|
||||
const resolved = await params.resolveApiKey({
|
||||
provider: "gigachat",
|
||||
cfg: params.baseConfig,
|
||||
@ -56,6 +58,7 @@ async function applyGigachatNonInteractiveApiKeyChoice(params: {
|
||||
flagName: "--gigachat-api-key",
|
||||
envVar: "GIGACHAT_CREDENTIALS",
|
||||
runtime: params.runtime,
|
||||
agentDir: params.agentDir,
|
||||
// Personal OAuth onboarding must not silently reuse an existing Basic
|
||||
// username:password profile and then rewrite the provider to OAuth config.
|
||||
allowProfile: false,
|
||||
@ -76,7 +79,7 @@ async function applyGigachatNonInteractiveApiKeyChoice(params: {
|
||||
}
|
||||
if (
|
||||
!(await params.maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setGigachatApiKey(value, undefined, params.apiKeyStorageOptions, {
|
||||
setGigachatApiKey(value, params.agentDir, params.apiKeyStorageOptions, {
|
||||
authMode: "oauth",
|
||||
insecureTls: "false",
|
||||
scope: "GIGACHAT_API_PERS",
|
||||
@ -101,6 +104,7 @@ export async function applySimpleNonInteractiveApiKeyChoice(params: {
|
||||
baseConfig: OpenClawConfig;
|
||||
opts: OnboardOptions;
|
||||
runtime: RuntimeEnv;
|
||||
agentDir?: string;
|
||||
apiKeyStorageOptions?: ApiKeyStorageOptions;
|
||||
resolveApiKey: (input: {
|
||||
provider: string;
|
||||
@ -109,6 +113,7 @@ export async function applySimpleNonInteractiveApiKeyChoice(params: {
|
||||
flagName: `--${string}`;
|
||||
envVar: string;
|
||||
runtime: RuntimeEnv;
|
||||
agentDir?: string;
|
||||
allowProfile?: boolean;
|
||||
}) => Promise<ResolvedNonInteractiveApiKey | null>;
|
||||
maybeSetResolvedApiKey: (
|
||||
@ -131,13 +136,14 @@ export async function applySimpleNonInteractiveApiKeyChoice(params: {
|
||||
flagName: "--litellm-api-key",
|
||||
envVar: "LITELLM_API_KEY",
|
||||
runtime: params.runtime,
|
||||
agentDir: params.agentDir,
|
||||
});
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
!(await params.maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setLitellmApiKey(value, undefined, params.apiKeyStorageOptions),
|
||||
setLitellmApiKey(value, params.agentDir, params.apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
|
||||
@ -3,7 +3,9 @@ import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { applyNonInteractiveAuthChoice } from "./auth-choice.js";
|
||||
|
||||
const applySimpleNonInteractiveApiKeyChoice = vi.hoisted(() =>
|
||||
vi.fn<() => Promise<OpenClawConfig | null | undefined>>(async () => undefined),
|
||||
vi.fn<typeof import("./auth-choice.api-key-providers.js").applySimpleNonInteractiveApiKeyChoice>(
|
||||
async () => undefined,
|
||||
),
|
||||
);
|
||||
vi.mock("./auth-choice.api-key-providers.js", () => ({
|
||||
applySimpleNonInteractiveApiKeyChoice,
|
||||
@ -50,4 +52,50 @@ describe("applyNonInteractiveAuthChoice", () => {
|
||||
expect(applyNonInteractivePluginProviderChoice).toHaveBeenCalledOnce();
|
||||
expect(applySimpleNonInteractiveApiKeyChoice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes the target agent dir into builtin non-interactive API key flows", async () => {
|
||||
const runtime = createRuntime();
|
||||
const nextConfig = {
|
||||
agents: {
|
||||
defaults: {},
|
||||
list: [
|
||||
{
|
||||
id: "work",
|
||||
default: true,
|
||||
agentDir: "/tmp/openclaw-agents/work/agent",
|
||||
},
|
||||
],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
applySimpleNonInteractiveApiKeyChoice.mockImplementationOnce(async ({ resolveApiKey }) => {
|
||||
await resolveApiKey({
|
||||
provider: "gigachat",
|
||||
cfg: nextConfig,
|
||||
flagName: "--gigachat-api-key",
|
||||
envVar: "GIGACHAT_CREDENTIALS",
|
||||
runtime: runtime as never,
|
||||
});
|
||||
return null;
|
||||
});
|
||||
resolveNonInteractiveApiKey.mockResolvedValueOnce(null);
|
||||
|
||||
await applyNonInteractiveAuthChoice({
|
||||
nextConfig,
|
||||
authChoice: "gigachat-api-key",
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: nextConfig,
|
||||
});
|
||||
|
||||
expect(applySimpleNonInteractiveApiKeyChoice).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentDir: "/tmp/openclaw-agents/work/agent",
|
||||
}),
|
||||
);
|
||||
expect(resolveNonInteractiveApiKey).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentDir: "/tmp/openclaw-agents/work/agent",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { resolveAgentDir, resolveDefaultAgentId } from "../../../agents/agent-scope.js";
|
||||
import type { ApiKeyCredential } from "../../../agents/auth-profiles/types.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import type { SecretInput } from "../../../config/types.secrets.js";
|
||||
@ -38,6 +39,7 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
env: process.env,
|
||||
});
|
||||
let nextConfig = params.nextConfig;
|
||||
const agentDir = resolveAgentDir(nextConfig, resolveDefaultAgentId(nextConfig));
|
||||
const requestedSecretInputMode = normalizeSecretInputModeInput(opts.secretInputMode);
|
||||
if (opts.secretInputMode && !requestedSecretInputMode) {
|
||||
runtime.error('Invalid --secret-input-mode. Use "plaintext" or "ref".');
|
||||
@ -76,6 +78,7 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
const resolveApiKey = (input: Parameters<typeof resolveNonInteractiveApiKey>[0]) =>
|
||||
resolveNonInteractiveApiKey({
|
||||
...input,
|
||||
agentDir,
|
||||
secretInputMode: requestedSecretInputMode,
|
||||
});
|
||||
const toApiKeyCredential = (params: {
|
||||
@ -179,6 +182,7 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
baseConfig,
|
||||
opts,
|
||||
runtime,
|
||||
agentDir,
|
||||
apiKeyStorageOptions,
|
||||
resolveApiKey,
|
||||
maybeSetResolvedApiKey,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user