fix: infer --auth-choice from API key flags during non-interactive onboarding (#9241)
* fix: infer --auth-choice from API key flags during non-interactive onboarding When --anthropic-api-key (or other provider key flags) is passed without an explicit --auth-choice, the auth choice defaults to "skip", silently discarding the API key. This means the gateway starts without credentials and fails on every inbound message with "No API key found for provider". Add inferAuthChoiceFromFlags() to derive the correct auth choice from whichever provider API key flag was supplied, so credentials are persisted to auth-profiles.json as expected. Fixes #8481 * fix: infer auth choice from API key flags (#8484) (thanks @f-trycua) * refactor: centralize auth choice inference flags (#8484) (thanks @f-trycua) --------- Co-authored-by: f-trycua <f@trycua.com>main
parent
385a7eba33
commit
22927b0834
|
|
@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Web UI: fix agent model selection saves for default/non-default agents and wrap long workspace paths. Thanks @Takhoffman.
|
||||
- Web UI: resolve header logo path when `gateway.controlUi.basePath` is set. (#7178) Thanks @Yeom-JinHo.
|
||||
- Web UI: apply button styling to the new-messages indicator.
|
||||
- Onboarding: infer auth choice from non-interactive API key flags. (#8484) Thanks @f-trycua.
|
||||
- Security: keep untrusted channel metadata out of system prompts (Slack/Discord). Thanks @KonstantinMirin.
|
||||
- Security: enforce sandboxed media paths for message tool attachments. (#9182) Thanks @victormier.
|
||||
- Security: require explicit credentials for gateway URL overrides to prevent credential leakage. (#8113) Thanks @victormier.
|
||||
|
|
|
|||
|
|
@ -96,4 +96,96 @@ describe("onboard (non-interactive): Cloudflare AI Gateway", () => {
|
|||
process.env.OPENCLAW_GATEWAY_PASSWORD = prev.password;
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
it("infers auth choice from API key flags", async () => {
|
||||
const prev = {
|
||||
home: process.env.HOME,
|
||||
stateDir: process.env.OPENCLAW_STATE_DIR,
|
||||
configPath: process.env.OPENCLAW_CONFIG_PATH,
|
||||
skipChannels: process.env.OPENCLAW_SKIP_CHANNELS,
|
||||
skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER,
|
||||
skipCron: process.env.OPENCLAW_SKIP_CRON,
|
||||
skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST,
|
||||
token: process.env.OPENCLAW_GATEWAY_TOKEN,
|
||||
password: process.env.OPENCLAW_GATEWAY_PASSWORD,
|
||||
};
|
||||
|
||||
process.env.OPENCLAW_SKIP_CHANNELS = "1";
|
||||
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
|
||||
process.env.OPENCLAW_SKIP_CRON = "1";
|
||||
process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
|
||||
delete process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||
|
||||
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-onboard-cf-gateway-infer-"));
|
||||
process.env.HOME = tempHome;
|
||||
process.env.OPENCLAW_STATE_DIR = tempHome;
|
||||
process.env.OPENCLAW_CONFIG_PATH = path.join(tempHome, "openclaw.json");
|
||||
vi.resetModules();
|
||||
|
||||
const runtime = {
|
||||
log: () => {},
|
||||
error: (msg: string) => {
|
||||
throw new Error(msg);
|
||||
},
|
||||
exit: (code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
|
||||
await runNonInteractiveOnboarding(
|
||||
{
|
||||
nonInteractive: true,
|
||||
cloudflareAiGatewayAccountId: "cf-account-id",
|
||||
cloudflareAiGatewayGatewayId: "cf-gateway-id",
|
||||
cloudflareAiGatewayApiKey: "cf-gateway-test-key",
|
||||
skipHealth: true,
|
||||
skipChannels: true,
|
||||
skipSkills: true,
|
||||
json: true,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
const { CONFIG_PATH } = await import("../config/config.js");
|
||||
const cfg = JSON.parse(await fs.readFile(CONFIG_PATH, "utf8")) as {
|
||||
auth?: {
|
||||
profiles?: Record<string, { provider?: string; mode?: string }>;
|
||||
};
|
||||
agents?: { defaults?: { model?: { primary?: string } } };
|
||||
};
|
||||
|
||||
expect(cfg.auth?.profiles?.["cloudflare-ai-gateway:default"]?.provider).toBe(
|
||||
"cloudflare-ai-gateway",
|
||||
);
|
||||
expect(cfg.auth?.profiles?.["cloudflare-ai-gateway:default"]?.mode).toBe("api_key");
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe("cloudflare-ai-gateway/claude-sonnet-4-5");
|
||||
|
||||
const { ensureAuthProfileStore } = await import("../agents/auth-profiles.js");
|
||||
const store = ensureAuthProfileStore();
|
||||
const profile = store.profiles["cloudflare-ai-gateway:default"];
|
||||
expect(profile?.type).toBe("api_key");
|
||||
if (profile?.type === "api_key") {
|
||||
expect(profile.provider).toBe("cloudflare-ai-gateway");
|
||||
expect(profile.key).toBe("cf-gateway-test-key");
|
||||
expect(profile.metadata).toEqual({
|
||||
accountId: "cf-account-id",
|
||||
gatewayId: "cf-gateway-id",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await fs.rm(tempHome, { recursive: true, force: true });
|
||||
process.env.HOME = prev.home;
|
||||
process.env.OPENCLAW_STATE_DIR = prev.stateDir;
|
||||
process.env.OPENCLAW_CONFIG_PATH = prev.configPath;
|
||||
process.env.OPENCLAW_SKIP_CHANNELS = prev.skipChannels;
|
||||
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prev.skipGmail;
|
||||
process.env.OPENCLAW_SKIP_CRON = prev.skipCron;
|
||||
process.env.OPENCLAW_SKIP_CANVAS_HOST = prev.skipCanvas;
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN = prev.token;
|
||||
process.env.OPENCLAW_GATEWAY_PASSWORD = prev.password;
|
||||
}
|
||||
}, 60_000);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
resolveControlUiLinks,
|
||||
waitForGatewayReachable,
|
||||
} from "../onboard-helpers.js";
|
||||
import { inferAuthChoiceFromFlags } from "./local/auth-choice-inference.js";
|
||||
import { applyNonInteractiveAuthChoice } from "./local/auth-choice.js";
|
||||
import { installGatewayDaemonNonInteractive } from "./local/daemon-install.js";
|
||||
import { applyNonInteractiveGatewayConfig } from "./local/gateway-config.js";
|
||||
|
|
@ -49,7 +50,19 @@ export async function runNonInteractiveOnboardingLocal(params: {
|
|||
},
|
||||
};
|
||||
|
||||
const authChoice = opts.authChoice ?? "skip";
|
||||
const inferredAuthChoice = inferAuthChoiceFromFlags(opts);
|
||||
if (!opts.authChoice && inferredAuthChoice.matches.length > 1) {
|
||||
runtime.error(
|
||||
[
|
||||
"Multiple API key flags were provided for non-interactive onboarding.",
|
||||
"Use a single provider flag or pass --auth-choice explicitly.",
|
||||
`Flags: ${inferredAuthChoice.matches.map((match) => match.label).join(", ")}`,
|
||||
].join("\n"),
|
||||
);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
const authChoice = opts.authChoice ?? inferredAuthChoice.choice ?? "skip";
|
||||
const nextConfigAfterAuth = await applyNonInteractiveAuthChoice({
|
||||
nextConfig,
|
||||
authChoice,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import type { AuthChoice, OnboardOptions } from "../../onboard-types.js";
|
||||
|
||||
type AuthChoiceFlag = {
|
||||
flag: keyof AuthChoiceFlagOptions;
|
||||
authChoice: AuthChoice;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type AuthChoiceFlagOptions = Pick<
|
||||
OnboardOptions,
|
||||
| "anthropicApiKey"
|
||||
| "geminiApiKey"
|
||||
| "openaiApiKey"
|
||||
| "openrouterApiKey"
|
||||
| "aiGatewayApiKey"
|
||||
| "cloudflareAiGatewayApiKey"
|
||||
| "moonshotApiKey"
|
||||
| "kimiCodeApiKey"
|
||||
| "syntheticApiKey"
|
||||
| "veniceApiKey"
|
||||
| "zaiApiKey"
|
||||
| "xiaomiApiKey"
|
||||
| "minimaxApiKey"
|
||||
| "opencodeZenApiKey"
|
||||
>;
|
||||
|
||||
const AUTH_CHOICE_FLAG_MAP = [
|
||||
{ flag: "anthropicApiKey", authChoice: "apiKey", label: "--anthropic-api-key" },
|
||||
{ flag: "geminiApiKey", authChoice: "gemini-api-key", label: "--gemini-api-key" },
|
||||
{ flag: "openaiApiKey", authChoice: "openai-api-key", label: "--openai-api-key" },
|
||||
{ flag: "openrouterApiKey", authChoice: "openrouter-api-key", label: "--openrouter-api-key" },
|
||||
{ flag: "aiGatewayApiKey", authChoice: "ai-gateway-api-key", label: "--ai-gateway-api-key" },
|
||||
{
|
||||
flag: "cloudflareAiGatewayApiKey",
|
||||
authChoice: "cloudflare-ai-gateway-api-key",
|
||||
label: "--cloudflare-ai-gateway-api-key",
|
||||
},
|
||||
{ flag: "moonshotApiKey", authChoice: "moonshot-api-key", label: "--moonshot-api-key" },
|
||||
{ flag: "kimiCodeApiKey", authChoice: "kimi-code-api-key", label: "--kimi-code-api-key" },
|
||||
{ flag: "syntheticApiKey", authChoice: "synthetic-api-key", label: "--synthetic-api-key" },
|
||||
{ flag: "veniceApiKey", authChoice: "venice-api-key", label: "--venice-api-key" },
|
||||
{ flag: "zaiApiKey", authChoice: "zai-api-key", label: "--zai-api-key" },
|
||||
{ flag: "xiaomiApiKey", authChoice: "xiaomi-api-key", label: "--xiaomi-api-key" },
|
||||
{ flag: "minimaxApiKey", authChoice: "minimax-api", label: "--minimax-api-key" },
|
||||
{ flag: "opencodeZenApiKey", authChoice: "opencode-zen", label: "--opencode-zen-api-key" },
|
||||
] satisfies ReadonlyArray<AuthChoiceFlag>;
|
||||
|
||||
export type AuthChoiceInference = {
|
||||
choice?: AuthChoice;
|
||||
matches: AuthChoiceFlag[];
|
||||
};
|
||||
|
||||
// Infer auth choice from explicit provider API key flags.
|
||||
export function inferAuthChoiceFromFlags(opts: OnboardOptions): AuthChoiceInference {
|
||||
const matches = AUTH_CHOICE_FLAG_MAP.filter(({ flag }) => {
|
||||
const value = opts[flag];
|
||||
if (typeof value === "string") {
|
||||
return value.trim().length > 0;
|
||||
}
|
||||
return Boolean(value);
|
||||
});
|
||||
|
||||
return {
|
||||
choice: matches[0]?.authChoice,
|
||||
matches,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue