FN-7761: fix Grok CLI auth to use logged-in CLI instead of requiring API key
Packaged fn serve/daemon/dashboard hosts previously failed with a misleading missing-API-key error for grok-cli agents even though the operator was already logged in via the Grok CLI. This fixes routing so those hosts eagerly ensure the bundled Grok Runtime plugin is installed/loaded before session creation, and no longer silently falls back to the key-requiring direct endpoint when no key is visible. - Eagerly ensure the bundled fusion-plugin-grok-runtime in serve, daemon, and dashboard commands before loadAllPlugins() so runtime id "grok" is available on fresh installs without manual plugin-settings setup. - agent-session-helpers.ts: deriveGrokRuntimeHintForNoVisibleKey now throws an actionable error (naming both remediations: install/enable the Grok CLI runtime plugin, or set GROK_API_KEY) instead of silently falling through to the key-requiring pi/openai-completions path when the runtime can't be loaded. - Update docs/grok-cli-contract.md to document the FN-7761 packaged-host wiring and new no-silent-fallback behavior. - Add regression tests for the packaged bootstrap behavior and bundled-plugin install path. - Add changeset for @runfusion/fusion (patch, category: fix). Files changed: .changeset/fn-7761-grok-cli-packaged-routing.md | 7 +++++ docs/grok-cli-contract.md | 19 +++++++++---- .../__tests__/grok-runtime-bootstrap.test.ts | 31 ++++++++++++++++++++++ packages/cli/src/commands/daemon.ts | 17 +++++++++++- packages/cli/src/commands/dashboard.ts | 20 +++++++++++++- packages/cli/src/commands/serve.ts | 19 +++++++++++-- .../__tests__/bundled-plugin-install.test.ts | 17 ++++++++++++ .../src/__tests__/grok-runtime-routing.test.ts | 17 ++++++------ packages/engine/src/agent-session-helpers.ts | 15 +++++++++-- 9 files changed, 142 insertions(+), 20 deletions(-) Fusion-Task-Id: FN-7761 Fusion-Task-Lineage: 3be5f054-965c-4e8a-ad91-6e61d4dc4a42 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7761-grok-cli-packaged-routing.md
Normal file
7
.changeset/fn-7761-grok-cli-packaged-routing.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Route Grok CLI models through the logged-in grok CLI in packaged hosts without requiring GROK_API_KEY.
|
||||||
|
category: fix
|
||||||
|
dev: Eagerly ensures the bundled Grok runtime in serve/daemon/dashboard and blocks silent direct-endpoint fallback when no key is visible.
|
||||||
@@ -134,7 +134,7 @@ Notes:
|
|||||||
`GrokCliProviderCard.tsx`) is out of scope for this task and is not
|
`GrokCliProviderCard.tsx`) is out of scope for this task and is not
|
||||||
modified here.
|
modified here.
|
||||||
|
|
||||||
## Wiring (resolved — FN-7725, extended by FN-7753/FN-7758)
|
## Wiring (resolved — FN-7725, extended by FN-7753/FN-7758/FN-7761)
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
FNXC:GrokCli 2026-07-09-00:00:
|
FNXC:GrokCli 2026-07-09-00:00:
|
||||||
@@ -160,7 +160,7 @@ via the dashboard's agent **Runtime Source → Runtime** picker
|
|||||||
plugin runtime, including the bundled Grok Runtime plugin's `runtimeId:
|
plugin runtime, including the bundled Grok Runtime plugin's `runtimeId:
|
||||||
"grok"`, with no Grok-specific code required).
|
"grok"`, with no Grok-specific code required).
|
||||||
|
|
||||||
**Automatic no-key fallback (FN-7753/FN-7758):** when `createResolvedAgentSession()` sees
|
**Automatic no-key fallback (FN-7753/FN-7758/FN-7761):** when `createResolvedAgentSession()` sees
|
||||||
all of the following, it derives the same effective `runtimeHint: "grok"` before
|
all of the following, it derives the same effective `runtimeHint: "grok"` before
|
||||||
calling `resolveRuntime()`:
|
calling `resolveRuntime()`:
|
||||||
|
|
||||||
@@ -172,6 +172,11 @@ calling `resolveRuntime()`:
|
|||||||
`~/.grok/user-settings.json`'s `apiKey` field; and
|
`~/.grok/user-settings.json`'s `apiKey` field; and
|
||||||
4. the bundled Grok Runtime plugin has registered runtime id `"grok"`.
|
4. the bundled Grok Runtime plugin has registered runtime id `"grok"`.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
FNXC:GrokCliRouting 2026-07-09-23:05:
|
||||||
|
FN-7761 closes the packaged-host availability gap. Packaged `fn serve`, `fn daemon`, and `fn dashboard` now eagerly ensure the bundled `fusion-plugin-grok-runtime` before `loadAllPlugins()`, so fresh installs expose runtime id `"grok"` to the shared session seam without requiring the operator to first open plugin settings. If the runtime still cannot be loaded, Fusion fails before pi session creation with a dual-remediation error: install/enable the Grok CLI runtime plugin, or set `GROK_API_KEY` to use the direct xAI endpoint.
|
||||||
|
-->
|
||||||
|
|
||||||
FN-7758 also requires dashboard Chat/QuickChat and room responders to forward
|
FN-7758 also requires dashboard Chat/QuickChat and room responders to forward
|
||||||
the configured default provider/model into this same session seam when a send has
|
the configured default provider/model into this same session seam when a send has
|
||||||
no explicit model and no bound agent runtime model. That keeps the no-key routing
|
no explicit model and no bound agent runtime model. That keeps the no-key routing
|
||||||
@@ -180,8 +185,12 @@ chat, QuickChat, and room responder surfaces instead of letting model-less chat
|
|||||||
bypass the auto-derive by omitting `defaultProvider`.
|
bypass the auto-derive by omitting `defaultProvider`.
|
||||||
|
|
||||||
If a Fusion-visible key exists, the direct xAI OpenAI-compatible endpoint remains
|
If a Fusion-visible key exists, the direct xAI OpenAI-compatible endpoint remains
|
||||||
the default. If the Grok runtime is not registered, Fusion leaves the session on
|
the default. If the Grok runtime is not registered or cannot be loaded in the
|
||||||
the existing PI/direct path rather than inventing a separate routing mode.
|
no-visible-key `grok-cli` case, Fusion no longer leaves the session on the
|
||||||
|
existing PI/direct path because that path produces the misleading missing-key
|
||||||
|
error. Instead it raises an actionable error that names both supported recovery
|
||||||
|
paths: install/enable the Grok CLI runtime plugin so the logged-in `grok` CLI
|
||||||
|
owns auth, or set `GROK_API_KEY` so the direct xAI endpoint can authenticate.
|
||||||
|
|
||||||
**Exact seam:** `packages/engine/src/agent-session-helpers.ts`'s
|
**Exact seam:** `packages/engine/src/agent-session-helpers.ts`'s
|
||||||
`extractRuntimeHint(runtimeConfig)` reads that hint from the assigned agent's
|
`extractRuntimeHint(runtimeConfig)` reads that hint from the assigned agent's
|
||||||
@@ -215,7 +224,7 @@ additive change," formalizing + testing + documenting the already-working
|
|||||||
path is lower risk and closes the actual gap (an *exercised* path, not just
|
path is lower risk and closes the actual gap (an *exercised* path, not just
|
||||||
an implemented adapter) without adding new user-facing config surface.
|
an implemented adapter) without adding new user-facing config surface.
|
||||||
|
|
||||||
**Model plumbing (FN-7753/FN-7758):** for the automatic no-key fallback, the selected
|
**Model plumbing (FN-7753/FN-7758/FN-7761):** for the automatic no-key fallback, the selected
|
||||||
`grok-cli/*` model id is preserved through `AgentRuntimeOptions.defaultModelId`
|
`grok-cli/*` model id is preserved through `AgentRuntimeOptions.defaultModelId`
|
||||||
(or promoted from `fallbackModelId` when the fallback provider is the grok-cli
|
(or promoted from `fallbackModelId` when the fallback provider is the grok-cli
|
||||||
selection), normalized by stripping a leading `grok-cli/` (or `grok/`) prefix,
|
selection), normalized by stripping a leading `grok-cli/` (or `grok/`) prefix,
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const commandsDir = resolve(__dirname, "..");
|
||||||
|
|
||||||
|
function readCommand(command: "serve" | "daemon" | "dashboard"): string {
|
||||||
|
return readFileSync(resolve(commandsDir, `${command}.ts`), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:GrokCliRouting 2026-07-09-23:05:
|
||||||
|
* FN-7761 regression guard for packaged host bootstrap. The Grok helper must run before loadAllPlugins() in each long-lived CLI host so the enabled bundled runtime is loaded and getRuntimeById("grok") can resolve before chat/executor sessions try to route grok-cli/no-key messages.
|
||||||
|
*/
|
||||||
|
describe("Grok CLI runtime packaged bootstrap", () => {
|
||||||
|
for (const command of ["serve", "daemon", "dashboard"] as const) {
|
||||||
|
it(`${command} eagerly ensures the bundled Grok runtime before loading enabled plugins`, () => {
|
||||||
|
const source = readCommand(command);
|
||||||
|
const importIndex = source.indexOf("ensureBundledGrokRuntimePluginInstalled");
|
||||||
|
const ensureIndex = source.indexOf("ensureBundledGrokRuntimePluginInstalled(pluginStore, pluginLoader)");
|
||||||
|
const loadIndex = source.indexOf("pluginLoader.loadAllPlugins()");
|
||||||
|
|
||||||
|
expect(importIndex).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(ensureIndex).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(loadIndex).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(ensureIndex).toBeLessThan(loadIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -76,7 +76,7 @@ import { resolveSelfExtension } from "./self-extension.js";
|
|||||||
import { wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
|
import { wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
|
||||||
import { getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
|
import { getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
|
||||||
import { resolveProject } from "../project-context.js";
|
import { resolveProject } from "../project-context.js";
|
||||||
import { ensureBundledDependencyGraphPluginInstalled } from "../plugins/bundled-plugin-install.js";
|
import { ensureBundledDependencyGraphPluginInstalled, ensureBundledGrokRuntimePluginInstalled } from "../plugins/bundled-plugin-install.js";
|
||||||
import { handleOpencodeGoApiKeySaved, syncStartupModels } from "./startup-model-sync.js";
|
import { handleOpencodeGoApiKeySaved, syncStartupModels } from "./startup-model-sync.js";
|
||||||
import { registerCustomProviders, reregisterCustomProviders } from "./custom-provider-registry.js";
|
import { registerCustomProviders, reregisterCustomProviders } from "./custom-provider-registry.js";
|
||||||
import { ensureCwdProjectRegistered } from "./ensure-project-registered.js";
|
import { ensureCwdProjectRegistered } from "./ensure-project-registered.js";
|
||||||
@@ -521,6 +521,21 @@ export async function runDaemon(opts: DaemonOptions = {}) {
|
|||||||
console.warn(`[plugins] Failed to auto-install bundled Dependency Graph plugin: ${err instanceof Error ? err.message : err}`);
|
console.warn(`[plugins] Failed to auto-install bundled Dependency Graph plugin: ${err instanceof Error ? err.message : err}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:GrokCliRouting 2026-07-09-23:05:
|
||||||
|
* FN-7761: packaged daemon sessions must load the bundled Grok CLI runtime before executors/reviewers create sessions, so grok-cli/no-key routing uses the logged-in `grok` CLI instead of pi's key-requiring direct endpoint.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
const installStatus = await ensureBundledGrokRuntimePluginInstalled(pluginStore, pluginLoader);
|
||||||
|
if (installStatus === "installed") {
|
||||||
|
console.log("[plugins] Installed bundled Grok CLI runtime plugin");
|
||||||
|
} else if (installStatus === "missing-bundle") {
|
||||||
|
console.warn("[plugins] Bundled Grok CLI runtime plugin was not found in this build");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[plugins] Failed to auto-install bundled Grok CLI runtime plugin: ${err instanceof Error ? err.message : err}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-load all enabled plugins so runtime UI (NewAgentDialog, AgentDetailView)
|
// Auto-load all enabled plugins so runtime UI (NewAgentDialog, AgentDetailView)
|
||||||
// can discover installed runtimes like Hermes and OpenClaw.
|
// can discover installed runtimes like Hermes and OpenClaw.
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ import {
|
|||||||
} from "./llama-cpp-extension.js";
|
} from "./llama-cpp-extension.js";
|
||||||
import { getCachedUpdateStatus, isUpdateCheckEnabled } from "../update-cache.js";
|
import { getCachedUpdateStatus, isUpdateCheckEnabled } from "../update-cache.js";
|
||||||
import { resolveSelfExtension } from "./self-extension.js";
|
import { resolveSelfExtension } from "./self-extension.js";
|
||||||
import { ensureBundledDependencyGraphPluginInstalled, ensureBundledPluginInstalled, isBundledPluginId } from "../plugins/bundled-plugin-install.js";
|
import { ensureBundledDependencyGraphPluginInstalled, ensureBundledGrokRuntimePluginInstalled, ensureBundledPluginInstalled, isBundledPluginId } from "../plugins/bundled-plugin-install.js";
|
||||||
import { registerCustomProviders, reregisterCustomProviders } from "./custom-provider-registry.js";
|
import { registerCustomProviders, reregisterCustomProviders } from "./custom-provider-registry.js";
|
||||||
import { handleOpencodeGoApiKeySaved, syncStartupModels } from "./startup-model-sync.js";
|
import { handleOpencodeGoApiKeySaved, syncStartupModels } from "./startup-model-sync.js";
|
||||||
import { DashboardTUI, DashboardLogSink, isTTYAvailable, type SystemInfo, type GitStatus, type GitCommit, type GitCommitDetail, type GitBranch, type GitWorktree, type FileEntry, type FileReadResult, type TaskStep as TUITaskStep, type TaskLogEntry as TUITaskLogEntry, type TaskDetailData, type TaskEvent } from "./dashboard-tui/index.js";
|
import { DashboardTUI, DashboardLogSink, isTTYAvailable, type SystemInfo, type GitStatus, type GitCommit, type GitCommitDetail, type GitBranch, type GitWorktree, type FileEntry, type FileReadResult, type TaskStep as TUITaskStep, type TaskLogEntry as TUITaskLogEntry, type TaskDetailData, type TaskEvent } from "./dashboard-tui/index.js";
|
||||||
@@ -1320,6 +1320,24 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:GrokCliRouting 2026-07-09-23:05:
|
||||||
|
* FN-7761: packaged `fn dashboard` must make fusion-plugin-grok-runtime enabled and loadable before chat sends. Without this eager Grok-scoped bootstrap, grok-cli/no-key messages bypass the logged-in `grok` CLI and hit pi's direct endpoint missing-key path.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
const installStatus = await ensureBundledGrokRuntimePluginInstalled(pluginStore, pluginLoader);
|
||||||
|
if (installStatus === "installed") {
|
||||||
|
logSink.log("Installed bundled Grok CLI runtime plugin", "plugins");
|
||||||
|
} else if (installStatus === "missing-bundle") {
|
||||||
|
logSink.log("Bundled Grok CLI runtime plugin was not found in this build", "plugins");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logSink.log(
|
||||||
|
`Failed to auto-install bundled Grok CLI runtime plugin: ${err instanceof Error ? err.message : err}`,
|
||||||
|
"plugins",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { loaded, errors } = await pluginLoader.loadAllPlugins();
|
const { loaded, errors } = await pluginLoader.loadAllPlugins();
|
||||||
logSink.log(`Loaded ${loaded} plugins (${errors} errors)`, "plugins");
|
logSink.log(`Loaded ${loaded} plugins (${errors} errors)`, "plugins");
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ import {
|
|||||||
import { resolveSelfExtension } from "./self-extension.js";
|
import { resolveSelfExtension } from "./self-extension.js";
|
||||||
import { registerCustomProviders, reregisterCustomProviders } from "./custom-provider-registry.js";
|
import { registerCustomProviders, reregisterCustomProviders } from "./custom-provider-registry.js";
|
||||||
import { handleOpencodeGoApiKeySaved, syncStartupModels } from "./startup-model-sync.js";
|
import { handleOpencodeGoApiKeySaved, syncStartupModels } from "./startup-model-sync.js";
|
||||||
import { ensureBundledDependencyGraphPluginInstalled, ensureBundledPluginInstalled, isBundledPluginId } from "../plugins/bundled-plugin-install.js";
|
import { ensureBundledDependencyGraphPluginInstalled, ensureBundledGrokRuntimePluginInstalled, ensureBundledPluginInstalled, isBundledPluginId } from "../plugins/bundled-plugin-install.js";
|
||||||
import { ensureCwdProjectRegistered } from "./ensure-project-registered.js";
|
import { ensureCwdProjectRegistered } from "./ensure-project-registered.js";
|
||||||
|
|
||||||
const DIAGNOSTIC_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
|
const DIAGNOSTIC_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
|
||||||
@@ -546,7 +546,22 @@ export async function runServe(
|
|||||||
console.warn(`[plugins] Failed to auto-install bundled Dependency Graph plugin: ${err instanceof Error ? err.message : err}`);
|
console.warn(`[plugins] Failed to auto-install bundled Dependency Graph plugin: ${err instanceof Error ? err.message : err}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazy-install hook for bundled runtime plugins (Hermes/OpenClaw/Paperclip).
|
/*
|
||||||
|
* FNXC:GrokCliRouting 2026-07-09-23:05:
|
||||||
|
* FN-7761: packaged `fn serve` must make the bundled Grok CLI runtime discoverable before any message-sending lane starts. Otherwise grok-cli/no-key selections fall through to the direct xAI endpoint and incorrectly ask for GROK_API_KEY even though the `grok` CLI owns auth.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
const installStatus = await ensureBundledGrokRuntimePluginInstalled(pluginStore, pluginLoader);
|
||||||
|
if (installStatus === "installed") {
|
||||||
|
console.log("[plugins] Installed bundled Grok CLI runtime plugin");
|
||||||
|
} else if (installStatus === "missing-bundle") {
|
||||||
|
console.warn("[plugins] Bundled Grok CLI runtime plugin was not found in this build");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[plugins] Failed to auto-install bundled Grok CLI runtime plugin: ${err instanceof Error ? err.message : err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy-install hook for bundled runtime plugins (Hermes/OpenClaw/Paperclip/Grok).
|
||||||
const ensureBundledPluginInstalledCallback = async (pluginId: string): Promise<boolean> => {
|
const ensureBundledPluginInstalledCallback = async (pluginId: string): Promise<boolean> => {
|
||||||
if (!isBundledPluginId(pluginId)) {
|
if (!isBundledPluginId(pluginId)) {
|
||||||
console.warn(`[plugins] ensureBundledPluginInstalled: unknown bundled plugin id "${pluginId}"`);
|
console.warn(`[plugins] ensureBundledPluginInstalled: unknown bundled plugin id "${pluginId}"`);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
BUNDLED_PLUGIN_IDS,
|
BUNDLED_PLUGIN_IDS,
|
||||||
ensureBundledDependencyGraphPluginInstalled,
|
ensureBundledDependencyGraphPluginInstalled,
|
||||||
ensureBundledCursorRuntimePluginInstalled,
|
ensureBundledCursorRuntimePluginInstalled,
|
||||||
|
ensureBundledGrokRuntimePluginInstalled,
|
||||||
ensureBundledPluginInstalled,
|
ensureBundledPluginInstalled,
|
||||||
type BundledPluginDirResolver,
|
type BundledPluginDirResolver,
|
||||||
} from "../bundled-plugin-install.js";
|
} from "../bundled-plugin-install.js";
|
||||||
@@ -47,6 +48,7 @@ import {
|
|||||||
const BUNDLED_PLUGIN_ID = "fusion-plugin-dependency-graph";
|
const BUNDLED_PLUGIN_ID = "fusion-plugin-dependency-graph";
|
||||||
const HERMES_PLUGIN_ID = "fusion-plugin-hermes-runtime";
|
const HERMES_PLUGIN_ID = "fusion-plugin-hermes-runtime";
|
||||||
const CURSOR_PLUGIN_ID = "fusion-plugin-cursor-runtime";
|
const CURSOR_PLUGIN_ID = "fusion-plugin-cursor-runtime";
|
||||||
|
const GROK_PLUGIN_ID = "fusion-plugin-grok-runtime";
|
||||||
const ROADMAP_PLUGIN_ID = "fusion-plugin-roadmap";
|
const ROADMAP_PLUGIN_ID = "fusion-plugin-roadmap";
|
||||||
const REPORTS_PLUGIN_ID = "fusion-plugin-reports";
|
const REPORTS_PLUGIN_ID = "fusion-plugin-reports";
|
||||||
const LINEAR_IMPORT_PLUGIN_ID = "fusion-plugin-linear-import";
|
const LINEAR_IMPORT_PLUGIN_ID = "fusion-plugin-linear-import";
|
||||||
@@ -189,6 +191,7 @@ describe("ensureBundledPluginInstalled (host-agnostic shared helper)", () => {
|
|||||||
expect(BUNDLED_PLUGIN_IDS).toContain(REPORTS_PLUGIN_ID);
|
expect(BUNDLED_PLUGIN_IDS).toContain(REPORTS_PLUGIN_ID);
|
||||||
expect(BUNDLED_PLUGIN_IDS).toContain(LINEAR_IMPORT_PLUGIN_ID);
|
expect(BUNDLED_PLUGIN_IDS).toContain(LINEAR_IMPORT_PLUGIN_ID);
|
||||||
expect(BUNDLED_PLUGIN_IDS).toContain(HERMES_PLUGIN_ID);
|
expect(BUNDLED_PLUGIN_IDS).toContain(HERMES_PLUGIN_ID);
|
||||||
|
expect(BUNDLED_PLUGIN_IDS).toContain(GROK_PLUGIN_ID);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fresh install: registers and loads the plugin when not in DB (CLI-shaped resolver)", async () => {
|
it("fresh install: registers and loads the plugin when not in DB (CLI-shaped resolver)", async () => {
|
||||||
@@ -350,6 +353,20 @@ describe("ensureBundledPluginInstalled (host-agnostic shared helper)", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("registers and loads the Grok runtime through the dedicated helper", async () => {
|
||||||
|
setupBundleExists(cliShapedResolver, { id: GROK_PLUGIN_ID });
|
||||||
|
const store = makePluginStore();
|
||||||
|
const loader = makePluginLoader();
|
||||||
|
|
||||||
|
const result = await ensureBundledGrokRuntimePluginInstalled(store as never, loader as never, cliShapedResolver);
|
||||||
|
|
||||||
|
expect(result).toBe("installed");
|
||||||
|
expect(store.registerPlugin).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ manifest: expect.objectContaining({ id: GROK_PLUGIN_ID }) }),
|
||||||
|
);
|
||||||
|
expect(loader.loadPlugin).toHaveBeenCalledWith(GROK_PLUGIN_ID);
|
||||||
|
});
|
||||||
|
|
||||||
it("registers Dependency Graph through the deprecated dedicated helper", async () => {
|
it("registers Dependency Graph through the deprecated dedicated helper", async () => {
|
||||||
setupBundleExists(cliShapedResolver);
|
setupBundleExists(cliShapedResolver);
|
||||||
const store = makePluginStore();
|
const store = makePluginStore();
|
||||||
|
|||||||
@@ -295,27 +295,26 @@ describe("Grok CLI runtime routing (FN-7725)", () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps grok-cli on pi when no key is visible but the Grok runtime is not registered", async () => {
|
it("surfaces an actionable dual-remediation error instead of falling through to the key-requiring pi runtime when the Grok runtime is unavailable", async () => {
|
||||||
vi.mocked(fusionCore.isGrokApiKeyFusionVisible).mockReturnValue(false);
|
vi.mocked(fusionCore.isGrokApiKeyFusionVisible).mockReturnValue(false);
|
||||||
const pluginRunner = createMockPluginRunner({
|
const pluginRunner = createMockPluginRunner({
|
||||||
getRuntimeById: vi.fn().mockReturnValue(undefined),
|
getRuntimeById: vi.fn().mockReturnValue(undefined),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await createResolvedAgentSession({
|
/*
|
||||||
|
* FNXC:GrokCliRouting 2026-07-09-23:05:
|
||||||
|
* FN-7761 symptom reproduction: packaged hosts previously left fusion-plugin-grok-runtime uninstalled, so getRuntimeById("grok") was undefined and grok-cli/no-key sessions fell through to pi's direct endpoint. The fixed invariant forbids that silent fallback and tells operators to either enable the Grok CLI runtime or set GROK_API_KEY.
|
||||||
|
*/
|
||||||
|
await expect(createResolvedAgentSession({
|
||||||
sessionPurpose: "executor",
|
sessionPurpose: "executor",
|
||||||
pluginRunner,
|
pluginRunner,
|
||||||
cwd: "/tmp/project",
|
cwd: "/tmp/project",
|
||||||
defaultProvider: "grok-cli",
|
defaultProvider: "grok-cli",
|
||||||
defaultModelId: "grok-4.5",
|
defaultModelId: "grok-4.5",
|
||||||
systemPrompt: "no-runtime-registered",
|
systemPrompt: "no-runtime-registered",
|
||||||
});
|
})).rejects.toThrow(/Install and enable the Grok CLI runtime plugin, or set GROK_API_KEY/);
|
||||||
|
|
||||||
expect(result.runtimeId).toBe("pi");
|
expect(mockCreateFnAgent).not.toHaveBeenCalled();
|
||||||
expect(result.wasConfigured).toBe(false);
|
|
||||||
expect(mockCreateFnAgent).toHaveBeenCalledWith(expect.objectContaining({
|
|
||||||
defaultProvider: "grok-cli",
|
|
||||||
defaultModelId: "grok-4.5",
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("auto-routes heartbeat/room responder grok-cli defaults to the Grok runtime when no Fusion-visible key exists", async () => {
|
it("auto-routes heartbeat/room responder grok-cli defaults to the Grok runtime when no Fusion-visible key exists", async () => {
|
||||||
|
|||||||
@@ -163,6 +163,13 @@ function stripGrokCliModelProviderPrefix(modelId: string | undefined): string |
|
|||||||
: normalized;
|
: normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildMissingGrokRuntimeError(): Error {
|
||||||
|
return new Error(
|
||||||
|
"Grok CLI models require the bundled Grok CLI runtime when no Fusion-visible GROK_API_KEY is set. "
|
||||||
|
+ "Install and enable the Grok CLI runtime plugin, or set GROK_API_KEY to use the direct xAI endpoint.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function deriveGrokRuntimeHintForNoVisibleKey(
|
function deriveGrokRuntimeHintForNoVisibleKey(
|
||||||
runtimeOptions: AgentRuntimeOptions,
|
runtimeOptions: AgentRuntimeOptions,
|
||||||
pluginRunner: PluginRunner | undefined,
|
pluginRunner: PluginRunner | undefined,
|
||||||
@@ -171,10 +178,11 @@ function deriveGrokRuntimeHintForNoVisibleKey(
|
|||||||
&& runtimeOptions.fallbackProvider !== GROK_CLI_PROVIDER_ID) return undefined;
|
&& runtimeOptions.fallbackProvider !== GROK_CLI_PROVIDER_ID) return undefined;
|
||||||
if (isGrokApiKeyFusionVisible()) return undefined;
|
if (isGrokApiKeyFusionVisible()) return undefined;
|
||||||
try {
|
try {
|
||||||
return pluginRunner?.getRuntimeById("grok") ? "grok" : undefined;
|
if (pluginRunner?.getRuntimeById("grok")) return "grok";
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
throw buildMissingGrokRuntimeError();
|
||||||
}
|
}
|
||||||
|
throw buildMissingGrokRuntimeError();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyGrokCliNoKeyRuntimeOptions(
|
function applyGrokCliNoKeyRuntimeOptions(
|
||||||
@@ -435,6 +443,9 @@ export async function createResolvedAgentSession(
|
|||||||
FN-7758 extends the no-visible-key invariant to configured fallback models. Pi resolves fallback models
|
FN-7758 extends the no-visible-key invariant to configured fallback models. Pi resolves fallback models
|
||||||
during session creation and prompt-time swaps through the key-requiring provider registry, so a grok-cli
|
during session creation and prompt-time swaps through the key-requiring provider registry, so a grok-cli
|
||||||
fallback must select the Grok CLI runtime up front and promote the fallback model into the CLI session.
|
fallback must select the Grok CLI runtime up front and promote the fallback model into the CLI session.
|
||||||
|
|
||||||
|
FNXC:GrokCliRouting 2026-07-09-23:05:
|
||||||
|
FN-7761 closes the packaged serve/daemon/dashboard gap: if grok-cli is selected and no Fusion-visible key exists, this seam must never silently fall through to the key-requiring pi/openai-completions runtime when the Grok plugin was not pre-installed. The hosts eagerly install/load the bundled runtime; if that genuinely fails, throw an operator-actionable error naming the two supported remediations.
|
||||||
*/
|
*/
|
||||||
const autoGrokRuntimeHint = !useMockRuntime && !runtimeHint
|
const autoGrokRuntimeHint = !useMockRuntime && !runtimeHint
|
||||||
? deriveGrokRuntimeHintForNoVisibleKey(runtimeOptions, pluginRunner)
|
? deriveGrokRuntimeHintForNoVisibleKey(runtimeOptions, pluginRunner)
|
||||||
|
|||||||
Reference in New Issue
Block a user