Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f81e586
Add rebrand for welcome UI, include launch.sh script
liveaverage Mar 11, 2026
53258bc
Remove BASH_SOURCE dependency
liveaverage Mar 11, 2026
9842d40
Address silent fail on launch.sh
liveaverage Mar 12, 2026
eb74ff4
Add favicon, handle ghcr.io login if params present, fix logo
liveaverage Mar 12, 2026
485a94e
Init LiteLLM implementation
nv-kasikritc Mar 12, 2026
754c756
LiteLLM working
nv-kasikritc Mar 12, 2026
dc1d7ac
Update welcome UI icon assets
liveaverage Mar 12, 2026
ce8197a
Add on-demand nemoclaw build; improve auto-pair
liveaverage Mar 12, 2026
6c319fa
Logo fixup, improve auto-approve cycle, NO_PROXY for localhost
liveaverage Mar 12, 2026
536e63c
Bump defualt context window, set NO_PROXY widely
liveaverage Mar 13, 2026
ec48954
Extend timer for device auto approval, minimize wait
liveaverage Mar 13, 2026
7d6355f
Reload dashboard once after pairing approval
liveaverage Mar 13, 2026
e512644
Revert nemoclaw runtime back to inference.local
liveaverage Mar 13, 2026
10d871a
Keep pairing watcher alive until approval
liveaverage Mar 13, 2026
9483694
Add proxy request tracing for sandbox launch
liveaverage Mar 13, 2026
b2f361c
Add override to skip nemoclaw image build
liveaverage Mar 13, 2026
6784eae
Add revised policy and NO_PROXY
liveaverage Mar 13, 2026
29720c9
Fix unconditional chown
liveaverage Mar 14, 2026
61e84fa
Added guarded reload for pairing; ensure custom policy.yaml bake-in
liveaverage Mar 14, 2026
93436b0
Add console logging for device pairing; extend NO_PROXY
liveaverage Mar 14, 2026
59b4389
Handle context mod for inference.local
liveaverage Mar 14, 2026
c35e759
Fix k3s image import on build; force reload on first pass timeout
liveaverage Mar 14, 2026
9ef9e78
Revise Brev README
liveaverage Mar 14, 2026
efeb9aa
Cleanup Brev section
liveaverage Mar 14, 2026
7aa8d18
Revert policy.yaml to orig
liveaverage Mar 14, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/AGENTS.md
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ This repo is the community ecosystem around OpenShell -- a hub for contributed s

### Quick Start with Brev

TODO: Add Brev instructions
Skip the setup and launch OpenShell Community on a fully configured Brev instance, whether you want to use Brev as a remote OpenShell gateway with or without GPU accelerators, or as an all-in-one playground for sandboxes, inference, and UI workflows.

| Instance | Best For | Deploy |
| -------- | -------- | ------ |
| CPU-only | Remote OpenShell gateway deployments, external inference endpoints, remote APIs, and lighter-weight sandbox workflows | <a href="https://brev.nvidia.com/"><img src="https://brev-assets.s3.us-west-1.amazonaws.com/nv-lb-dark.svg" alt="Deploy on Brev" height="40"/></a> |
| NVIDIA H100 | All-in-one OpenShell playgrounds, locally hosted LLM endpoints, GPU-heavy sandboxes, and higher-throughput agent workloads | <a href="https://brev.nvidia.com/"><img src="https://brev-assets.s3.us-west-1.amazonaws.com/nv-lb-dark.svg" alt="Deploy on Brev" height="40"/></a> |

After the Brev instance is ready, access the Welcome UI to inject provider keys and access your Openclaw sandbox.

### Using Sandboxes

Expand Down
3 changes: 2 additions & 1 deletion brev/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
brev-start-vm.sh
brev-start-vm.sh
reset.sh
151 changes: 150 additions & 1 deletion brev/launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ 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:-}"
DEFAULT_NEMOCLAW_IMAGE="ghcr.io/nvidia/openshell-community/sandboxes/nemoclaw:latest"
if [[ -n "${NEMOCLAW_IMAGE+x}" ]]; then
NEMOCLAW_IMAGE_EXPLICIT=1
else
NEMOCLAW_IMAGE_EXPLICIT=0
fi
NEMOCLAW_IMAGE="${NEMOCLAW_IMAGE:-$DEFAULT_NEMOCLAW_IMAGE}"
SKIP_NEMOCLAW_IMAGE_BUILD="${SKIP_NEMOCLAW_IMAGE_BUILD:-}"
CLUSTER_CONTAINER_NAME="${CLUSTER_CONTAINER_NAME:-openshell-cluster-openshell}"

mkdir -p "$(dirname "$LAUNCH_LOG")"
touch "$LAUNCH_LOG"
Expand Down Expand Up @@ -252,6 +261,136 @@ docker_login_ghcr_if_needed() {
fi
}

should_build_nemoclaw_image() {
if [[ "$SKIP_NEMOCLAW_IMAGE_BUILD" == "1" || "$SKIP_NEMOCLAW_IMAGE_BUILD" == "true" || "$SKIP_NEMOCLAW_IMAGE_BUILD" == "yes" ]]; then
return 1
fi
[[ -n "$COMMUNITY_REF" && "$COMMUNITY_REF" != "main" ]]
}

maybe_use_branch_local_nemoclaw_tag() {
if ! should_build_nemoclaw_image; then
return
fi

if [[ "$NEMOCLAW_IMAGE_EXPLICIT" == "1" || "$NEMOCLAW_IMAGE" != "$DEFAULT_NEMOCLAW_IMAGE" ]]; then
return
fi

NEMOCLAW_IMAGE="ghcr.io/nvidia/openshell-community/sandboxes/nemoclaw:local-dev"
log "Using non-main branch NeMoClaw image tag: $NEMOCLAW_IMAGE"
}

build_nemoclaw_image_if_needed() {
local docker_cmd=()
local image_context="$REPO_ROOT/sandboxes/nemoclaw"
local dockerfile_path="$image_context/Dockerfile"

if ! should_build_nemoclaw_image; then
if [[ "$SKIP_NEMOCLAW_IMAGE_BUILD" == "1" || "$SKIP_NEMOCLAW_IMAGE_BUILD" == "true" || "$SKIP_NEMOCLAW_IMAGE_BUILD" == "yes" ]]; then
log "Skipping local NeMoClaw image build by override (SKIP_NEMOCLAW_IMAGE_BUILD=${SKIP_NEMOCLAW_IMAGE_BUILD})."
else
log "Skipping local NeMoClaw image build (COMMUNITY_REF=${COMMUNITY_REF:-<unset>})."
fi
return
fi

if [[ ! -f "$dockerfile_path" ]]; then
log "NeMoClaw Dockerfile not found: $dockerfile_path"
exit 1
fi

if command -v docker >/dev/null 2>&1; then
docker_cmd=(docker)
elif command -v sudo >/dev/null 2>&1; then
docker_cmd=(sudo docker)
else
log "Docker is required to build the NeMoClaw sandbox image."
exit 1
fi

log "Building local NeMoClaw image for non-main ref '$COMMUNITY_REF': $NEMOCLAW_IMAGE"
if ! "${docker_cmd[@]}" build \
--pull \
--tag "$NEMOCLAW_IMAGE" \
--file "$dockerfile_path" \
"$image_context"; then
log "Local NeMoClaw image build failed."
exit 1
fi

log "Local NeMoClaw image ready: $NEMOCLAW_IMAGE"
}

resolve_docker_cmd() {
if command -v docker >/dev/null 2>&1; then
printf 'docker'
return 0
fi
if command -v sudo >/dev/null 2>&1; then
printf 'sudo docker'
return 0
fi
return 1
}

resolve_cluster_container_name() {
local docker_bin

if [[ -n "$CLUSTER_CONTAINER_NAME" ]]; then
printf '%s' "$CLUSTER_CONTAINER_NAME"
return 0
fi

docker_bin="$(resolve_docker_cmd)" || return 1

CLUSTER_CONTAINER_NAME="$($docker_bin ps --format '{{.Names}}\t{{.Image}}' | awk '$1 ~ /^openshell-cluster-/ { print $1; exit }')"
if [[ -z "$CLUSTER_CONTAINER_NAME" ]]; then
CLUSTER_CONTAINER_NAME="$($docker_bin ps --format '{{.Names}}\t{{.Image}}' | awk '$2 ~ /ghcr.io\\/nvidia\\/openshell\\/cluster/ { print $1; exit }')"
fi

[[ -n "$CLUSTER_CONTAINER_NAME" ]]
}

import_nemoclaw_image_into_cluster_if_needed() {
local docker_bin cluster_name

if ! should_build_nemoclaw_image && [[ "$NEMOCLAW_IMAGE_EXPLICIT" != "1" ]]; then
log "Skipping cluster image import; using registry-backed image: $NEMOCLAW_IMAGE"
return
fi

docker_bin="$(resolve_docker_cmd)" || {
log "Docker not available; skipping cluster image import."
return
}

if ! $docker_bin image inspect "$NEMOCLAW_IMAGE" >/dev/null 2>&1; then
log "Local NeMoClaw image not present on host; skipping cluster image import: $NEMOCLAW_IMAGE"
return
fi

if ! cluster_name="$(resolve_cluster_container_name)"; then
log "OpenShell cluster container not found; skipping cluster image import."
return
fi

log "Importing NeMoClaw image into cluster containerd: $NEMOCLAW_IMAGE -> $cluster_name"
if ! $docker_bin save "$NEMOCLAW_IMAGE" | $docker_bin exec -i "$cluster_name" sh -lc 'ctr -n k8s.io images import -'; then
log "Failed to import NeMoClaw image into cluster containerd."
exit 1
fi

if ! $docker_bin exec -i "$cluster_name" sh -lc "ctr -n k8s.io images ls | awk '{print \$1}' | grep -Fx '$NEMOCLAW_IMAGE' >/dev/null"; then
log "Imported image tag not found in cluster containerd: $NEMOCLAW_IMAGE"
log "Cluster image list:"
$docker_bin exec -i "$cluster_name" sh -lc "ctr -n k8s.io images ls | grep 'sandboxes/nemoclaw' || true"
exit 1
fi

log "Cluster image import complete: $NEMOCLAW_IMAGE"
}

checkout_repo_ref() {
if [[ -z "$COMMUNITY_REF" ]]; then
return
Expand Down Expand Up @@ -518,7 +657,12 @@ start_welcome_ui() {
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 &
nohup env \
PORT="$PORT" \
REPO_ROOT="$REPO_ROOT" \
CLI_BIN="$CLI_BIN" \
NEMOCLAW_IMAGE="$NEMOCLAW_IMAGE" \
node server.js >> "$WELCOME_UI_LOG" 2>&1 &
WELCOME_UI_PID=$!
export WELCOME_UI_PID
log "Welcome UI PID: $WELCOME_UI_PID"
Expand All @@ -542,8 +686,11 @@ main() {
step "Resolving CLI"
resolve_cli
ensure_cli_compat_aliases
maybe_use_branch_local_nemoclaw_tag
step "Authenticating registries"
docker_login_ghcr_if_needed
step "Preparing NeMoClaw image"
build_nemoclaw_image_if_needed
step "Ensuring Node.js"
ensure_node

Expand All @@ -555,6 +702,8 @@ main() {

step "Starting gateway"
start_gateway
step "Importing NeMoClaw image into cluster"
import_nemoclaw_image_into_cluster_if_needed

step "Configuring providers"
run_provider_create_or_replace \
Expand Down
20 changes: 20 additions & 0 deletions brev/welcome-ui/OpenShell-Icon-Logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions brev/welcome-ui/OpenShell-Icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified brev/welcome-ui/favicon.ico
Binary file not shown.
4 changes: 2 additions & 2 deletions brev/welcome-ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenShell — Agent Sandbox</title>
<link rel="icon" type="image/svg+xml" href="/openshell-mark.svg">
<link rel="icon" type="image/svg+xml" href="/OpenShell-Icon.svg">
<link rel="alternate icon" href="/favicon.ico" sizes="any">
<link rel="stylesheet" href="styles.css?v=7">
<link rel="preconnect" href="https://fonts.googleapis.com">
Expand All @@ -16,7 +16,7 @@
<!-- Top bar -->
<header class="topbar">
<div class="topbar__brand">
<img class="topbar__logo" src="openshell-mark.svg" alt="OpenShell">
<img class="topbar__logo" src="OpenShell-Icon-Logo.svg" alt="OpenShell">
<span class="topbar__divider"></span>
<span class="topbar__title">OpenShell</span>
<span class="topbar__badge">Sandbox</span>
Expand Down
5 changes: 0 additions & 5 deletions brev/welcome-ui/openshell-mark.svg

This file was deleted.

59 changes: 51 additions & 8 deletions brev/welcome-ui/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const SANDBOX_START_CMD = process.env.SANDBOX_START_CMD || "nemoclaw-start";
const SANDBOX_BASE_IMAGE =
process.env.SANDBOX_BASE_IMAGE ||
"ghcr.io/nvidia/openshell-community/sandboxes/openclaw:latest";
const NEMOCLAW_IMAGE = (process.env.NEMOCLAW_IMAGE || "").trim();
const POLICY_FILE = path.join(SANDBOX_DIR, "policy.yaml");

const LOG_FILE = "/tmp/nemoclaw-sandbox-create.log";
Expand Down Expand Up @@ -264,6 +265,12 @@ const injectKeyState = {
keyHash: null,
};

// Raw API key stored in memory so it can be passed to the sandbox at
// creation time. Not persisted to disk.
let _nvidiaApiKey = process.env.NVIDIA_INFERENCE_API_KEY
|| process.env.NVIDIA_INTEGRATE_API_KEY
|| "";

// ── Brev ID detection & URL building ───────────────────────────────────────

function extractBrevId(host) {
Expand All @@ -286,7 +293,7 @@ function buildOpenclawUrl(token) {
} else {
url = `http://127.0.0.1:${PORT}/`;
}
if (token) url += `?token=${token}`;
if (token) url += `#token=${token}`;
return url;
}

Expand Down Expand Up @@ -627,18 +634,44 @@ function runSandboxCreate() {
const cmd = [
CLI_BIN, "sandbox", "create",
"--name", SANDBOX_NAME,
"--from", SANDBOX_DIR,
"--from", NEMOCLAW_IMAGE || SANDBOX_DIR,
"--forward", "18789",
];
if (policyPath) cmd.push("--policy", policyPath);
cmd.push(
"--",
"env",
`CHAT_UI_URL=${chatUiUrl}`,
SANDBOX_START_CMD
);
const envArgs = [`CHAT_UI_URL=${chatUiUrl}`];
const loopbackNoProxy = [
"127.0.0.1",
"localhost",
"::1",
"navigator.navigator.svc.cluster.local",
".svc",
".svc.cluster.local",
"10.42.0.0/16",
"10.43.0.0/16",
].join(",");
const mergedNoProxy = [
process.env.NO_PROXY || process.env.no_proxy || "",
loopbackNoProxy,
]
.filter(Boolean)
.join(",");
envArgs.push(`NO_PROXY=${mergedNoProxy}`);
envArgs.push(`no_proxy=${mergedNoProxy}`);
const nvapiKey = _nvidiaApiKey
|| process.env.NVIDIA_INFERENCE_API_KEY
|| process.env.NVIDIA_INTEGRATE_API_KEY
|| "";
if (nvapiKey) {
envArgs.push(`NVIDIA_INFERENCE_API_KEY=${nvapiKey}`);
envArgs.push(`NVIDIA_INTEGRATE_API_KEY=${nvapiKey}`);
}

cmd.push("--", "env", ...envArgs, SANDBOX_START_CMD);

const cmdDisplay = cmd.slice(0, 8).join(" ") + " -- ...";
if (NEMOCLAW_IMAGE) {
logWelcome(`Using NeMoClaw image override: ${NEMOCLAW_IMAGE}`);
}
logWelcome(`Running: ${cmdDisplay}`);

const logFd = fs.openSync(LOG_FILE, "w");
Expand Down Expand Up @@ -1077,6 +1110,9 @@ async function handleClusterInferenceSet(req, res) {
// ── Reverse proxy (HTTP) ───────────────────────────────────────────────────

function proxyToSandbox(clientReq, clientRes) {
logWelcome(
`proxy http in ${clientReq.method || "GET"} ${clientReq.url || "/"} -> 127.0.0.1:${SANDBOX_PORT}`
);
const headers = {};
for (const [key, val] of Object.entries(clientReq.headers)) {
if (key.toLowerCase() === "host") continue;
Expand All @@ -1094,6 +1130,9 @@ function proxyToSandbox(clientReq, clientRes) {
};

const upstream = http.request(opts, (upstreamRes) => {
logWelcome(
`proxy http out ${clientReq.method || "GET"} ${clientReq.url || "/"} status=${upstreamRes.statusCode || 0}`
);
// Filter hop-by-hop + content-length (we'll set our own)
const outHeaders = {};
for (const [key, val] of Object.entries(upstreamRes.headers)) {
Expand Down Expand Up @@ -1132,6 +1171,7 @@ function proxyToSandbox(clientReq, clientRes) {
// ── Reverse proxy (WebSocket) ──────────────────────────────────────────────

function proxyWebSocket(req, clientSocket, head) {
logWelcome(`proxy ws in ${req.method || "GET"} ${req.url || "/"} -> 127.0.0.1:${SANDBOX_PORT}`);
const upstream = net.createConnection(
{ host: "127.0.0.1", port: SANDBOX_PORT },
() => {
Expand Down Expand Up @@ -1271,8 +1311,10 @@ async function handleInjectKey(req, res) {
injectKeyState.status = "injecting";
injectKeyState.error = null;
injectKeyState.keyHash = keyH;
_nvidiaApiKey = key;

runInjectKey(key, keyH);

return jsonResponse(res, 202, { ok: true, started: true });
}

Expand Down Expand Up @@ -1561,6 +1603,7 @@ function _resetForTesting() {
detectedBrevId = "";
_brevEnvId = "";
renderedIndex = null;
_nvidiaApiKey = "";
}

function _setMocksForTesting(mocks) {
Expand Down
Loading
Loading