Compare commits

...

3 Commits

Author SHA1 Message Date
Gustavo Madeira Santana
fa22b155e0 refactor: centralize auth choice inference flags (#8484) (thanks @f-trycua) 2026-02-04 20:23:25 -05:00
Gustavo Madeira Santana
55acd021c6 fix: infer auth choice from API key flags (#8484) (thanks @f-trycua) 2026-02-04 20:23:25 -05:00
f-trycua
85bf3ed2ed 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
2026-02-04 20:22:51 -05:00
4 changed files with 174 additions and 1 deletions

View File

@@ -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.

View File

@@ -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);
});

View File

@@ -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,

View File

@@ -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,
};
}