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