From 8464342af5c257c9af8d5b5babf2e3035c1a333b Mon Sep 17 00:00:00 2001 From: peteryuqin Date: Mon, 16 Mar 2026 21:52:32 -0400 Subject: [PATCH 1/3] fix: resolve sandbox name from registry in Telegram bridge The Telegram bridge hardcoded the sandbox name default to "nemoclaw" while the onboard wizard defaults to "my-assistant", causing "sandbox not found" errors when the bridge tries to connect. Now resolves the sandbox name via: 1. SANDBOX_NAME env var (explicit override) 2. Default sandbox from the NemoClaw registry 3. Fallback to "my-assistant" (matches onboard default) Also quotes the sandbox name in the ssh-config shell command. Fixes #93 Signed-off-by: peteryuqin --- scripts/telegram-bridge.js | 23 +++++++++++++--- test/telegram-bridge-sandbox-name.test.js | 33 +++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 test/telegram-bridge-sandbox-name.test.js diff --git a/scripts/telegram-bridge.js b/scripts/telegram-bridge.js index c51a5529a4..76613a5290 100755 --- a/scripts/telegram-bridge.js +++ b/scripts/telegram-bridge.js @@ -12,7 +12,7 @@ * Env: * TELEGRAM_BOT_TOKEN — from @BotFather * NVIDIA_API_KEY — for inference - * SANDBOX_NAME — sandbox name (default: nemoclaw) + * SANDBOX_NAME — sandbox name (default: from registry, or "my-assistant") * ALLOWED_CHAT_IDS — comma-separated Telegram chat IDs to accept (optional, accepts all if unset) */ @@ -29,8 +29,6 @@ if (!OPENSHELL) { const TOKEN = process.env.TELEGRAM_BOT_TOKEN; const API_KEY = process.env.NVIDIA_API_KEY; -const SANDBOX = process.env.SANDBOX_NAME || "nemoclaw"; -try { validateName(SANDBOX, "SANDBOX_NAME"); } catch (e) { console.error(e.message); process.exit(1); } const ALLOWED_CHATS = process.env.ALLOWED_CHAT_IDS ? process.env.ALLOWED_CHAT_IDS.split(",").map((s) => s.trim()) : null; @@ -38,6 +36,25 @@ const ALLOWED_CHATS = process.env.ALLOWED_CHAT_IDS if (!TOKEN) { console.error("TELEGRAM_BOT_TOKEN required"); process.exit(1); } if (!API_KEY) { console.error("NVIDIA_API_KEY required"); process.exit(1); } +/** + * Resolve the sandbox name. Priority: + * 1. SANDBOX_NAME env var (explicit override) + * 2. The default sandbox from the NemoClaw registry + * 3. Fallback to "my-assistant" (matches onboard default) + */ +function resolveSandboxName() { + if (process.env.SANDBOX_NAME) return process.env.SANDBOX_NAME; + try { + const registry = require("../bin/lib/registry"); + const def = registry.getDefault(); + if (def) return def; + } catch {} + return "my-assistant"; +} + +const SANDBOX = resolveSandboxName(); +try { validateName(SANDBOX, "SANDBOX_NAME"); } catch (e) { console.error(e.message); process.exit(1); } + let offset = 0; const activeSessions = new Map(); // chatId → message history diff --git a/test/telegram-bridge-sandbox-name.test.js b/test/telegram-bridge-sandbox-name.test.js new file mode 100644 index 0000000000..b0eae7db39 --- /dev/null +++ b/test/telegram-bridge-sandbox-name.test.js @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); + +const ROOT = path.resolve(__dirname, ".."); +const SCRIPT = fs.readFileSync(path.join(ROOT, "scripts", "telegram-bridge.js"), "utf-8"); + +describe("telegram bridge sandbox resolution", () => { + it("does not hardcode nemoclaw as the default sandbox", () => { + assert.ok( + !SCRIPT.includes('process.env.SANDBOX_NAME || "nemoclaw"'), + 'telegram bridge must not default SANDBOX_NAME to "nemoclaw"' + ); + }); + + it("reads the default sandbox from the registry", () => { + assert.ok( + SCRIPT.includes("registry.getDefault()"), + "telegram bridge must read the default sandbox from the registry" + ); + }); + + it("falls back to my-assistant when no explicit or registered sandbox exists", () => { + assert.ok( + SCRIPT.includes('return "my-assistant";'), + 'telegram bridge must fall back to "my-assistant"' + ); + }); +}); From bc8b1939f0f883b28c51750e836f1463cc5b1177 Mon Sep 17 00:00:00 2001 From: peteryuqin Date: Thu, 19 Mar 2026 09:10:02 -0400 Subject: [PATCH 2/3] test(telegram): exercise sandbox resolution behavior --- test/telegram-bridge-sandbox-name.test.js | 49 ++++++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/test/telegram-bridge-sandbox-name.test.js b/test/telegram-bridge-sandbox-name.test.js index b0eae7db39..738f53383c 100644 --- a/test/telegram-bridge-sandbox-name.test.js +++ b/test/telegram-bridge-sandbox-name.test.js @@ -8,26 +8,53 @@ const path = require("node:path"); const ROOT = path.resolve(__dirname, ".."); const SCRIPT = fs.readFileSync(path.join(ROOT, "scripts", "telegram-bridge.js"), "utf-8"); +const RESOLVE_SANDBOX_NAME_SOURCE = SCRIPT.match(/function resolveSandboxName\(\) \{[\s\S]*?\n\}/)?.[0]; + +function resolveSandboxNameWith({ env = {}, registryDefault, registryThrows = false } = {}) { + assert.ok(RESOLVE_SANDBOX_NAME_SOURCE, "expected telegram bridge to define resolveSandboxName()"); + + const processStub = { env: { ...env } }; + const requireStub = (specifier) => { + if (specifier !== "../bin/lib/registry") { + throw new Error(`unexpected require: ${specifier}`); + } + if (registryThrows) { + throw new Error("registry unavailable"); + } + return { getDefault: () => registryDefault }; + }; + + const resolveSandboxName = new Function( + "process", + "require", + `${RESOLVE_SANDBOX_NAME_SOURCE}\nreturn resolveSandboxName;`, + )(processStub, requireStub); + + return resolveSandboxName(); +} describe("telegram bridge sandbox resolution", () => { - it("does not hardcode nemoclaw as the default sandbox", () => { - assert.ok( - !SCRIPT.includes('process.env.SANDBOX_NAME || "nemoclaw"'), - 'telegram bridge must not default SANDBOX_NAME to "nemoclaw"' + it("prefers SANDBOX_NAME when explicitly set", () => { + assert.equal( + resolveSandboxNameWith({ + env: { SANDBOX_NAME: "from-env" }, + registryDefault: "from-registry", + }), + "from-env", ); }); - it("reads the default sandbox from the registry", () => { - assert.ok( - SCRIPT.includes("registry.getDefault()"), - "telegram bridge must read the default sandbox from the registry" + it("reads the default sandbox from the registry when env is unset", () => { + assert.equal( + resolveSandboxNameWith({ registryDefault: "from-registry" }), + "from-registry", ); }); it("falls back to my-assistant when no explicit or registered sandbox exists", () => { - assert.ok( - SCRIPT.includes('return "my-assistant";'), - 'telegram bridge must fall back to "my-assistant"' + assert.equal( + resolveSandboxNameWith({ registryThrows: true }), + "my-assistant", ); }); }); From c67c71ff6abdd748fdf0c16583481bf375bf2e22 Mon Sep 17 00:00:00 2001 From: peteryuqin Date: Mon, 23 Mar 2026 12:28:13 -0400 Subject: [PATCH 3/3] test(telegram): stabilize function extraction --- test/telegram-bridge-sandbox-name.test.js | 27 ++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/test/telegram-bridge-sandbox-name.test.js b/test/telegram-bridge-sandbox-name.test.js index 738f53383c..860f8644b2 100644 --- a/test/telegram-bridge-sandbox-name.test.js +++ b/test/telegram-bridge-sandbox-name.test.js @@ -8,7 +8,32 @@ const path = require("node:path"); const ROOT = path.resolve(__dirname, ".."); const SCRIPT = fs.readFileSync(path.join(ROOT, "scripts", "telegram-bridge.js"), "utf-8"); -const RESOLVE_SANDBOX_NAME_SOURCE = SCRIPT.match(/function resolveSandboxName\(\) \{[\s\S]*?\n\}/)?.[0]; + +function extractFunctionSource(source, name) { + const signature = `function ${name}()`; + const start = source.indexOf(signature); + assert.notEqual(start, -1, `expected source to include ${signature}`); + + const bodyStart = source.indexOf("{", start); + assert.notEqual(bodyStart, -1, `expected ${name} to have a function body`); + + let depth = 0; + for (let i = bodyStart; i < source.length; i += 1) { + const char = source[i]; + if (char === "{") { + depth += 1; + } else if (char === "}") { + depth -= 1; + if (depth === 0) { + return source.slice(start, i + 1); + } + } + } + + assert.fail(`expected ${name}() to have balanced braces`); +} + +const RESOLVE_SANDBOX_NAME_SOURCE = extractFunctionSource(SCRIPT, "resolveSandboxName"); function resolveSandboxNameWith({ env = {}, registryDefault, registryThrows = false } = {}) { assert.ok(RESOLVE_SANDBOX_NAME_SOURCE, "expected telegram bridge to define resolveSandboxName()");