diff --git a/brev/launch.sh b/brev/launch.sh index 9296266..6040400 100755 --- a/brev/launch.sh +++ b/brev/launch.sh @@ -566,19 +566,23 @@ } set_inference_route() { + # Set the default inference route to one model (nvidia-endpoints + kimi-k2.5). + # Canonical CLI per brev/welcome-ui/SERVER_ARCHITECTURE.md: cluster inference set + # (CLI_BIN is either "openshell" or "nemoclaw"; both accept the same subcommands.) + # Try canonical first, then "inference set" as fallback for other CLI versions. log "Configuring inference route..." - if "$CLI_BIN" inference set --provider nvidia-endpoints --model minimaxai/minimax-m2.5 >/dev/null 2>&1; then - log "Configured inference via '$CLI_BIN inference set'." + if "$CLI_BIN" cluster inference set --provider nvidia-endpoints --model moonshotai/kimi-k2.5 --no-verify >/dev/null 2>&1; then + log "Configured inference via '$CLI_BIN cluster inference set'." return fi - if "$CLI_BIN" cluster inference set --provider nvidia-endpoints --model minimaxai/minimax-m2.5 >/dev/null 2>&1; then - log "Configured inference via legacy '$CLI_BIN cluster inference set'." + if "$CLI_BIN" inference set --provider nvidia-endpoints --model moonshotai/kimi-k2.5 --no-verify >/dev/null 2>&1; then + log "Configured inference via '$CLI_BIN inference set'." return fi - log "Unable to configure inference route with either current or legacy CLI commands." + log "Unable to configure inference route with either 'cluster inference set' or 'inference set'." exit 1 } diff --git a/brev/welcome-ui/SERVER_ARCHITECTURE.md b/brev/welcome-ui/SERVER_ARCHITECTURE.md index fc6a762..a9543ab 100644 --- a/brev/welcome-ui/SERVER_ARCHITECTURE.md +++ b/brev/welcome-ui/SERVER_ARCHITECTURE.md @@ -126,6 +126,8 @@ The server operates in **two distinct modes** depending on sandbox readiness: | `LOG_FILE` | `/tmp/nemoclaw-sandbox-create.log` | Sandbox creation log (written by subprocess) | | `PROVIDER_CONFIG_CACHE` | `/tmp/nemoclaw-provider-config-cache.json` | Provider config values cache | | `OTHER_AGENTS_YAML` | `ROOT/other-agents.yaml` | YAML modal definition file | +| `INFERENCE_PROVIDERS_YAML` | `ROOT/inference-providers.yaml` | Inference provider picker and per-partner instructions | +| `NCP_LOGOS_DIR` | `SANDBOX_DIR/ncp-logos` | Partner and NVIDIA logos served at `/ncp-logos/*` | | `NEMOCLAW_IMAGE` | `ghcr.io/nvidia/openshell-community/sandboxes/openclaw-nvidia:local` | Optional image override | | `SANDBOX_PORT` | `18789` | Port the sandbox listens on (localhost) | @@ -845,6 +847,21 @@ steps: # Array of instruction sections 4. **Fallback:** If YAML fails to parse or PyYAML is not installed, the placeholder is replaced with an HTML comment: `` +### Inference Provider Picker (`inference-providers.yaml`) + +The server also renders `inference-providers.yaml` into HTML and injects it into `index.html`, replacing the `{{INFERENCE_PROVIDER_PICKER}}` placeholder. This provides: + +- **Picker screen:** "Choose your inference provider" with NVIDIA (free) in its own row and paid partners in a 5×2 grid. Each tile has `data-provider-id` for JS. +- **Partner instruction blocks:** For each partner, a `div#provider-instructions-{id}` with title, intro, and steps (same schema as other-agents steps). Shown when the user clicks a partner. + +**YAML schema:** Top-level `nvidia: { displayName, logoFile }` and `partners: [ { id, name, logoFile, instructions: { title, intro?, steps[] } } ]`. Logo filenames refer to files under `NCP_LOGOS_DIR`, served at `GET /ncp-logos/`. + +**Fallback:** If the file is missing or invalid, the placeholder is replaced with `` and the Install OpenClaw modal shows only the NVIDIA API key view (no picker). + +### NCP Logos Route (`GET /ncp-logos/*`) + +In welcome-ui mode (before sandbox is ready), `GET /ncp-logos/` serves static files from `SANDBOX_DIR/ncp-logos/`. Path traversal is rejected; only files under that directory are served. Used for NVIDIA and partner logos in the provider picker. MIME type for `.webp` is `image/webp`. + --- ## 9. Policy Management Pipeline @@ -1146,6 +1163,8 @@ All CLI commands are executed via `subprocess.run()` or `subprocess.Popen()`. Ev |------|------|----------| | `ROOT/index.html` | First request to `/` | Yes | | `ROOT/other-agents.yaml` | First request to `/` | No (graceful fallback) | +| `ROOT/inference-providers.yaml` | First request to `/` | No (graceful fallback) | +| `SANDBOX_DIR/ncp-logos/*` | `GET /ncp-logos/` | No (optional logos) | | `ROOT/styles.css` | Static file serving | Yes (for UI) | | `ROOT/app.js` | Static file serving | Yes (for UI) | | `SANDBOX_DIR/policy.yaml` | Sandbox creation | No (graceful fallback) | diff --git a/brev/welcome-ui/__tests__/cluster-inference.test.js b/brev/welcome-ui/__tests__/cluster-inference.test.js index 7db882d..8ae5f77 100644 --- a/brev/welcome-ui/__tests__/cluster-inference.test.js +++ b/brev/welcome-ui/__tests__/cluster-inference.test.js @@ -151,7 +151,7 @@ describe("POST /api/cluster-inference", () => { expect(res.status).toBe(400); }); - it("TC-CI10: calls nemoclaw cluster inference set with --provider and --model", async () => { + it("TC-CI10: calls CLI (openshell or nemoclaw) with cluster inference set or inference set, --provider, --model, --no-verify", async () => { execFile.mockImplementation((cmd, args, opts, cb) => { if (typeof opts === "function") { cb = opts; opts = {}; } cb(null, "", ""); @@ -162,7 +162,7 @@ describe("POST /api/cluster-inference", () => { .send({ providerName: "test-prov", modelId: "test-model" }); const setCall = execFile.mock.calls.find( - (c) => c[0] === "nemoclaw" && c[1]?.includes("inference") && c[1]?.includes("set") + (c) => (c[0] === "openshell" || c[0] === "nemoclaw") && c[1]?.includes("inference") && c[1]?.includes("set") ); expect(setCall).toBeDefined(); const args = setCall[1]; @@ -170,5 +170,6 @@ describe("POST /api/cluster-inference", () => { expect(args).toContain("test-prov"); expect(args).toContain("--model"); expect(args).toContain("test-model"); + expect(args).toContain("--no-verify"); }); }); diff --git a/brev/welcome-ui/__tests__/template-render.test.js b/brev/welcome-ui/__tests__/template-render.test.js index db534d5..aa3da49 100644 --- a/brev/welcome-ui/__tests__/template-render.test.js +++ b/brev/welcome-ui/__tests__/template-render.test.js @@ -3,7 +3,13 @@ import { describe, it, expect, beforeEach } from 'vitest'; import serverModule from '../server.js'; -const { renderOtherAgentsModal, getRenderedIndex, escapeHtml, _resetForTesting } = serverModule; +const { + renderOtherAgentsModal, + renderInferenceProviderPickerAndInstructions, + getRenderedIndex, + escapeHtml, + _resetForTesting, +} = serverModule; // === TC-T01 through TC-T14: YAML-to-HTML template rendering === @@ -88,6 +94,26 @@ describe("renderOtherAgentsModal", () => { }); }); +describe("renderInferenceProviderPickerAndInstructions", () => { + it("TC-T15: picker contains NVIDIA row and heading when YAML present", () => { + const html = renderInferenceProviderPickerAndInstructions(); + if (!html) return; + expect(html).toContain("Choose your inference provider"); + expect(html).toContain('data-provider-id="nvidia"'); + expect(html).toContain("Free endpoint provider"); + }); + + it("TC-T16: picker contains partner grid and provider-instructions blocks", () => { + const html = renderInferenceProviderPickerAndInstructions(); + if (!html) return; + expect(html).toContain("provider-picker__grid"); + expect(html).toContain("install-partner-instructions"); + expect(html).toContain("partner-instructions-content"); + expect(html).toContain("provider-instructions-"); + expect(html).toContain("Back to providers"); + }); +}); + describe("getRenderedIndex", () => { beforeEach(() => { _resetForTesting(); @@ -112,4 +138,16 @@ describe("getRenderedIndex", () => { const hasComment = html.includes(""); expect(hasModal || hasComment).toBe(true); }); + + it("TC-T17: {{INFERENCE_PROVIDER_PICKER}} is replaced in index.html", () => { + const html = getRenderedIndex(); + expect(html).not.toContain("{{INFERENCE_PROVIDER_PICKER}}"); + }); + + it("TC-T18: inference picker replaced with content or fallback comment", () => { + const html = getRenderedIndex(); + const hasPicker = html.includes("install-provider-picker"); + const hasComment = html.includes(""); + expect(hasPicker || hasComment).toBe(true); + }); }); diff --git a/brev/welcome-ui/app.js b/brev/welcome-ui/app.js index 4ba3de2..b9895a3 100644 --- a/brev/welcome-ui/app.js +++ b/brev/welcome-ui/app.js @@ -14,6 +14,9 @@ // Install modal elements const installMain = $("#install-main"); + const installNvidiaKey = $("#install-nvidia-key"); + const installProviderPicker = $("#install-provider-picker"); + const installPartnerInstructions = $("#install-partner-instructions"); const stepError = $("#install-step-error"); const apiKeyInput = $("#api-key-input"); const toggleKeyVis = $("#toggle-key-vis"); @@ -233,6 +236,24 @@ stepError.hidden = true; } + // -- Install modal screens (picker / NVIDIA / partner) ----------------- + + function showInstallScreen(screen, providerId) { + if (installProviderPicker) installProviderPicker.hidden = screen !== "picker"; + if (installNvidiaKey) installNvidiaKey.hidden = screen !== "nvidia"; + if (installPartnerInstructions) { + installPartnerInstructions.hidden = screen !== "partner"; + if (screen === "partner" && providerId) { + const content = $("#partner-instructions-content"); + if (content) { + content.querySelectorAll("[id^=\"provider-instructions-\"]").forEach((el) => { + el.hidden = el.id !== "provider-instructions-" + providerId; + }); + } + } + } + } + function showError(msg) { stopPolling(); installMain.hidden = true; @@ -387,6 +408,7 @@ updateButtonState(); showOverlay(overlayInstall); + showInstallScreen("nvidia"); if (!keyInjected) { startPolling(); } @@ -398,6 +420,7 @@ updateButtonState(); showOverlay(overlayInstall); + showInstallScreen("nvidia"); startPolling(); } } catch { @@ -425,13 +448,19 @@ showOverlay(overlayInstall); if (installFailed) { stepError.hidden = false; - installMain.hidden = true; + if (installProviderPicker) installProviderPicker.hidden = true; + if (installNvidiaKey) installNvidiaKey.hidden = true; + if (installPartnerInstructions) installPartnerInstructions.hidden = true; } else { - showMainView(); + if (installProviderPicker) { + showInstallScreen("picker"); + } else { + showInstallScreen("nvidia"); + apiKeyInput.focus(); + if (!installTriggered) triggerInstall(); + } } - apiKeyInput.focus(); updateButtonState(); - if (!installTriggered && !installFailed) triggerInstall(); }); cardOther.addEventListener("click", () => { @@ -442,6 +471,56 @@ closeInstall.addEventListener("click", () => hideOverlay(overlayInstall)); closeInstr.addEventListener("click", () => hideOverlay(overlayInstr)); + const backFromNvidia = $("#back-from-nvidia"); + if (backFromNvidia) { + backFromNvidia.addEventListener("click", () => showInstallScreen("picker")); + } + document.addEventListener("click", (e) => { + const backPartner = e.target.closest("#back-from-partner"); + if (backPartner) showInstallScreen("picker"); + }); + + function handleProviderChoice(id) { + if (id === "nvidia") { + showInstallScreen("nvidia"); + apiKeyInput.focus(); + if (!installTriggered && !installFailed) triggerInstall(); + } else { + showInstallScreen("partner", id); + } + } + + function onProviderPickerClick(e) { + const tile = e.target.closest("[data-provider-id]"); + if (!tile) return; + const id = tile.getAttribute("data-provider-id"); + if (!id) return; + e.preventDefault(); + e.stopPropagation(); + handleProviderChoice(id); + } + + function onProviderPickerKeydown(e) { + if (e.key !== "Enter" && e.key !== " ") return; + const tile = e.target.closest("[data-provider-id]"); + if (!tile) return; + const id = tile.getAttribute("data-provider-id"); + if (!id) return; + e.preventDefault(); + handleProviderChoice(id); + } + + if (installProviderPicker) { + installProviderPicker.addEventListener("click", onProviderPickerClick); + installProviderPicker.addEventListener("keydown", onProviderPickerKeydown); + } else { + const installBody = $("#install-body"); + if (installBody) { + installBody.addEventListener("click", onProviderPickerClick); + installBody.addEventListener("keydown", onProviderPickerKeydown); + } + } + closeOnBackdrop(overlayInstall); closeOnBackdrop(overlayInstr); diff --git a/brev/welcome-ui/index.html b/brev/welcome-ui/index.html index 4d95a34..04cd130 100644 --- a/brev/welcome-ui/index.html +++ b/brev/welcome-ui/index.html @@ -6,7 +6,7 @@ OpenShell — Agent Sandbox - + @@ -82,7 +82,11 @@