From 99d6cb97cb510b3739c68dc59584cad84896be2b Mon Sep 17 00:00:00 2001 From: JR Morgan Date: Wed, 11 Mar 2026 15:03:18 -0700 Subject: [PATCH 01/14] Add rebrand for welcome UI, include launch.sh script --- brev/launch.sh | 436 ++++++++++++++++++++++++++++++ brev/welcome-ui/app.js | 16 +- brev/welcome-ui/other-agents.yaml | 18 +- brev/welcome-ui/server.js | 105 ++++--- 4 files changed, 519 insertions(+), 56 deletions(-) create mode 100755 brev/launch.sh diff --git a/brev/launch.sh b/brev/launch.sh new file mode 100755 index 0000000..f57de3b --- /dev/null +++ b/brev/launch.sh @@ -0,0 +1,436 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT_REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +REPO_ROOT="" +WELCOME_UI_DIR="" + +PORT="${PORT:-8081}" +CLI_BIN="${CLI_BIN:-}" +CLI_RELEASE_TAG="${CLI_RELEASE_TAG:-devel}" +AUTO_INSTALL_CLI="${AUTO_INSTALL_CLI:-1}" +GITHUB_TOKEN="${GITHUB_TOKEN:-${GH_TOKEN:-${GITHUB_PAT:-}}}" +COMMUNITY_REPO="${COMMUNITY_REPO:-NVIDIA/OpenShell-Community}" +COMMUNITY_REF="${COMMUNITY_REF:-${COMMUNITY_BRANCH:-}}" +CLONE_ROOT="${CLONE_ROOT:-/home/ubuntu}" +CLONE_DIR="${CLONE_DIR:-$CLONE_ROOT/OpenShell-Community}" +GATEWAY_LOG="${GATEWAY_LOG:-/tmp/openshell-gateway.log}" +WELCOME_UI_LOG="${WELCOME_UI_LOG:-/tmp/welcome-ui.log}" +WAIT_TIMEOUT_SECS="${WAIT_TIMEOUT_SECS:-30}" + +log() { + printf '[launch.sh] %s\n' "$*" +} + +step() { + printf '\n[launch.sh] === %s ===\n' "$*" +} + +require_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + log "Missing required command: $1" + exit 1 + fi +} + +repo_has_welcome_ui() { + [[ -d "$1/brev/welcome-ui" ]] +} + +wait_for_tcp_port() { + local port="$1" + local timeout_secs="${2:-30}" + local start_ts + start_ts="$(date +%s)" + + while true; do + if (echo >"/dev/tcp/127.0.0.1/$port") >/dev/null 2>&1; then + return 0 + fi + + if (( "$(date +%s)" - start_ts >= timeout_secs )); then + return 1 + fi + + sleep 1 + done +} + +wait_for_log_pattern() { + local logfile="$1" + local pattern="$2" + local timeout_secs="${3:-30}" + local start_ts + start_ts="$(date +%s)" + + while true; do + if [[ -f "$logfile" ]] && grep -q "$pattern" "$logfile"; then + return 0 + fi + + if (( "$(date +%s)" - start_ts >= timeout_secs )); then + return 1 + fi + + sleep 1 + done +} + +detect_arch() { + case "$(uname -m)" in + x86_64|amd64) echo "x86_64" ;; + aarch64|arm64) echo "aarch64" ;; + *) + log "Unsupported architecture: $(uname -m)" + exit 1 + ;; + esac +} + +ensure_gh() { + if command -v gh >/dev/null 2>&1; then + log "GitHub CLI already installed." + return + fi + + log "Installing GitHub CLI..." + require_cmd sudo + require_cmd apt-get + sudo apt-get update + sudo apt-get install -y gh +} + +gh_auth_if_needed() { + if ! command -v gh >/dev/null 2>&1; then + return + fi + + if gh auth status >/dev/null 2>&1; then + return + fi + + if [[ -z "$GITHUB_TOKEN" ]]; then + log "GitHub CLI is unauthenticated. Continuing without auth." + return + fi + + log "Authenticating GitHub CLI from environment token..." + if ! printf '%s\n' "$GITHUB_TOKEN" | gh auth login --with-token >/dev/null 2>&1; then + log "GitHub authentication failed." + exit 1 + fi +} + +checkout_repo_ref() { + if [[ -z "$COMMUNITY_REF" ]]; then + return + fi + + require_cmd git + log "Checking out OpenShell-Community ref: $COMMUNITY_REF" + + git -C "$CLONE_DIR" fetch --all --tags --prune + + if git -C "$CLONE_DIR" show-ref --verify --quiet "refs/remotes/origin/$COMMUNITY_REF"; then + git -C "$CLONE_DIR" checkout -B "$COMMUNITY_REF" "origin/$COMMUNITY_REF" + return + fi + + if git -C "$CLONE_DIR" show-ref --verify --quiet "refs/tags/$COMMUNITY_REF"; then + git -C "$CLONE_DIR" checkout --detach "refs/tags/$COMMUNITY_REF" + return + fi + + if git -C "$CLONE_DIR" rev-parse --verify --quiet "$COMMUNITY_REF^{commit}" >/dev/null; then + git -C "$CLONE_DIR" checkout --detach "$COMMUNITY_REF" + return + fi + + git -C "$CLONE_DIR" fetch origin "$COMMUNITY_REF" + git -C "$CLONE_DIR" checkout --detach FETCH_HEAD +} + +clone_repo_if_needed() { + if repo_has_welcome_ui "$CLONE_DIR"; then + log "Using existing repo checkout at $CLONE_DIR" + checkout_repo_ref + return + fi + + require_cmd git + + if [[ -e "$CLONE_DIR" ]]; then + log "Clone target exists but is not a valid repo checkout: $CLONE_DIR" + exit 1 + fi + + mkdir -p "$CLONE_ROOT" + + if [[ -n "$GITHUB_TOKEN" ]]; then + log "Cloning ${COMMUNITY_REPO} into $CLONE_DIR with token auth..." + if [[ -n "$COMMUNITY_REF" ]]; then + git clone --branch "$COMMUNITY_REF" "https://${GITHUB_TOKEN}@github.com/${COMMUNITY_REPO}.git" "$CLONE_DIR" \ + || git clone "https://${GITHUB_TOKEN}@github.com/${COMMUNITY_REPO}.git" "$CLONE_DIR" + else + git clone "https://${GITHUB_TOKEN}@github.com/${COMMUNITY_REPO}.git" "$CLONE_DIR" + fi + else + log "Cloning ${COMMUNITY_REPO} into $CLONE_DIR..." + if [[ -n "$COMMUNITY_REF" ]]; then + git clone --branch "$COMMUNITY_REF" "https://github.com/${COMMUNITY_REPO}.git" "$CLONE_DIR" \ + || git clone "https://github.com/${COMMUNITY_REPO}.git" "$CLONE_DIR" + else + git clone "https://github.com/${COMMUNITY_REPO}.git" "$CLONE_DIR" + fi + fi + + checkout_repo_ref +} + +install_cli_from_release() { + local arch tmpdir repo pattern archive candidate + + ensure_gh + gh_auth_if_needed + + arch="$(detect_arch)" + tmpdir="$(mktemp -d)" + trap 'rm -rf "$tmpdir"' RETURN + + for candidate in openshell nemoclaw; do + case "$candidate" in + openshell) repo="NVIDIA/OpenShell" ;; + nemoclaw) repo="NVIDIA/NemoClaw" ;; + esac + + pattern="${candidate}-${arch}-unknown-linux-musl.tar.gz" + log "Trying CLI download: ${repo} ${CLI_RELEASE_TAG} ${pattern}" + if gh release download "$CLI_RELEASE_TAG" --repo "$repo" --pattern "$pattern" --dir "$tmpdir" >/dev/null 2>&1; then + archive="$tmpdir/$pattern" + tar xzf "$archive" -C "$tmpdir" + sudo install -m 755 "$tmpdir/$candidate" "/usr/local/bin/$candidate" + echo "$candidate" + return 0 + fi + done + + log "Unable to install CLI from GitHub releases." + exit 1 +} + +resolve_cli() { + if [[ -n "$CLI_BIN" ]]; then + require_cmd "$CLI_BIN" + log "Using CLI from CLI_BIN: $CLI_BIN" + return + fi + + if command -v openshell >/dev/null 2>&1; then + CLI_BIN="openshell" + log "Detected installed CLI: $CLI_BIN" + return + fi + + if command -v nemoclaw >/dev/null 2>&1; then + CLI_BIN="nemoclaw" + log "Detected installed CLI: $CLI_BIN" + return + fi + + if [[ "$AUTO_INSTALL_CLI" != "1" ]]; then + log "Neither openshell nor nemoclaw is installed." + exit 1 + fi + + CLI_BIN="$(install_cli_from_release)" +} + +ensure_cli_compat_aliases() { + local cli_path + + cli_path="$(command -v "$CLI_BIN")" + + if [[ "$CLI_BIN" == "openshell" ]] && ! command -v nemoclaw >/dev/null 2>&1; then + sudo ln -sf "$cli_path" /usr/local/bin/nemoclaw + log "Created compatibility alias: nemoclaw -> openshell" + fi + + if [[ "$CLI_BIN" == "nemoclaw" ]] && ! command -v openshell >/dev/null 2>&1; then + sudo ln -sf "$cli_path" /usr/local/bin/openshell + log "Created compatibility alias: openshell -> nemoclaw" + fi +} + +resolve_repo_root() { + if repo_has_welcome_ui "$SCRIPT_REPO_ROOT"; then + REPO_ROOT="$SCRIPT_REPO_ROOT" + elif repo_has_welcome_ui "$PWD"; then + REPO_ROOT="$PWD" + else + clone_repo_if_needed + REPO_ROOT="$CLONE_DIR" + fi + + WELCOME_UI_DIR="$REPO_ROOT/brev/welcome-ui" +} + +ensure_node() { + if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then + log "Node.js already installed: $(node --version)" + log "npm already installed: $(npm --version)" + return + fi + + log "Installing Node.js LTS via nvm..." + require_cmd curl + curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash + + export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" + # shellcheck disable=SC1090 + . "$NVM_DIR/nvm.sh" + nvm install --lts +} + +set_inference_route() { + log "Configuring inference route..." + + if "$CLI_BIN" inference set --provider nvidia-endpoints --model moonshotai/kimi-k2.5 >/dev/null 2>&1; then + log "Configured inference via '$CLI_BIN inference set'." + return + fi + + if "$CLI_BIN" cluster inference set --provider nvidia-endpoints --model moonshotai/kimi-k2.5 >/dev/null 2>&1; then + log "Configured inference via legacy '$CLI_BIN cluster inference set'." + return + fi + + log "Unable to configure inference route with either current or legacy CLI commands." + exit 1 +} + +run_provider_create_or_replace() { + local name="$1" + shift + + log "Configuring provider: $name" + if "$CLI_BIN" provider create --name "$name" "$@" >/dev/null 2>&1; then + log "Created provider: $name" + return + fi + + log "Provider create failed for $name. Replacing existing provider..." + "$CLI_BIN" provider delete "$name" >/dev/null 2>&1 || true + "$CLI_BIN" provider create --name "$name" "$@" + log "Recreated provider: $name" +} + +start_gateway() { + : > "$GATEWAY_LOG" + log "Resetting gateway state if it already exists..." + log "Gateway log: $GATEWAY_LOG" + + if "$CLI_BIN" gateway destroy >> "$GATEWAY_LOG" 2>&1; then + log "Existing gateway destroyed." + else + log "Gateway destroy returned non-zero. Continuing with fresh start." + fi + + log "Starting gateway..." + if ! "$CLI_BIN" gateway start 2>&1 | tee -a "$GATEWAY_LOG"; then + log "Gateway start failed. Last log lines:" + tail -n 50 "$GATEWAY_LOG" || true + exit 1 + fi + + if ! wait_for_log_pattern "$GATEWAY_LOG" "Gateway .* ready\\|Active gateway set" "$WAIT_TIMEOUT_SECS"; then + log "Gateway did not become ready within ${WAIT_TIMEOUT_SECS}s. Last log lines:" + tail -n 50 "$GATEWAY_LOG" || true + exit 1 + fi + + log "Gateway reported ready." +} + +install_ui_deps() { + require_cmd npm + cd "$WELCOME_UI_DIR" + + log "Installing welcome UI dependencies in $WELCOME_UI_DIR" + if [[ -f package-lock.json ]]; then + npm ci + else + npm install + fi +} + +start_welcome_ui() { + cd "$WELCOME_UI_DIR" + + : > "$WELCOME_UI_LOG" + log "Starting welcome UI in background..." + log "Welcome UI log: $WELCOME_UI_LOG" + + nohup env PORT="$PORT" REPO_ROOT="$REPO_ROOT" CLI_BIN="$CLI_BIN" node server.js >> "$WELCOME_UI_LOG" 2>&1 & + WELCOME_UI_PID=$! + export WELCOME_UI_PID + log "Welcome UI PID: $WELCOME_UI_PID" + + if ! wait_for_tcp_port "$PORT" "$WAIT_TIMEOUT_SECS"; then + log "Welcome UI did not open port $PORT within ${WAIT_TIMEOUT_SECS}s. Last log lines:" + tail -n 100 "$WELCOME_UI_LOG" || true + exit 1 + fi + + log "Welcome UI started at http://localhost:${PORT}" +} + +main() { + require_cmd tar + require_cmd sudo + + step "Resolving repo" + resolve_repo_root + step "Resolving CLI" + resolve_cli + ensure_cli_compat_aliases + step "Ensuring Node.js" + ensure_node + + log "Using repo root: $REPO_ROOT" + if [[ -n "$COMMUNITY_REF" ]]; then + log "Using community ref: $COMMUNITY_REF" + fi + log "Using CLI: $CLI_BIN" + + step "Starting gateway" + start_gateway + + step "Configuring providers" + run_provider_create_or_replace \ + nvidia-inference \ + --type openai \ + --credential OPENAI_API_KEY=unused \ + --config OPENAI_BASE_URL=https://inference-api.nvidia.com/v1 + + run_provider_create_or_replace \ + nvidia-endpoints \ + --type nvidia \ + --credential NVIDIA_API_KEY=unused \ + --config NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1 + + set_inference_route + + step "Installing welcome UI dependencies" + install_ui_deps + step "Starting welcome UI" + start_welcome_ui + + step "Ready" + log "Gateway log: $GATEWAY_LOG" + log "Welcome UI log: $WELCOME_UI_LOG" + log "Open http://localhost:${PORT}" +} + +main "$@" diff --git a/brev/welcome-ui/app.js b/brev/welcome-ui/app.js index 6573983..cdcc0eb 100644 --- a/brev/welcome-ui/app.js +++ b/brev/welcome-ui/app.js @@ -159,7 +159,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(); @@ -191,7 +191,7 @@ btnLaunch.classList.add("btn--ready"); btnSpinner.hidden = true; btnSpinner.style.display = "none"; - btnLaunchLabel.textContent = "Open NemoClaw"; + btnLaunchLabel.textContent = "Open OpenShell"; } else if (sandboxReady && keyValid && !keyInjected) { btnLaunch.disabled = true; btnLaunch.classList.remove("btn--ready"); @@ -249,7 +249,7 @@ setLogIcon(logSandboxIcon, "done"); logSandbox.querySelector(".console__text").textContent = - "Secure NemoClaw sandbox created."; + "Secure OpenShell sandbox created."; setLogIcon(logGatewayIcon, "spin"); startPolling(); } catch { @@ -314,7 +314,7 @@ 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; @@ -346,7 +346,7 @@ setLogIcon(logSandboxIcon, "done"); logSandbox.querySelector(".console__text").textContent = - "Secure NemoClaw sandbox created."; + "Secure OpenShell sandbox created."; setLogIcon(logGatewayIcon, "done"); logGateway.querySelector(".console__text").textContent = "OpenClaw agent gateway online."; @@ -361,7 +361,7 @@ setLogIcon(logSandboxIcon, "done"); logSandbox.querySelector(".console__text").textContent = - "Secure NemoClaw sandbox created."; + "Secure OpenShell sandbox created."; setLogIcon(logGatewayIcon, "spin"); updateButtonState(); @@ -379,11 +379,11 @@ 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 "; + connectCmd.textContent = "openshell gateway add "; } } diff --git a/brev/welcome-ui/other-agents.yaml b/brev/welcome-ui/other-agents.yaml index f00758b..621a36d 100644 --- a/brev/welcome-ui/other-agents.yaml +++ b/brev/welcome-ui/other-agents.yaml @@ -21,38 +21,38 @@ title: Bring Your Own Agent intro: >- Connect from your laptop, create a sandbox, and run any agent inside it. - The NemoClaw TUI on your machine surfaces policy recommendations from the agent. + The OpenShell TUI on your machine surfaces policy recommendations from the agent. You approve or deny — the sandbox boundary is never violated. steps: - - title: Install NemoClaw CLI + - title: Install OpenShell CLI commands: - - "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" copyable: true - title: Add the gateway block_id: connect-cmd-block commands: - - cmd: "nemoclaw gateway add " + - cmd: "openshell gateway add " id: connect-cmd copy_button_id: copy-connect copyable: true description: >- This will initiate a JWT auth flow in your browser. - Once authorized, you can run any NemoClaw command against this deployment. + Once authorized, you can run any OpenShell command against this deployment. - title: Create a sandbox and run your agent commands: - comment: Claude Code - cmd: "nemoclaw sandbox create -- claude" + cmd: "openshell sandbox create -- claude" - comment: OpenCode - cmd: "nemoclaw sandbox create -- opencode" + cmd: "openshell sandbox create -- opencode" - comment: Codex - cmd: "nemoclaw sandbox create -- codex" + cmd: "openshell sandbox create -- codex" - title: Manage policies with the TUI commands: - - nemoclaw term + - openshell term copyable: true description: >- When your agent hits a policy denial, it reads the policy, diagnoses the block, diff --git a/brev/welcome-ui/server.js b/brev/welcome-ui/server.js index 713cfae..befa62e 100644 --- a/brev/welcome-ui/server.js +++ b/brev/welcome-ui/server.js @@ -31,8 +31,10 @@ try { const PORT = parseInt(process.env.PORT || "8081", 10); const ROOT = __dirname; const REPO_ROOT = process.env.REPO_ROOT || path.join(ROOT, "..", ".."); +const CLI_BIN = process.env.CLI_BIN || "openshell"; const SANDBOX_DIR = path.join(REPO_ROOT, "sandboxes", "nemoclaw"); -const NEMOCLAW_IMAGE = "ghcr.io/nvidia/openshell-community/sandboxes/nemoclaw:local"; +const SANDBOX_NAME = process.env.SANDBOX_NAME || "nemoclaw"; +const SANDBOX_START_CMD = process.env.SANDBOX_START_CMD || "nemoclaw-start"; const POLICY_FILE = path.join(SANDBOX_DIR, "policy.yaml"); const LOG_FILE = "/tmp/nemoclaw-sandbox-create.log"; @@ -129,6 +131,22 @@ function execCmd(args, timeoutMs = 30000) { }); } +function cliArgs(...args) { + return [CLI_BIN, ...args]; +} + +async function execFirstSuccess(commands, timeoutMs = 30000) { + let lastResult = null; + for (const args of commands) { + const result = await execCmd(args, timeoutMs); + if (result.code === 0) { + return { ...result, args }; + } + lastResult = { ...result, args }; + } + return lastResult || { code: 1, stdout: "", stderr: "no command executed", args: [] }; +} + function portOpen(host, port, timeoutMs = 1000) { return new Promise((resolve) => { const sock = new net.Socket(); @@ -518,7 +536,7 @@ async function generateGatewayPolicy() { } } -async function syncPolicyToGateway(yamlText, sandboxName = "nemoclaw") { +async function syncPolicyToGateway(yamlText, sandboxName = SANDBOX_NAME) { log("policy-sync", `step 2/4: stripping inference+process fields (${yamlText.length} bytes in)`); const stripped = stripPolicyFields(yamlText, ["process"]); log("policy-sync", ` stripped to ${stripped.length} bytes`); @@ -529,7 +547,7 @@ async function syncPolicyToGateway(yamlText, sandboxName = "nemoclaw") { ); try { fs.writeFileSync(tmpPath, stripped); - const args = ["nemoclaw", "policy", "set", sandboxName, "--policy", tmpPath]; + const args = cliArgs("policy", "set", sandboxName, "--policy", tmpPath); log("policy-sync", `step 3/4: running ${args.join(" ")}`); const t0 = Date.now(); @@ -565,7 +583,7 @@ async function syncPolicyToGateway(yamlText, sandboxName = "nemoclaw") { async function cleanupExistingSandbox() { try { - await execCmd(["nemoclaw", "sandbox", "delete", "nemoclaw"], 30000); + await execCmd(cliArgs("sandbox", "delete", SANDBOX_NAME), 30000); } catch { // ignore } @@ -584,10 +602,9 @@ function runSandboxCreate() { const policyPath = await generateGatewayPolicy(); const cmd = [ - "nemoclaw", "sandbox", "create", - "--name", "nemoclaw", - "--from", "nemoclaw", - // "--from", NEMOCLAW_IMAGE, + CLI_BIN, "sandbox", "create", + "--name", SANDBOX_NAME, + "--from", SANDBOX_DIR, "--forward", "18789", ]; if (policyPath) cmd.push("--policy", policyPath); @@ -595,7 +612,7 @@ function runSandboxCreate() { "--", "env", `CHAT_UI_URL=${chatUiUrl}`, - "nemoclaw-start" + SANDBOX_START_CMD ); const cmdDisplay = cmd.slice(0, 8).join(" ") + " -- ..."; @@ -711,12 +728,11 @@ function runInjectKey(key, keyHash) { log("inject-key", `step 1/3: received key (hash=${keyHash.slice(0, 12)}…)`); const args = [ - "nemoclaw", "provider", "update", "nvidia-inference", - "--type", "openai", + ...cliArgs("provider", "update", "nvidia-inference"), "--credential", `OPENAI_API_KEY=${key}`, "--config", "OPENAI_BASE_URL=https://inference-api.nvidia.com/v1", ]; - log("inject-key", "step 2/3: running nemoclaw provider update nvidia-inference …"); + log("inject-key", `step 2/3: running ${CLI_BIN} provider update nvidia-inference …`); const t0 = Date.now(); execCmd(args, 120000) @@ -782,7 +798,7 @@ async function handleProvidersList(req, res) { let names; try { const result = await execCmd( - ["nemoclaw", "provider", "list", "--names"], + cliArgs("provider", "list", "--names"), 30000 ); if (result.code !== 0) { @@ -801,7 +817,7 @@ async function handleProvidersList(req, res) { for (const name of names) { try { const detail = await execCmd( - ["nemoclaw", "provider", "get", name], + cliArgs("provider", "get", name), 30000 ); if (detail.code === 0) { @@ -837,7 +853,7 @@ async function handleProviderCreate(req, res) { }); } - const cmd = ["nemoclaw", "provider", "create", "--name", name, "--type", ptype]; + const cmd = [...cliArgs("provider", "create"), "--name", name, "--type", ptype]; const creds = data.credentials || {}; const configs = data.config || {}; if (Object.keys(creds).length === 0) { @@ -873,14 +889,7 @@ async function handleProviderUpdate(req, res, name) { } const ptype = (data.type || "").trim(); - if (!ptype) { - return jsonResponse(res, 400, { - ok: false, - error: "type is required", - }); - } - - const cmd = ["nemoclaw", "provider", "update", name, "--type", ptype]; + const cmd = [...cliArgs("provider", "update"), name]; for (const [k, v] of Object.entries(data.credentials || {})) { cmd.push("--credential", `${k}=${v}`); } @@ -891,10 +900,29 @@ async function handleProviderUpdate(req, res, name) { try { const result = await execCmd(cmd, 30000); - if (result.code !== 0) { + if (result.code === 0) { + if (Object.keys(configs).length > 0) cacheProviderConfig(name, configs); + return jsonResponse(res, 200, { ok: true }); + } + + if (!ptype) { const err = (result.stderr || result.stdout || "update failed").trim(); return jsonResponse(res, 400, { ok: false, error: err }); } + + await execCmd(cliArgs("provider", "delete", name), 30000); + const createCmd = [...cliArgs("provider", "create"), "--name", name, "--type", ptype]; + for (const [k, v] of Object.entries(data.credentials || {})) { + createCmd.push("--credential", `${k}=${v}`); + } + for (const [k, v] of Object.entries(configs)) { + createCmd.push("--config", `${k}=${v}`); + } + const recreated = await execCmd(createCmd, 30000); + if (recreated.code !== 0) { + const err = (recreated.stderr || recreated.stdout || "update failed").trim(); + return jsonResponse(res, 400, { ok: false, error: err }); + } if (Object.keys(configs).length > 0) cacheProviderConfig(name, configs); return jsonResponse(res, 200, { ok: true }); } catch (e) { @@ -904,10 +932,7 @@ async function handleProviderUpdate(req, res, name) { async function handleProviderDelete(req, res, name) { try { - const result = await execCmd( - ["nemoclaw", "provider", "delete", name], - 30000 - ); + const result = await execCmd(cliArgs("provider", "delete", name), 30000); if (result.code !== 0) { const err = (result.stderr || result.stdout || "delete failed").trim(); return jsonResponse(res, 400, { ok: false, error: err }); @@ -948,8 +973,11 @@ function parseClusterInference(stdout) { async function handleClusterInferenceGet(req, res) { try { - const result = await execCmd( - ["nemoclaw", "cluster", "inference", "get"], + const result = await execFirstSuccess( + [ + cliArgs("inference", "get"), + cliArgs("cluster", "inference", "get"), + ], 30000 ); if (result.code !== 0) { @@ -1003,11 +1031,10 @@ async function handleClusterInferenceSet(req, res) { }); } try { - const result = await execCmd( + const result = await execFirstSuccess( [ - "nemoclaw", "cluster", "inference", "set", - "--provider", providerName, - "--model", modelId, + cliArgs("inference", "set", "--provider", providerName, "--model", modelId), + cliArgs("cluster", "inference", "set", "--provider", providerName, "--model", modelId), ], 30000 ); @@ -1239,10 +1266,10 @@ async function handleConnectionDetails(req, res) { gatewayPort: 8080, instructions: { install: - "curl -fsSL https://github.com/NVIDIA/NemoClaw/releases/download/devel/install.sh | sh", - connect: `nemoclaw gateway add ${gatewayUrl}`, - createSandbox: "nemoclaw sandbox create -- claude", - tui: "nemoclaw term", + "curl -fsSL https://github.com/NVIDIA/OpenShell/releases/download/devel/install.sh | sh", + connect: `openshell gateway add ${gatewayUrl}`, + createSandbox: "openshell sandbox create -- claude", + tui: "openshell term", }, }); } @@ -1522,7 +1549,7 @@ function _setMocksForTesting(mocks) { if (require.main === module) { bootstrapConfigCache(); server.listen(PORT, "", () => { - console.log(`NemoClaw Welcome UI → http://localhost:${PORT}`); + console.log(`OpenShell Welcome UI -> http://localhost:${PORT}`); }); } From 7f735c0a0e1a993a48c25044fdb4c29d1ab77586 Mon Sep 17 00:00:00 2001 From: JR Morgan Date: Wed, 11 Mar 2026 16:40:54 -0700 Subject: [PATCH 02/14] Remove BASH_SOURCE dependency --- brev/launch.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/brev/launch.sh b/brev/launch.sh index f57de3b..435b18d 100755 --- a/brev/launch.sh +++ b/brev/launch.sh @@ -2,7 +2,12 @@ set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SOURCE_PATH="${BASH_SOURCE[0]-}" +if [[ -z "$SOURCE_PATH" || "$SOURCE_PATH" == "bash" || "$SOURCE_PATH" == "-bash" ]]; then + SCRIPT_DIR="$PWD" +else + SCRIPT_DIR="$(cd "$(dirname "$SOURCE_PATH")" && pwd)" +fi SCRIPT_REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" REPO_ROOT="" WELCOME_UI_DIR="" @@ -18,8 +23,13 @@ CLONE_ROOT="${CLONE_ROOT:-/home/ubuntu}" CLONE_DIR="${CLONE_DIR:-$CLONE_ROOT/OpenShell-Community}" GATEWAY_LOG="${GATEWAY_LOG:-/tmp/openshell-gateway.log}" WELCOME_UI_LOG="${WELCOME_UI_LOG:-/tmp/welcome-ui.log}" +LAUNCH_LOG="${LAUNCH_LOG:-/tmp/openshell-launch.log}" WAIT_TIMEOUT_SECS="${WAIT_TIMEOUT_SECS:-30}" +mkdir -p "$(dirname "$LAUNCH_LOG")" +touch "$LAUNCH_LOG" +exec > >(tee -a "$LAUNCH_LOG") 2>&1 + log() { printf '[launch.sh] %s\n' "$*" } From 8b419013ef9d4dfcb38f2a22dfd45662e98a971b Mon Sep 17 00:00:00 2001 From: JR Morgan Date: Wed, 11 Mar 2026 16:48:14 -0700 Subject: [PATCH 03/14] Invalid key handlin --- brev/welcome-ui/app.js | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/brev/welcome-ui/app.js b/brev/welcome-ui/app.js index cdcc0eb..0fcd43f 100644 --- a/brev/welcome-ui/app.js +++ b/brev/welcome-ui/app.js @@ -120,6 +120,8 @@ let injectInFlight = false; let injectTimer = null; let lastSubmittedKey = ""; + let keyInjectError = ""; + let installFailed = false; function stopPolling() { if (pollTimer) { @@ -132,6 +134,7 @@ if (key === lastSubmittedKey) return; lastSubmittedKey = key; keyInjected = false; + keyInjectError = ""; injectInFlight = true; updateButtonState(); try { @@ -169,6 +172,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"; @@ -192,12 +198,18 @@ btnSpinner.hidden = true; btnSpinner.style.display = "none"; btnLaunchLabel.textContent = "Open OpenShell"; - } else if (sandboxReady && keyValid && !keyInjected) { + } 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"); @@ -268,6 +280,12 @@ if (!injectInFlight) { keyInjected = !!data.key_injected; } + keyInjectError = data.key_inject_error || ""; + if (keyInjectError) { + injectInFlight = false; + keyInjected = false; + lastSubmittedKey = ""; + } if (data.status === "running") { sandboxReady = true; @@ -284,6 +302,7 @@ } else if (data.status === "error") { stopPolling(); installTriggered = false; + installFailed = true; showError(data.error || "Sandbox creation failed"); } else { updateButtonState(); @@ -308,6 +327,8 @@ sandboxUrl = null; installTriggered = false; keyInjected = false; + keyInjectError = ""; + installFailed = false; lastSubmittedKey = ""; stopPolling(); @@ -391,8 +412,13 @@ cardOpenclaw.addEventListener("click", () => { showOverlay(overlayInstall); - showMainView(); - if (!installTriggered) { + if (installFailed) { + stepError.hidden = false; + installMain.hidden = true; + } else { + showMainView(); + } + if (!installTriggered && !installFailed) { triggerInstall(); } apiKeyInput.focus(); From 7f783451d5f2eb4df847b0d4f4c5c3493b723e96 Mon Sep 17 00:00:00 2001 From: JR Morgan Date: Wed, 11 Mar 2026 16:58:29 -0700 Subject: [PATCH 04/14] Wait for valid key before triggering --- brev/welcome-ui/app.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/brev/welcome-ui/app.js b/brev/welcome-ui/app.js index 0fcd43f..df21a32 100644 --- a/brev/welcome-ui/app.js +++ b/brev/welcome-ui/app.js @@ -152,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); } @@ -418,9 +421,6 @@ } else { showMainView(); } - if (!installTriggered && !installFailed) { - triggerInstall(); - } apiKeyInput.focus(); updateButtonState(); }); From 7625def039eb10a694f3480a79861f0d6454d985 Mon Sep 17 00:00:00 2001 From: JR Morgan Date: Wed, 11 Mar 2026 17:03:29 -0700 Subject: [PATCH 05/14] Address silent fail on launch.sh --- brev/launch.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/brev/launch.sh b/brev/launch.sh index 435b18d..04dfb25 100755 --- a/brev/launch.sh +++ b/brev/launch.sh @@ -221,7 +221,8 @@ install_cli_from_release() { archive="$tmpdir/$pattern" tar xzf "$archive" -C "$tmpdir" sudo install -m 755 "$tmpdir/$candidate" "/usr/local/bin/$candidate" - echo "$candidate" + CLI_BIN="$candidate" + log "Installed CLI from release: $CLI_BIN" return 0 fi done @@ -231,6 +232,8 @@ install_cli_from_release() { } resolve_cli() { + log "Checking for installed CLI binaries..." + if [[ -n "$CLI_BIN" ]]; then require_cmd "$CLI_BIN" log "Using CLI from CLI_BIN: $CLI_BIN" @@ -254,7 +257,7 @@ resolve_cli() { exit 1 fi - CLI_BIN="$(install_cli_from_release)" + install_cli_from_release } ensure_cli_compat_aliases() { From 9e8af2e557f1bf3040197cb2137f097a8941adff Mon Sep 17 00:00:00 2001 From: JR Morgan Date: Wed, 11 Mar 2026 17:04:18 -0700 Subject: [PATCH 06/14] Add retry count and delay --- brev/launch.sh | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/brev/launch.sh b/brev/launch.sh index 04dfb25..f3437b9 100755 --- a/brev/launch.sh +++ b/brev/launch.sh @@ -25,6 +25,8 @@ GATEWAY_LOG="${GATEWAY_LOG:-/tmp/openshell-gateway.log}" WELCOME_UI_LOG="${WELCOME_UI_LOG:-/tmp/welcome-ui.log}" LAUNCH_LOG="${LAUNCH_LOG:-/tmp/openshell-launch.log}" WAIT_TIMEOUT_SECS="${WAIT_TIMEOUT_SECS:-30}" +CLI_RETRY_COUNT="${CLI_RETRY_COUNT:-5}" +CLI_RETRY_DELAY_SECS="${CLI_RETRY_DELAY_SECS:-3}" mkdir -p "$(dirname "$LAUNCH_LOG")" touch "$LAUNCH_LOG" @@ -88,6 +90,26 @@ wait_for_log_pattern() { done } +retry_cli() { + local attempt=1 + local max_attempts="${CLI_RETRY_COUNT}" + local delay_secs="${CLI_RETRY_DELAY_SECS}" + + while true; do + if "$@"; then + return 0 + fi + + if (( attempt >= max_attempts )); then + return 1 + fi + + log "Command failed, retrying (${attempt}/${max_attempts}): $*" + sleep "$delay_secs" + attempt=$((attempt + 1)) + done +} + detect_arch() { case "$(uname -m)" in x86_64|amd64) echo "x86_64" ;; @@ -328,17 +350,29 @@ run_provider_create_or_replace() { shift log "Configuring provider: $name" - if "$CLI_BIN" provider create --name "$name" "$@" >/dev/null 2>&1; then + if retry_cli "$CLI_BIN" provider create --name "$name" "$@" >/dev/null 2>&1; then log "Created provider: $name" return fi log "Provider create failed for $name. Replacing existing provider..." - "$CLI_BIN" provider delete "$name" >/dev/null 2>&1 || true - "$CLI_BIN" provider create --name "$name" "$@" + retry_cli "$CLI_BIN" provider delete "$name" >/dev/null 2>&1 || true + retry_cli "$CLI_BIN" provider create --name "$name" "$@" log "Recreated provider: $name" } +wait_for_gateway_cli() { + log "Waiting for gateway CLI operations to stabilize..." + if retry_cli "$CLI_BIN" provider list --names >/dev/null 2>&1; then + log "Gateway CLI is responsive." + return + fi + + log "Gateway CLI did not stabilize. Last gateway log lines:" + tail -n 50 "$GATEWAY_LOG" || true + exit 1 +} + start_gateway() { : > "$GATEWAY_LOG" log "Resetting gateway state if it already exists..." @@ -364,6 +398,7 @@ start_gateway() { fi log "Gateway reported ready." + wait_for_gateway_cli } install_ui_deps() { From 6cbe026719197ca78e9920f46118d40cecff798c Mon Sep 17 00:00:00 2001 From: JR Morgan Date: Wed, 11 Mar 2026 17:22:00 -0700 Subject: [PATCH 07/14] Add favicon, handle ghcr.io login if params present, fix logo --- brev/launch.sh | 50 ++++++++++++++++++++++++++++++++++++ brev/welcome-ui/favicon.ico | Bin 0 -> 25214 bytes brev/welcome-ui/index.html | 24 +++++++++-------- brev/welcome-ui/styles.css | 9 ++++--- 4 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 brev/welcome-ui/favicon.ico diff --git a/brev/launch.sh b/brev/launch.sh index f3437b9..78243e8 100755 --- a/brev/launch.sh +++ b/brev/launch.sh @@ -27,6 +27,8 @@ LAUNCH_LOG="${LAUNCH_LOG:-/tmp/openshell-launch.log}" WAIT_TIMEOUT_SECS="${WAIT_TIMEOUT_SECS:-30}" CLI_RETRY_COUNT="${CLI_RETRY_COUNT:-5}" CLI_RETRY_DELAY_SECS="${CLI_RETRY_DELAY_SECS:-3}" +GHCR_LOGIN="${GHCR_LOGIN:-auto}" +GHCR_USER="${GHCR_USER:-}" mkdir -p "$(dirname "$LAUNCH_LOG")" touch "$LAUNCH_LOG" @@ -155,6 +157,52 @@ gh_auth_if_needed() { fi } +resolve_ghcr_user() { + if [[ -n "$GHCR_USER" ]]; then + return 0 + fi + + if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then + GHCR_USER="$(gh api user -q .login 2>/dev/null || true)" + fi + + if [[ -z "$GHCR_USER" ]]; then + GHCR_USER="${GITHUB_USER:-${USER:-}}" + fi + + [[ -n "$GHCR_USER" ]] +} + +docker_login_ghcr_if_needed() { + if [[ "$GHCR_LOGIN" == "0" || "$GHCR_LOGIN" == "false" || "$GHCR_LOGIN" == "no" ]]; then + log "Skipping GHCR login by configuration." + return + fi + + if [[ -z "$GITHUB_TOKEN" ]]; then + log "No GitHub token available; skipping GHCR login." + return + fi + + if ! command -v docker >/dev/null 2>&1; then + log "Docker not available; skipping GHCR login." + return + fi + + if ! resolve_ghcr_user; then + log "Could not determine GHCR username; skipping GHCR login." + return + fi + + log "Logging into ghcr.io as $GHCR_USER ..." + if printf '%s\n' "$GITHUB_TOKEN" | docker login ghcr.io -u "$GHCR_USER" --password-stdin >/dev/null 2>&1; then + log "GHCR login succeeded." + return + fi + + log "GHCR login failed. Continuing, but private image pulls may fail." +} + checkout_repo_ref() { if [[ -z "$COMMUNITY_REF" ]]; then return @@ -443,6 +491,8 @@ main() { step "Resolving CLI" resolve_cli ensure_cli_compat_aliases + step "Authenticating registries" + docker_login_ghcr_if_needed step "Ensuring Node.js" ensure_node diff --git a/brev/welcome-ui/favicon.ico b/brev/welcome-ui/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9e35bef5b8258264d8e67b0ff69b4b29feecdcba GIT binary patch literal 25214 zcmeHP33wdEm42<3J!8qAWg;Ik2zzW{8y^sDCd6Wf1u; z0@U^Z;?F>HEIG|k>E9ntTB4=lk7&XSTol=Qd69;=$A_?_bga#>#7Ip?sp(k-Q%qq^TNG_#7HJDlIXPlYRw2K&lrQ{co zsS@liPjYT4{c0E#A9j-zPolG#j6Y9M1G^cD$K!FB(EhLiwi`%Dy9mrigJDbkhj|LGzYjA$0CfnE>n zM2ic)TDw8*1hQOWB+Io(5}oK?G{YrDtzS7wulK-cBm>or(GD#1Sok`j6bq2pjn!Pa z)9VE5wY^TL&%@dj?}EN`CnTZ?X~!~(OCHVT&ZX7d53n33APSRRPz|{>RxB(G@WhpL z2Rotmq2;iX>EZ&L&BDIOIZ3VyX{>y4`it(aMUg9#zYhuG(UFX2s9&Y(4lF^~j<{8* zbWVFq%kuX1UG0AP<0Ja|F7b)k-%qS@?Jbc$yRYW}`<+*7y5F$d-g_|i`WsI)H+Emv zQ*YP5G0v_(=?eSGY4!FSuQb{3^z_=Bapm=|@!L~JFR+!wc|IP90cM`N$d^toW6z1L z2|khC(Rt3eV}voT%Q4a|f-^QUMfRF5a?Ob%Z-c+Z(lX&T zj1!GD13cfVg!678p$#fjvT`XA&yh;+T#4MhOvc=Oo{a5kk(v$XOU*AYkX<$azr0W; z0F&-q8O;uII+De){P=7-$j z4XUdOR^~=@R|@h_K33IEGW{J#H-Z+klvl1CVspXO@;WmIY`=kW zMSZ}ZdbF*9;B07^#|H36_pnk|;8dfPYk%mVQam3+O#iA(1W{HKq*xw~8&42}idr^N z32ZGMx*GKOV$xP9SI~FA6RN)xbvx5p?GJe|u*}q;@AD_pV>M#1K3HCjRm6;=4u|$7 z7Q}`*qZB&FVnXjuV;rxsDn~UmR1_bI=)}rM4X1>k>L|Q*dGv#G$4E~oc14v!{ur_O z$jpz`hMWjx3&A$hpk{uSP#^AZp@o{CzURaEui9*{{G3?Bj0O##nBojvedV;1ML^$X z@;-}cB-F){F~oD8@^h;p<68XO%D+#o3{4^~FlLeAJ?h=xmC?LM?ZQ3kh87uT?ooMv zdQYpmKb>;#N|~C!NM>woli9dG-Lrd@% ztx>aP)4)7PC^nX7m6>2+^=^O)>K})yiI#;I=ul{@dL_kMB;9}Ln5mBV{vmP{jYlbo z`ueGsg%|A5tdo*eWmS(}%(E(_<|ykkAn+nok2Wgy^j~FssM={WJe`$H|0nMq{BufS+!0phO&RBuQK55670xK*@f>~f zy(^>+n3lg-Vt9VO$9;Hy?w?KO-+!4T9==xge`GbDm+N!LS8xtF9OsZ@@r?Y$XK#~J zpAXI)XXDJ_;F);K%ekF)Ov{d!Bk*a80M@4wcBj%T=&JpA_2fwHE-2SxOPjI_cdhzL zEHEfmO&PzN)RKd8r66NfjX~{1)K}HYXuct>tCl*o@l@4I9iFR<;pVD0n(?UL6%A=q z*~7;q|EZQ=t}@BgZ!-pd5j61j-Tks3MR{t(08q>c1mS z|EOG7dUC1X;Cnm$1>{m&MwH+7cK4M1_al%?Rbbw{1ia~^7j!eQWmI zw;Mp80^UPDRy3ZuR5kc}0EeRP9MD^U7m@!9sGeJ;=1M6Z=jCAj_WSQSGoj}?*#9Fi zP-n6KMV$#1a;aU>_9)={(Df?l<|18#{nXdgVE(rIpG(!i*5$yTfbg8)e7O}k1IPl8 z01p8hfSZ99;1FO!s0Tqdm#ReFKENda=ii4wSpQJ@ZS&uAnxOMV6{8^LPojJ@`2GV( zxHRFWrNFFqcb>M*Tw2TS5N>3>@Eq>)G)B=TgT)&->76+Wr~&6nMBV z3MJ2}1>Xbz4&bLi9_8PH4xHzVC)e3!0H14?PM$Lj^{XJ;3p%p-lS?f|`@aEZ4BtZD z|I#na=2A9rEI?n`uUL^Yxzs4|<^kseuL4Gop}zs|c%X3OSJa+`Txtq%9q?Dcv@>+; z@%Nl>qTkwgJ&wOanXmhGV%~EeRXTNiIByq%_ch=+;2@Ow09q%`bIYNG?mZ=|=MCpiIPM3dPLHdue;8%@%lV@9>ps7a@_Ha% z@X2$kAa@~n1LybK;2#bCP-788{r>=_--G>U%(zAyKeT_rIYVE|ycX#v$MJ`tcpQMV z_3M{oe=X#K`OZFh&a>c~>xw>c9la9Nd<{x>o7bA>dKDu2g|F2g%S+B7HRDTyJ zm--CyzYf)RBF}diI??~2ooOGYPov)d;tZPTHTkv@ZD?}>U;*6w9tXmDOupEE&q+Y; zujs3p1DyZiIsZLXM_r!t2cY;HE~Voi9&hU98nCbFGV;7G+5o-o2XkW+c#MzU+XBA! zP5Th|?>QBazYp@kczVvwAv+DI56{_Ug*uZN?Egz>vm3yDEzp}wu}?uCIOjd*oPw{# zvf*|E^MU`l)WOOI+iLLawYPB%`Ab9foX`3zQw0HJisui)LlGyXq`e6S`_&U3yF-cWPC^!(S?`VzF&X9YdRoC7Br^NQwD{|H`qJ)q1c zzyo+rEB1$tv~I3{VcpN8yxRcXUj*-6z|7&{Xb^v+D3?ki2Co37pMOLCV(1Lw$u*N_ zH+{Xc-pIy`ngaEl&!YYgwEr+9_W<(6YnSI72AS}lXmpyf8hZW|3?jp`nCD~w-G5E* zL3xj`7|L9qr-7aTaNqe?&_%#8fQvHy)cwG5u?~(h1&-(;@JrwKu`2070$)(1?KI3@{zW^8B5deBN#3@66;^xOHs5BJGYPwIl9mO;_43%XZs} zJgI1E<)SUu7~VeLFL@51;dKIU0;d#w_8fi}VJ`3ufbTo*1O5z{=S{=Wt=spU2=tr_ z@SP0xu)lW$mjZmw-U=)R_65d-e4@W!1=a%p4GiT&Y5RO{$@ieT?K_~$0RC?i#=q3~ z^O@RBK=A%V%Z>%_c!0V}jn!cL`W-CeOuwH2^z&8EnS=c0!0Es=U^Z|ha08&9@#^Pa zIh5xrU(JVpm-A)t9|ywmD7LTl{??ay8kEm`ZGdCTXSRICcr|bZz%e)s7zNY-D*(P* zrT#wPd?0wft>Zym4q)EL1@`rLKY}{8XFMAKzJpo=kjGfe0tUwaMeq($UDfpuqD*;$ z&!>ZD;-0e$%8a3Tz8<#U3chK#*w=RwcpSI40R4Zh2HR&FzT3D6;4{?Gz-qvZcMtOW z0H*=eV{DLW`<@d+p0OqGIe>Z?Q#1FMn4ttO-2Y#od>g=aw8uKej93on=b_p^<4-C3 z*F)Cu|Hbe75}(Jug=1*!bIwf%Xe$jETWuj+#M_dsgwa~)d;&{y{3Y~KftR|v z{lJd{pu+=rt#VJf7ijmvHLn%;GN8ZL;5oY^&v^U=C}pb@Z|n90KW0H6uccoBZv&^Q zDt(>LK>0X;v8IouYz)O4-oEjJRL?2)k?R823y#rH`iHCAKKt4SUVp>s-PZb%*gqTg zfAtaJgm8Gv`EmsQKSn@5Zy4~|BUZnUEn~&%lV+vw)_XeH^*!F&474QXs z_aCpQJRSC&gMklKdwsw5KFU*pXMt}4hXeGH=QP^i6QEtjgl96npVPiRS5W^n;E%u| z0Ne9BB~gHT<9`CY&$t$#UJbUtE5-h90;r#RH|1CR`IA6H#Gg8t)+`_wy n^V+bu?X(4^$<9j6|6A=t2Z%i20_LZ4nPqZ;9)L2Y4e9>^moyUg literal 0 HcmV?d00001 diff --git a/brev/welcome-ui/index.html b/brev/welcome-ui/index.html index 8a263a2..aed074e 100644 --- a/brev/welcome-ui/index.html +++ b/brev/welcome-ui/index.html @@ -3,7 +3,8 @@ - NemoClaw — Agent Sandbox + OpenShell — Agent Sandbox + @@ -14,12 +15,13 @@
- - NemoClaw + OpenShell Sandbox
@@ -31,10 +33,10 @@

- Run Any AI Agent in
the NemoClaw Sandbox + Run Any AI Agent in
the OpenShell Sandbox

- NemoClaw lets any AI agent run in a secure sandbox with policy guardrails + OpenShell lets any AI agent run in a secure sandbox with policy guardrails the agent itself helps manage. One launchable, two paths.

@@ -47,7 +49,7 @@

Install OpenClaw

-

Experience it first, learn later. One-click install of the OpenClaw coding agent with NemoClaw safety policies, sandboxing, and model routing. Everything in one browser tab.

+

Experience it first, learn later. One-click install of the OpenClaw coding agent with OpenShell safety policies, sandboxing, and model routing. Everything in one browser tab.

Other Agents

-

Bring your own agent. Run Claude Code, OpenCode, DeepAgents, or your own framework. Manage policies via the NemoClaw CLI and TUI from your laptop.

+

Bring your own agent. Run Claude Code, OpenCode, DeepAgents, or your own framework. Manage policies via the OpenShell CLI and TUI from your laptop.