diff --git a/.changeset/fn-7624-github-onboarding-auth.md b/.changeset/fn-7624-github-onboarding-auth.md
new file mode 100644
index 0000000000..79afefa7e9
--- /dev/null
+++ b/.changeset/fn-7624-github-onboarding-auth.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Fix onboarding GitHub sign-in button erroring instead of starting GitHub auth.
+category: fix
+dev: The onboarding/settings GitHub step no longer offers dashboard-managed OAuth login when no `github` OAuth provider is registered (pi ships only anthropic/github-copilot/openai-codex); it now presents gh CLI (`gh auth login`) guidance. `/api/auth/login` returns a clear unknown-provider error for `github` instead of a misleading "model not found".
diff --git a/packages/dashboard/app/components/ModelOnboardingModal.tsx b/packages/dashboard/app/components/ModelOnboardingModal.tsx
index 8cffb4953b..d3b92f0316 100644
--- a/packages/dashboard/app/components/ModelOnboardingModal.tsx
+++ b/packages/dashboard/app/components/ModelOnboardingModal.tsx
@@ -2882,6 +2882,16 @@ export function ModelOnboardingModal({
)}
+ {/*
+ * FNXC:ProviderAuth 2026-07-07-00:00:
+ * FN-7624: no `github` OAuth provider is ever registered on any Fusion host (pi ships only
+ * anthropic/github-copilot/openai-codex), so this whole `!hasGithubProvider` branch must never
+ * render a dashboard OAuth login affordance (a previous "Connect OAuth (optional)" button here
+ * called handleLogin("github") unconditionally when the gh CLI was ready, which always failed
+ * against a non-existent provider). The gh-CLI-ready and not-ready copy below are the only
+ * offered actions; OAuth login only appears in the `hasGithubProvider` branch below, which is
+ * unreachable while no `github` provider is registered.
+ */}
{!hasGithubProvider ? (
@@ -2913,29 +2923,7 @@ export function ModelOnboardingModal({
? t("setup.continueWithGhCli", "Continue with gh CLI auth →")
: t("setup.continueWithoutGitHub", "Continue without GitHub →")}
- {isGitHubReadyViaCli && (
- (authActionInProgress === "github" || isGithubLoginInProgress) ? (
-
-
- {t("setup.waitingForOauthLogin", "Waiting for OAuth login…")}
-
- ) : (
-
handleLogin("github")}
- >
-
- {t("setup.connectOauthOptional", "Connect OAuth (optional)")}
-
- )
- )}
- {isGitHubReadyViaCli && (authActionInProgress === "github" || isGithubLoginInProgress) && loginInstructions.github && (
-
- )}
) : (
<>
diff --git a/packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx b/packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx
index 404b4f53e0..ebe9e80fec 100644
--- a/packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx
+++ b/packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx
@@ -1906,7 +1906,37 @@ describe("ModelOnboardingModal", () => {
expect(screen.getByTestId("onboarding-git-prerequisite")).toHaveTextContent("Install Git before project setup");
expect(screen.getByRole("button", { name: "Continue with gh CLI auth →" })).toBeTruthy();
- expect(screen.getByRole("button", { name: /Connect OAuth/ })).toBeTruthy();
+ // FN-7624: no `github` OAuth provider is registered here (providers: []), so no dashboard
+ // OAuth login affordance may render — clicking it used to call handleLogin("github") against
+ // a provider that does not exist, surfacing a login error. Only the gh-CLI continue path is offered.
+ expect(screen.queryByRole("button", { name: /Connect OAuth/ })).toBeNull();
+ });
+
+ it("FN-7624: never renders a dashboard GitHub OAuth login affordance when no github OAuth provider is registered, across all gh CLI states", async () => {
+ const scenarios: Array<{ label: string; ghCli?: { available: boolean; authenticated: boolean } }> = [
+ { label: "gh CLI missing", ghCli: { available: false, authenticated: false } },
+ { label: "gh CLI unauthenticated", ghCli: { available: true, authenticated: false } },
+ { label: "gh CLI authenticated", ghCli: { available: true, authenticated: true } },
+ ];
+
+ for (const scenario of scenarios) {
+ mockFetchAuthStatus.mockResolvedValueOnce({
+ providers: [],
+ ghCli: scenario.ghCli,
+ });
+
+ const { unmount } = render( );
+
+ await navigateToGitHubStep();
+
+ // No OAuth login trigger of any kind should render — no "Connect GitHub OAuth", no
+ // "Connect OAuth (optional)", no leftover empty CTA wrapper — since providerAvailable is false.
+ expect(screen.queryByRole("button", { name: /Connect OAuth/ })).toBeNull();
+ expect(screen.queryByRole("button", { name: /Connect GitHub OAuth/ })).toBeNull();
+ expect(screen.queryByTestId("onboarding-github-connect-cta")).toBeNull();
+
+ unmount();
+ }
});
it("does not show a Git prerequisite shell when auth status fails to load", async () => {
diff --git a/packages/dashboard/src/__tests__/routes-auth.test.ts b/packages/dashboard/src/__tests__/routes-auth.test.ts
index 52f8a86e62..5e44a35166 100644
--- a/packages/dashboard/src/__tests__/routes-auth.test.ts
+++ b/packages/dashboard/src/__tests__/routes-auth.test.ts
@@ -2457,6 +2457,28 @@ describe("POST /auth/login", () => {
expect(res.body.error).toContain("Unknown provider");
});
+ it("FN-7624: returns a clear, non-misleading 400 for provider: \"github\" when only built-in OAuth providers are registered (never a 'model not found' style error)", async () => {
+ // Only the built-in dashboard OAuth providers are registered — no `github` provider exists,
+ // matching the real pi OAuth registry (anthropic / github-copilot / openai-codex only).
+ (authStorage.getOAuthProviders as ReturnType).mockReturnValue([
+ { id: "anthropic", name: "Anthropic" },
+ { id: "github-copilot", name: "GitHub Copilot" },
+ { id: "openai-codex", name: "OpenAI Codex" },
+ ]);
+
+ // This reproduces the original symptom: the onboarding GitHub button used to call
+ // POST /api/auth/login { provider: "github" } and surface a confusing error.
+ const res = await REQUEST(buildApp(), "POST", "/api/auth/login", JSON.stringify({ provider: "github" }), {
+ "Content-Type": "application/json",
+ });
+
+ expect(res.status).toBe(400);
+ expect(res.body.error).toContain("Unknown provider");
+ expect(res.body.error).toContain("github");
+ // Assert the error is never the misleading "model not found" style message the user reported.
+ expect(res.body.error.toLowerCase()).not.toContain("model not found");
+ });
+
it("returns 409 when login is already in progress for the same provider", async () => {
let releaseLogin: (() => void) | undefined;
(authStorage.login as ReturnType).mockImplementation(
diff --git a/packages/dashboard/src/routes/register-auth-routes.ts b/packages/dashboard/src/routes/register-auth-routes.ts
index cf38503980..f5402fce53 100644
--- a/packages/dashboard/src/routes/register-auth-routes.ts
+++ b/packages/dashboard/src/routes/register-auth-routes.ts
@@ -1149,7 +1149,19 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
const oauthProviders = storage.getOAuthProviders();
const found = oauthProviders.find((p) => p.id === provider || p.id === storageProvider);
if (!found) {
- throw badRequest(`Unknown provider: ${provider}`);
+ /*
+ * FNXC:ProviderAuth 2026-07-07-00:00:
+ * FN-7624: `github` is NOT a dashboard-managed OAuth provider — pi's OAuth registry only
+ * ships `anthropic`, `github-copilot`, and `openai-codex` (see @earendil-works/pi-ai/oauth via
+ * packages/engine/src/auth-storage.ts). Fusion's real GitHub integration is gh CLI / token
+ * based (`githubAuthMode: "gh-cli" | "token"`), so the onboarding/settings UI must never offer
+ * a dashboard OAuth login for `github`. If this branch is ever reached for `github` anyway
+ * (e.g. a stale client build), return a clear, actionable message naming the provider and that
+ * no OAuth flow exists for it — never a generic/misleading error like "model not found".
+ */
+ throw badRequest(
+ `Unknown provider: "${provider}" has no registered OAuth login flow. Registered dashboard OAuth providers are: ${oauthProviders.map((p) => p.id).join(", ") || "none"}. GitHub integration uses the GitHub CLI (run \`gh auth login\`) or a token, not dashboard OAuth.`,
+ );
}
const loginProvider = found.id === provider ? provider : storageProvider;