Skip to content

Commit e5bdb94

Browse files
Fix Brev NeMoClaw Launch Path, Image Loading, and Pairing Bootstrap (#30)
* Add rebrand for welcome UI, include launch.sh script * Remove BASH_SOURCE dependency * Address silent fail on launch.sh * Add favicon, handle ghcr.io login if params present, fix logo * Init LiteLLM implementation * LiteLLM working * Update welcome UI icon assets * Add on-demand nemoclaw build; improve auto-pair * Logo fixup, improve auto-approve cycle, NO_PROXY for localhost * Bump defualt context window, set NO_PROXY widely * Extend timer for device auto approval, minimize wait * Reload dashboard once after pairing approval * Revert nemoclaw runtime back to inference.local * Keep pairing watcher alive until approval * Add proxy request tracing for sandbox launch * Add override to skip nemoclaw image build * Add revised policy and NO_PROXY * Fix unconditional chown * Added guarded reload for pairing; ensure custom policy.yaml bake-in * Add console logging for device pairing; extend NO_PROXY * Handle context mod for inference.local * Fix k3s image import on build; force reload on first pass timeout * Revise Brev README * Cleanup Brev section * Revert policy.yaml to orig --------- Co-authored-by: nv-kasikritc <kchantharuan@nvidia.com>
1 parent e8030cb commit e5bdb94

File tree

16 files changed

+612
-62
lines changed

16 files changed

+612
-62
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/AGENTS.md

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,14 @@ This repo is the community ecosystem around OpenShell -- a hub for contributed s
3030

3131
### Quick Start with Brev
3232

33-
TODO: Add Brev instructions
33+
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.
34+
35+
| Instance | Best For | Deploy |
36+
| -------- | -------- | ------ |
37+
| 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> |
38+
| 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> |
39+
40+
After the Brev instance is ready, access the Welcome UI to inject provider keys and access your Openclaw sandbox.
3441

3542
### Using Sandboxes
3643

brev/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
brev-start-vm.sh
1+
brev-start-vm.sh
2+
reset.sh

brev/launch.sh

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ CLI_RETRY_COUNT="${CLI_RETRY_COUNT:-5}"
3232
CLI_RETRY_DELAY_SECS="${CLI_RETRY_DELAY_SECS:-3}"
3333
GHCR_LOGIN="${GHCR_LOGIN:-auto}"
3434
GHCR_USER="${GHCR_USER:-}"
35+
DEFAULT_NEMOCLAW_IMAGE="ghcr.io/nvidia/openshell-community/sandboxes/nemoclaw:latest"
36+
if [[ -n "${NEMOCLAW_IMAGE+x}" ]]; then
37+
NEMOCLAW_IMAGE_EXPLICIT=1
38+
else
39+
NEMOCLAW_IMAGE_EXPLICIT=0
40+
fi
41+
NEMOCLAW_IMAGE="${NEMOCLAW_IMAGE:-$DEFAULT_NEMOCLAW_IMAGE}"
42+
SKIP_NEMOCLAW_IMAGE_BUILD="${SKIP_NEMOCLAW_IMAGE_BUILD:-}"
43+
CLUSTER_CONTAINER_NAME="${CLUSTER_CONTAINER_NAME:-openshell-cluster-openshell}"
3544

3645
mkdir -p "$(dirname "$LAUNCH_LOG")"
3746
touch "$LAUNCH_LOG"
@@ -252,6 +261,136 @@ docker_login_ghcr_if_needed() {
252261
fi
253262
}
254263

264+
should_build_nemoclaw_image() {
265+
if [[ "$SKIP_NEMOCLAW_IMAGE_BUILD" == "1" || "$SKIP_NEMOCLAW_IMAGE_BUILD" == "true" || "$SKIP_NEMOCLAW_IMAGE_BUILD" == "yes" ]]; then
266+
return 1
267+
fi
268+
[[ -n "$COMMUNITY_REF" && "$COMMUNITY_REF" != "main" ]]
269+
}
270+
271+
maybe_use_branch_local_nemoclaw_tag() {
272+
if ! should_build_nemoclaw_image; then
273+
return
274+
fi
275+
276+
if [[ "$NEMOCLAW_IMAGE_EXPLICIT" == "1" || "$NEMOCLAW_IMAGE" != "$DEFAULT_NEMOCLAW_IMAGE" ]]; then
277+
return
278+
fi
279+
280+
NEMOCLAW_IMAGE="ghcr.io/nvidia/openshell-community/sandboxes/nemoclaw:local-dev"
281+
log "Using non-main branch NeMoClaw image tag: $NEMOCLAW_IMAGE"
282+
}
283+
284+
build_nemoclaw_image_if_needed() {
285+
local docker_cmd=()
286+
local image_context="$REPO_ROOT/sandboxes/nemoclaw"
287+
local dockerfile_path="$image_context/Dockerfile"
288+
289+
if ! should_build_nemoclaw_image; then
290+
if [[ "$SKIP_NEMOCLAW_IMAGE_BUILD" == "1" || "$SKIP_NEMOCLAW_IMAGE_BUILD" == "true" || "$SKIP_NEMOCLAW_IMAGE_BUILD" == "yes" ]]; then
291+
log "Skipping local NeMoClaw image build by override (SKIP_NEMOCLAW_IMAGE_BUILD=${SKIP_NEMOCLAW_IMAGE_BUILD})."
292+
else
293+
log "Skipping local NeMoClaw image build (COMMUNITY_REF=${COMMUNITY_REF:-<unset>})."
294+
fi
295+
return
296+
fi
297+
298+
if [[ ! -f "$dockerfile_path" ]]; then
299+
log "NeMoClaw Dockerfile not found: $dockerfile_path"
300+
exit 1
301+
fi
302+
303+
if command -v docker >/dev/null 2>&1; then
304+
docker_cmd=(docker)
305+
elif command -v sudo >/dev/null 2>&1; then
306+
docker_cmd=(sudo docker)
307+
else
308+
log "Docker is required to build the NeMoClaw sandbox image."
309+
exit 1
310+
fi
311+
312+
log "Building local NeMoClaw image for non-main ref '$COMMUNITY_REF': $NEMOCLAW_IMAGE"
313+
if ! "${docker_cmd[@]}" build \
314+
--pull \
315+
--tag "$NEMOCLAW_IMAGE" \
316+
--file "$dockerfile_path" \
317+
"$image_context"; then
318+
log "Local NeMoClaw image build failed."
319+
exit 1
320+
fi
321+
322+
log "Local NeMoClaw image ready: $NEMOCLAW_IMAGE"
323+
}
324+
325+
resolve_docker_cmd() {
326+
if command -v docker >/dev/null 2>&1; then
327+
printf 'docker'
328+
return 0
329+
fi
330+
if command -v sudo >/dev/null 2>&1; then
331+
printf 'sudo docker'
332+
return 0
333+
fi
334+
return 1
335+
}
336+
337+
resolve_cluster_container_name() {
338+
local docker_bin
339+
340+
if [[ -n "$CLUSTER_CONTAINER_NAME" ]]; then
341+
printf '%s' "$CLUSTER_CONTAINER_NAME"
342+
return 0
343+
fi
344+
345+
docker_bin="$(resolve_docker_cmd)" || return 1
346+
347+
CLUSTER_CONTAINER_NAME="$($docker_bin ps --format '{{.Names}}\t{{.Image}}' | awk '$1 ~ /^openshell-cluster-/ { print $1; exit }')"
348+
if [[ -z "$CLUSTER_CONTAINER_NAME" ]]; then
349+
CLUSTER_CONTAINER_NAME="$($docker_bin ps --format '{{.Names}}\t{{.Image}}' | awk '$2 ~ /ghcr.io\\/nvidia\\/openshell\\/cluster/ { print $1; exit }')"
350+
fi
351+
352+
[[ -n "$CLUSTER_CONTAINER_NAME" ]]
353+
}
354+
355+
import_nemoclaw_image_into_cluster_if_needed() {
356+
local docker_bin cluster_name
357+
358+
if ! should_build_nemoclaw_image && [[ "$NEMOCLAW_IMAGE_EXPLICIT" != "1" ]]; then
359+
log "Skipping cluster image import; using registry-backed image: $NEMOCLAW_IMAGE"
360+
return
361+
fi
362+
363+
docker_bin="$(resolve_docker_cmd)" || {
364+
log "Docker not available; skipping cluster image import."
365+
return
366+
}
367+
368+
if ! $docker_bin image inspect "$NEMOCLAW_IMAGE" >/dev/null 2>&1; then
369+
log "Local NeMoClaw image not present on host; skipping cluster image import: $NEMOCLAW_IMAGE"
370+
return
371+
fi
372+
373+
if ! cluster_name="$(resolve_cluster_container_name)"; then
374+
log "OpenShell cluster container not found; skipping cluster image import."
375+
return
376+
fi
377+
378+
log "Importing NeMoClaw image into cluster containerd: $NEMOCLAW_IMAGE -> $cluster_name"
379+
if ! $docker_bin save "$NEMOCLAW_IMAGE" | $docker_bin exec -i "$cluster_name" sh -lc 'ctr -n k8s.io images import -'; then
380+
log "Failed to import NeMoClaw image into cluster containerd."
381+
exit 1
382+
fi
383+
384+
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
385+
log "Imported image tag not found in cluster containerd: $NEMOCLAW_IMAGE"
386+
log "Cluster image list:"
387+
$docker_bin exec -i "$cluster_name" sh -lc "ctr -n k8s.io images ls | grep 'sandboxes/nemoclaw' || true"
388+
exit 1
389+
fi
390+
391+
log "Cluster image import complete: $NEMOCLAW_IMAGE"
392+
}
393+
255394
checkout_repo_ref() {
256395
if [[ -z "$COMMUNITY_REF" ]]; then
257396
return
@@ -518,7 +657,12 @@ start_welcome_ui() {
518657
log "Starting welcome UI in background..."
519658
log "Welcome UI log: $WELCOME_UI_LOG"
520659

521-
nohup env PORT="$PORT" REPO_ROOT="$REPO_ROOT" CLI_BIN="$CLI_BIN" node server.js >> "$WELCOME_UI_LOG" 2>&1 &
660+
nohup env \
661+
PORT="$PORT" \
662+
REPO_ROOT="$REPO_ROOT" \
663+
CLI_BIN="$CLI_BIN" \
664+
NEMOCLAW_IMAGE="$NEMOCLAW_IMAGE" \
665+
node server.js >> "$WELCOME_UI_LOG" 2>&1 &
522666
WELCOME_UI_PID=$!
523667
export WELCOME_UI_PID
524668
log "Welcome UI PID: $WELCOME_UI_PID"
@@ -542,8 +686,11 @@ main() {
542686
step "Resolving CLI"
543687
resolve_cli
544688
ensure_cli_compat_aliases
689+
maybe_use_branch_local_nemoclaw_tag
545690
step "Authenticating registries"
546691
docker_login_ghcr_if_needed
692+
step "Preparing NeMoClaw image"
693+
build_nemoclaw_image_if_needed
547694
step "Ensuring Node.js"
548695
ensure_node
549696

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

556703
step "Starting gateway"
557704
start_gateway
705+
step "Importing NeMoClaw image into cluster"
706+
import_nemoclaw_image_into_cluster_if_needed
558707

559708
step "Configuring providers"
560709
run_provider_create_or_replace \
Lines changed: 20 additions & 0 deletions
Loading

brev/welcome-ui/OpenShell-Icon.svg

Lines changed: 1 addition & 0 deletions
Loading

brev/welcome-ui/favicon.ico

0 Bytes
Binary file not shown.

brev/welcome-ui/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>OpenShell — Agent Sandbox</title>
7-
<link rel="icon" type="image/svg+xml" href="/openshell-mark.svg">
7+
<link rel="icon" type="image/svg+xml" href="/OpenShell-Icon.svg">
88
<link rel="alternate icon" href="/favicon.ico" sizes="any">
99
<link rel="stylesheet" href="styles.css?v=7">
1010
<link rel="preconnect" href="https://fonts.googleapis.com">
@@ -16,7 +16,7 @@
1616
<!-- Top bar -->
1717
<header class="topbar">
1818
<div class="topbar__brand">
19-
<img class="topbar__logo" src="openshell-mark.svg" alt="OpenShell">
19+
<img class="topbar__logo" src="OpenShell-Icon-Logo.svg" alt="OpenShell">
2020
<span class="topbar__divider"></span>
2121
<span class="topbar__title">OpenShell</span>
2222
<span class="topbar__badge">Sandbox</span>

brev/welcome-ui/openshell-mark.svg

Lines changed: 0 additions & 5 deletions
This file was deleted.

brev/welcome-ui/server.js

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const SANDBOX_START_CMD = process.env.SANDBOX_START_CMD || "nemoclaw-start";
3838
const SANDBOX_BASE_IMAGE =
3939
process.env.SANDBOX_BASE_IMAGE ||
4040
"ghcr.io/nvidia/openshell-community/sandboxes/openclaw:latest";
41+
const NEMOCLAW_IMAGE = (process.env.NEMOCLAW_IMAGE || "").trim();
4142
const POLICY_FILE = path.join(SANDBOX_DIR, "policy.yaml");
4243

4344
const LOG_FILE = "/tmp/nemoclaw-sandbox-create.log";
@@ -264,6 +265,12 @@ const injectKeyState = {
264265
keyHash: null,
265266
};
266267

268+
// Raw API key stored in memory so it can be passed to the sandbox at
269+
// creation time. Not persisted to disk.
270+
let _nvidiaApiKey = process.env.NVIDIA_INFERENCE_API_KEY
271+
|| process.env.NVIDIA_INTEGRATE_API_KEY
272+
|| "";
273+
267274
// ── Brev ID detection & URL building ───────────────────────────────────────
268275

269276
function extractBrevId(host) {
@@ -286,7 +293,7 @@ function buildOpenclawUrl(token) {
286293
} else {
287294
url = `http://127.0.0.1:${PORT}/`;
288295
}
289-
if (token) url += `?token=${token}`;
296+
if (token) url += `#token=${token}`;
290297
return url;
291298
}
292299

@@ -627,18 +634,44 @@ function runSandboxCreate() {
627634
const cmd = [
628635
CLI_BIN, "sandbox", "create",
629636
"--name", SANDBOX_NAME,
630-
"--from", SANDBOX_DIR,
637+
"--from", NEMOCLAW_IMAGE || SANDBOX_DIR,
631638
"--forward", "18789",
632639
];
633640
if (policyPath) cmd.push("--policy", policyPath);
634-
cmd.push(
635-
"--",
636-
"env",
637-
`CHAT_UI_URL=${chatUiUrl}`,
638-
SANDBOX_START_CMD
639-
);
641+
const envArgs = [`CHAT_UI_URL=${chatUiUrl}`];
642+
const loopbackNoProxy = [
643+
"127.0.0.1",
644+
"localhost",
645+
"::1",
646+
"navigator.navigator.svc.cluster.local",
647+
".svc",
648+
".svc.cluster.local",
649+
"10.42.0.0/16",
650+
"10.43.0.0/16",
651+
].join(",");
652+
const mergedNoProxy = [
653+
process.env.NO_PROXY || process.env.no_proxy || "",
654+
loopbackNoProxy,
655+
]
656+
.filter(Boolean)
657+
.join(",");
658+
envArgs.push(`NO_PROXY=${mergedNoProxy}`);
659+
envArgs.push(`no_proxy=${mergedNoProxy}`);
660+
const nvapiKey = _nvidiaApiKey
661+
|| process.env.NVIDIA_INFERENCE_API_KEY
662+
|| process.env.NVIDIA_INTEGRATE_API_KEY
663+
|| "";
664+
if (nvapiKey) {
665+
envArgs.push(`NVIDIA_INFERENCE_API_KEY=${nvapiKey}`);
666+
envArgs.push(`NVIDIA_INTEGRATE_API_KEY=${nvapiKey}`);
667+
}
668+
669+
cmd.push("--", "env", ...envArgs, SANDBOX_START_CMD);
640670

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

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

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

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

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

12751316
runInjectKey(key, keyH);
1317+
12761318
return jsonResponse(res, 202, { ok: true, started: true });
12771319
}
12781320

@@ -1561,6 +1603,7 @@ function _resetForTesting() {
15611603
detectedBrevId = "";
15621604
_brevEnvId = "";
15631605
renderedIndex = null;
1606+
_nvidiaApiKey = "";
15641607
}
15651608

15661609
function _setMocksForTesting(mocks) {

0 commit comments

Comments
 (0)