Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
585 changes: 585 additions & 0 deletions brev/launch.sh

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions brev/welcome-ui/SERVER_ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# NemoClaw Welcome UI — `server.py` Complete Architecture Reference
# OpenShell Welcome UI — `server.py` Complete Architecture Reference

> **Purpose:** This document provides an exhaustive, implementation-level description of `server.py` so that a software engineer can faithfully recreate it in Node.js with log-streaming support. Every endpoint, state machine, threading model, edge case, and dependency is documented.

Expand Down Expand Up @@ -409,7 +409,7 @@ Step 10: Cleanup temp policy file

### 6.3 `POST /api/inject-key`

**Purpose:** Asynchronously update the NemoClaw provider credential with an API key.
**Purpose:** Asynchronously update the OpenShell provider credential with an API key.

**Request Body:**
```json
Expand Down Expand Up @@ -450,7 +450,7 @@ Step 3: If success:

### 6.4 `POST /api/policy-sync`

**Purpose:** Push a policy YAML to the NemoClaw gateway via the host-side CLI.
**Purpose:** Push a policy YAML to the OpenShell gateway via the host-side CLI.

**Request Body:** Raw YAML text (Content-Type is not checked, but body is read as UTF-8).

Expand Down Expand Up @@ -499,10 +499,10 @@ Step 6: Cleanup tempfile (always, even on failure — in finally block)
"gatewayUrl": "https://8080-xxx.brevlab.com",
"gatewayPort": 8080,
"instructions": {
"install": "curl -fsSL https://github.com/NVIDIA/NemoClaw/releases/download/devel/install.sh | sh",
"connect": "nemoclaw gateway add https://8080-xxx.brevlab.com",
"createSandbox": "nemoclaw sandbox create -- claude",
"tui": "nemoclaw term"
"install": "curl -fsSL https://github.com/NVIDIA/OpenShell/releases/download/devel/install.sh | sh",
"connect": "openshell gateway add https://8080-xxx.brevlab.com",
"createSandbox": "openshell sandbox create -- claude",
"tui": "openshell term"
}
}
```
Expand All @@ -519,7 +519,7 @@ Step 6: Cleanup tempfile (always, even on failure — in finally block)

### 6.6 `GET /api/providers`

**Purpose:** List all configured NemoClaw providers with their details.
**Purpose:** List all configured OpenShell providers with their details.

**Processing:**
```
Expand Down Expand Up @@ -1013,7 +1013,7 @@ Gateway URL:
Else: http://{hostname}:8080
```

This is a DIFFERENT port (8080) — the NemoClaw gateway itself, not the welcome-ui.
This is a DIFFERENT port (8080) — the OpenShell gateway itself, not the welcome-ui.

---

Expand Down Expand Up @@ -1081,7 +1081,7 @@ Page Load
│ → keyInjected tracked via sandbox-status polling
├── When sandboxReady + keyValid + keyInjected:
│ → "Open NemoClaw" button enabled
│ → "Open OpenShell" button enabled
│ → Click opens: sandboxUrl + ?nvapi=<key> in new tab
└── User clicks "Other Agents" card
Expand All @@ -1098,7 +1098,7 @@ Page Load
| 2 | API key valid + tasks running | "Provisioning Sandbox..." | No (spinner) |
| 3 | API key empty + tasks done | "Waiting for API key..." | No |
| 4 | API key valid + sandbox ready + key not injected | "Configuring API key..." | No (spinner) |
| 5 | API key valid + sandbox ready + key injected | "Open NemoClaw" | Yes |
| 5 | API key valid + sandbox ready + key injected | "Open OpenShell" | Yes |

### API Key Validation

Expand Down
12 changes: 6 additions & 6 deletions brev/welcome-ui/__tests__/connection-details.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ describe("GET /api/connection-details", () => {
expect(res.body.gatewayPort).toBe(8080);
expect(res.body.instructions).toBeDefined();
expect(res.body.instructions.install).toContain("curl");
expect(res.body.instructions.connect).toContain("nemoclaw gateway add");
expect(res.body.instructions.createSandbox).toContain("nemoclaw sandbox create");
expect(res.body.instructions.tui).toBe("nemoclaw term");
expect(res.body.instructions.connect).toContain("openshell gateway add");
expect(res.body.instructions.createSandbox).toContain("openshell sandbox create");
expect(res.body.instructions.tui).toBe("openshell term");
});

it("TC-CD02: with Brev ID, gatewayUrl is https://8080-{id}.brevlab.com", async () => {
Expand Down Expand Up @@ -99,11 +99,11 @@ describe("GET /api/connection-details", () => {
it("TC-CD06: instructions contain exact CLI strings", async () => {
const res = await request(server).get("/api/connection-details");
expect(res.body.instructions.install).toBe(
"curl -fsSL https://github.com/NVIDIA/NemoClaw/releases/download/devel/install.sh | sh"
"curl -fsSL https://github.com/NVIDIA/OpenShell/releases/download/devel/install.sh | sh"
);
expect(res.body.instructions.createSandbox).toBe(
"nemoclaw sandbox create -- claude"
"openshell sandbox create -- claude"
);
expect(res.body.instructions.tui).toBe("nemoclaw term");
expect(res.body.instructions.tui).toBe("openshell term");
});
});
2 changes: 1 addition & 1 deletion brev/welcome-ui/__tests__/routing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ describe("routing — priority", () => {
const res = await request(server).get("/");
expect(res.status).toBe(200);
expect(res.headers["content-type"]).toContain("text/html");
expect(res.text).toContain("NemoClaw");
expect(res.text).toContain("OpenShell");
});

it("TC-R13: unknown path returns 404 when sandbox NOT ready", async () => {
Expand Down
4 changes: 2 additions & 2 deletions brev/welcome-ui/__tests__/static-files.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe("static file serving", () => {
const res = await request(server).get("/styles.css");
expect(res.status).toBe(200);
expect(res.headers["content-type"]).toContain("text/css");
expect(res.text).toContain("NemoClaw");
expect(res.text).toContain("OpenShell");
});

it("TC-SF02: GET /app.js returns JS with application/javascript content-type", async () => {
Expand All @@ -43,7 +43,7 @@ describe("static file serving", () => {
expect(res.status).toBe(200);
expect(res.headers["content-type"]).toContain("text/html");
expect(res.text).not.toContain("{{OTHER_AGENTS_MODAL}}");
expect(res.text).toContain("NemoClaw");
expect(res.text).toContain("OpenShell");
});

it("TC-SF05: GET /index.html returns templated index.html", async () => {
Expand Down
6 changes: 3 additions & 3 deletions brev/welcome-ui/__tests__/template-render.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe("renderOtherAgentsModal", () => {
it("TC-T07: steps are auto-numbered (1., 2., etc.)", () => {
const html = renderOtherAgentsModal();
if (!html) return;
expect(html).toContain("1. Install NemoClaw CLI");
expect(html).toContain("1. Install OpenShell CLI");
expect(html).toContain("2. Add the gateway");
expect(html).toContain("3. Create a sandbox");
expect(html).toContain("4. Manage policies");
Expand All @@ -54,7 +54,7 @@ describe("renderOtherAgentsModal", () => {
const html = renderOtherAgentsModal();
if (!html) return;
expect(html).toContain('<span class="comment"># Claude Code</span>');
expect(html).toContain("nemoclaw sandbox create -- claude");
expect(html).toContain("openshell sandbox create -- claude");
});

it("TC-T10: dict command with id renders cmd span with id attribute", () => {
Expand All @@ -73,7 +73,7 @@ describe("renderOtherAgentsModal", () => {
it("TC-T12: copyable + single command + no button ID renders data-copy", () => {
const html = renderOtherAgentsModal();
if (!html) return;
// "Install NemoClaw CLI" step has copyable:true and one command, no copy_button_id
// "Install OpenShell CLI" step has copyable:true and one command, no copy_button_id
expect(html).toContain("data-copy=");
});

Expand Down
69 changes: 51 additions & 18 deletions brev/welcome-ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@
let injectInFlight = false;
let injectTimer = null;
let lastSubmittedKey = "";
let keyInjectError = "";
let installFailed = false;

function stopPolling() {
if (pollTimer) {
Expand All @@ -132,6 +134,7 @@
if (key === lastSubmittedKey) return;
lastSubmittedKey = key;
keyInjected = false;
keyInjectError = "";
injectInFlight = true;
updateButtonState();
try {
Expand All @@ -149,6 +152,9 @@
updateButtonState();
const key = apiKeyInput.value.trim();
if (!isApiKeyValid()) return;
if (!sandboxReady && !installTriggered && !installFailed) {
triggerInstall();
}
if (injectTimer) clearTimeout(injectTimer);
injectTimer = setTimeout(() => submitKeyForInjection(key), 300);
}
Expand All @@ -159,7 +165,7 @@
* 2. API valid + tasks running -> "Provisioning Sandbox…" (disabled, spinner)
* 3. API empty + tasks complete -> "Waiting for API key…" (disabled)
* 4. API valid + sandbox ready + !key -> "Configuring API key…" (disabled, spinner)
* 5. API valid + sandbox ready + key -> "Open NemoClaw" (enabled)
* 5. API valid + sandbox ready + key -> "Open OpenShell" (enabled)
*/
function updateButtonState() {
const keyValid = isApiKeyValid();
Expand All @@ -169,6 +175,9 @@
if (keyRaw.length === 0) {
keyHint.textContent = "";
keyHint.className = "form-field__hint";
} else if (keyInjectError) {
keyHint.textContent = keyInjectError;
keyHint.className = "form-field__hint form-field__hint--warn";
} else if (keyValid) {
keyHint.textContent = "Valid key format";
keyHint.className = "form-field__hint form-field__hint--ok";
Expand All @@ -191,13 +200,19 @@
btnLaunch.classList.add("btn--ready");
btnSpinner.hidden = true;
btnSpinner.style.display = "none";
btnLaunchLabel.textContent = "Open NemoClaw";
} else if (sandboxReady && keyValid && !keyInjected) {
btnLaunchLabel.textContent = "Open OpenShell";
} else if (sandboxReady && keyValid && !keyInjected && (injectInFlight || !keyInjectError)) {
btnLaunch.disabled = true;
btnLaunch.classList.remove("btn--ready");
btnSpinner.hidden = false;
btnSpinner.style.display = "";
btnLaunchLabel.textContent = "Configuring API key\u2026";
} else if (sandboxReady && keyValid && !keyInjected) {
btnLaunch.disabled = true;
btnLaunch.classList.remove("btn--ready");
btnSpinner.hidden = true;
btnSpinner.style.display = "none";
btnLaunchLabel.textContent = "Update API key to retry";
} else if (!sandboxReady && keyValid) {
btnLaunch.disabled = true;
btnLaunch.classList.remove("btn--ready");
Expand Down Expand Up @@ -225,6 +240,18 @@
errorMessage.textContent = msg;
}

function setSandboxChecklistCreating() {
setLogIcon(logSandboxIcon, "spin");
logSandbox.querySelector(".console__text").textContent =
"Provisioning secure OpenShell sandbox...";
}

function setSandboxChecklistReady() {
setLogIcon(logSandboxIcon, "done");
logSandbox.querySelector(".console__text").textContent =
"Secure OpenShell sandbox created.";
}

async function triggerInstall() {
if (installTriggered) return;
installTriggered = true;
Expand All @@ -247,9 +274,7 @@
return;
}

setLogIcon(logSandboxIcon, "done");
logSandbox.querySelector(".console__text").textContent =
"Secure NemoClaw sandbox created.";
setSandboxChecklistCreating();
setLogIcon(logGatewayIcon, "spin");
startPolling();
} catch {
Expand All @@ -268,11 +293,18 @@
if (!injectInFlight) {
keyInjected = !!data.key_injected;
}
keyInjectError = data.key_inject_error || "";
if (keyInjectError) {
injectInFlight = false;
keyInjected = false;
lastSubmittedKey = "";
}

if (data.status === "running") {
sandboxReady = true;
sandboxUrl = data.url || null;

setSandboxChecklistReady();
setLogIcon(logGatewayIcon, "done");
logGateway.querySelector(".console__text").textContent =
"OpenClaw agent gateway online.";
Expand All @@ -284,6 +316,7 @@
} else if (data.status === "error") {
stopPolling();
installTriggered = false;
installFailed = true;
showError(data.error || "Sandbox creation failed");
} else {
updateButtonState();
Expand All @@ -308,13 +341,15 @@
sandboxUrl = null;
installTriggered = false;
keyInjected = false;
keyInjectError = "";
installFailed = false;
lastSubmittedKey = "";
stopPolling();

setLogIcon(logSandboxIcon, null);
setLogIcon(logGatewayIcon, null);
logSandbox.querySelector(".console__text").textContent =
"Initializing secure NemoClaw sandbox...";
"Initializing secure OpenShell sandbox...";
logGateway.querySelector(".console__text").textContent =
"Launching OpenClaw agent gateway...";
logReady.hidden = true;
Expand Down Expand Up @@ -344,9 +379,7 @@
sandboxUrl = data.url;
installTriggered = true;

setLogIcon(logSandboxIcon, "done");
logSandbox.querySelector(".console__text").textContent =
"Secure NemoClaw sandbox created.";
setSandboxChecklistReady();
setLogIcon(logGatewayIcon, "done");
logGateway.querySelector(".console__text").textContent =
"OpenClaw agent gateway online.";
Expand All @@ -359,9 +392,7 @@
} else if (data.status === "creating") {
installTriggered = true;

setLogIcon(logSandboxIcon, "done");
logSandbox.querySelector(".console__text").textContent =
"Secure NemoClaw sandbox created.";
setSandboxChecklistCreating();
setLogIcon(logGatewayIcon, "spin");
updateButtonState();

Expand All @@ -379,21 +410,23 @@
try {
const res = await fetch("/api/connection-details");
const data = await res.json();
const cmd = `nemoclaw cluster connect ${data.hostname}`;
const cmd = data.instructions?.connect || `openshell gateway add ${data.gatewayUrl}`;
connectCmd.textContent = cmd;
copyConnect.dataset.copy = cmd;
} catch {
connectCmd.textContent = "nemoclaw cluster connect <hostname>";
connectCmd.textContent = "openshell gateway add <gateway-url>";
}
}

// -- Event wiring ---------------------------------------------------

cardOpenclaw.addEventListener("click", () => {
showOverlay(overlayInstall);
showMainView();
if (!installTriggered) {
triggerInstall();
if (installFailed) {
stepError.hidden = false;
installMain.hidden = true;
} else {
showMainView();
}
apiKeyInput.focus();
updateButtonState();
Expand Down
Binary file added brev/welcome-ui/favicon.ico
Binary file not shown.
Loading
Loading