Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
105 changes: 100 additions & 5 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const {
ensureApiKey,
ensureGithubToken,
getCredential,
saveCredential,
isRepoPrivate,
} = require("./lib/credentials");
const registry = require("./lib/registry");
Expand All @@ -54,6 +55,7 @@ const GLOBAL_COMMANDS = new Set([
"setup",
"setup-spark",
"start",
"telegram",
"stop",
"status",
"debug",
Expand Down Expand Up @@ -719,6 +721,8 @@ async function deploy(instanceName) {
if (ghToken) envLines.push(`GITHUB_TOKEN=${shellQuote(ghToken)}`);
const tgToken = getCredential("TELEGRAM_BOT_TOKEN");
if (tgToken) envLines.push(`TELEGRAM_BOT_TOKEN=${shellQuote(tgToken)}`);
const allowedChatIds = getCredential("ALLOWED_CHAT_IDS");
if (allowedChatIds) envLines.push(`ALLOWED_CHAT_IDS=${shellQuote(allowedChatIds)}`);
const discordToken = getCredential("DISCORD_BOT_TOKEN");
if (discordToken) envLines.push(`DISCORD_BOT_TOKEN=${shellQuote(discordToken)}`);
const slackToken = getCredential("SLACK_BOT_TOKEN");
Expand Down Expand Up @@ -766,18 +770,105 @@ async function deploy(instanceName) {
);
}

async function start() {
function normalizeTelegramChatIds(rawValue) {
const chatIds = String(rawValue || "")
.split(/[,\s]+/)
.map((value) => value.trim())
.filter(Boolean);
if (chatIds.length === 0) {
throw new Error("At least one Telegram chat ID is required.");
}
for (const chatId of chatIds) {
if (!/^-?\d+$/.test(chatId)) {
throw new Error(`Invalid Telegram chat ID: ${chatId}`);
}
}
return [...new Set(chatIds)].join(",");
}

async function start(args = []) {
const supportedFlags = new Set(["--discover-chat-id"]);
const unknown = args.filter((arg) => !supportedFlags.has(arg));
if (unknown.length > 0) {
console.error(` Unknown start option(s): ${unknown.join(", ")}`);
process.exit(1);
}

const discoveryMode = args.includes("--discover-chat-id");
const { defaultSandbox } = registry.listSandboxes();
const safeName =
defaultSandbox && /^[a-zA-Z0-9._-]+$/.test(defaultSandbox) ? defaultSandbox : null;
const sandboxEnv = safeName ? `SANDBOX_NAME=${shellQuote(safeName)}` : "";
run(`${sandboxEnv} bash "${SCRIPTS}/start-services.sh"`);
const envAssignments = [];
if (safeName) envAssignments.push(`SANDBOX_NAME=${shellQuote(safeName)}`);
const allowedChatIds = getCredential("ALLOWED_CHAT_IDS");
if (allowedChatIds) envAssignments.push(`ALLOWED_CHAT_IDS=${shellQuote(allowedChatIds)}`);
if (discoveryMode) envAssignments.push("NEMOCLAW_TELEGRAM_DISCOVERY=1");
const envPrefix = envAssignments.length > 0 ? `${envAssignments.join(" ")} ` : "";
run(`${envPrefix}bash "${SCRIPTS}/start-services.sh"`);
}

function stop() {
run(`bash "${SCRIPTS}/start-services.sh" --stop`);
}

function telegramHelp() {
console.log(`
${G}Telegram:${R}
nemoclaw telegram allow <chat-id[,chat-id...]> Save allowed Telegram chat IDs
nemoclaw telegram show Show saved Telegram chat IDs
nemoclaw telegram clear Remove the saved Telegram allowlist
nemoclaw telegram discover Start services in discovery-only mode

${D}Tip:${R} use ${B}nemoclaw start --discover-chat-id${R}${D} to reply with your chat ID
without forwarding messages to the agent.${R}
`);
}

async function telegramCommand(args = []) {
const [action, ...rest] = args;
switch (action) {
case undefined:
case "help":
case "--help":
case "-h":
telegramHelp();
return;
case "allow": {
let allowlist;
try {
allowlist = normalizeTelegramChatIds(rest.join(","));
} catch (err) {
console.error(` ${err.message}`);
process.exit(1);
}
saveCredential("ALLOWED_CHAT_IDS", allowlist);
console.log(` Saved Telegram allowlist: ${allowlist}`);
console.log(" Stored in ~/.nemoclaw/credentials.json (mode 600)");
return;
}
case "show": {
const allowlist = getCredential("ALLOWED_CHAT_IDS");
if (!allowlist) {
console.log(" No Telegram allowlist configured.");
return;
}
console.log(` Telegram allowlist: ${allowlist}`);
return;
}
case "clear":
saveCredential("ALLOWED_CHAT_IDS", "");
console.log(" Cleared Telegram allowlist.");
return;
case "discover":
await start(["--discover-chat-id"]);
return;
default:
console.error(` Unknown telegram action: ${action}`);
console.error(" Valid actions: allow, show, clear, discover");
process.exit(1);
}
}

function debug(args) {
const result = spawnSync("bash", [path.join(SCRIPTS, "debug.sh"), ...args], {
stdio: "inherit",
Expand Down Expand Up @@ -1147,9 +1238,10 @@ function help() {
nemoclaw deploy <instance> Deploy to a Brev VM and start services

${G}Services:${R}
nemoclaw start Start auxiliary services ${D}(Telegram, tunnel)${R}
nemoclaw start ${D}[--discover-chat-id]${R} Start auxiliary services ${D}(Telegram, tunnel)${R}
nemoclaw stop Stop all services
nemoclaw status Show sandbox list and service status
nemoclaw telegram allow <id> Save allowed Telegram chat IDs

Troubleshooting:
nemoclaw debug [--quick] Collect diagnostics for bug reports
Expand Down Expand Up @@ -1197,7 +1289,10 @@ const [cmd, ...args] = process.argv.slice(2);
await deploy(args[0]);
break;
case "start":
await start();
await start(args);
break;
case "telegram":
await telegramCommand(args);
break;
case "stop":
stop();
Expand Down
2 changes: 2 additions & 0 deletions docs/deployment/deploy-to-remote-gpu.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ The deploy script performs the following steps on the VM:
3. Runs `nemoclaw onboard` (the setup wizard) to create the gateway, register providers, and launch the sandbox.
4. Starts auxiliary services, such as the Telegram bridge and cloudflared tunnel.

If you configured a Telegram bot token but not an allowlist yet, the bridge stays disabled until you either save `ALLOWED_CHAT_IDS` with `nemoclaw telegram allow <chat-id>` or run discovery mode with `nemoclaw start --discover-chat-id`.

## Connect to the Remote Sandbox

After deployment finishes, the deploy command opens an interactive shell inside the remote sandbox.
Expand Down
27 changes: 23 additions & 4 deletions docs/deployment/set-up-telegram-bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,19 @@ The `start` command launches the following services:
- The Telegram bridge forwards messages between Telegram and the agent.
- The cloudflared tunnel provides external access to the sandbox.

The Telegram bridge starts only when the `TELEGRAM_BOT_TOKEN` environment variable is set.
The Telegram bridge starts only when the following are configured:

- `TELEGRAM_BOT_TOKEN`
- `NVIDIA_API_KEY`
- `ALLOWED_CHAT_IDS`

If you do not know your Telegram chat ID yet, start the bridge in discovery-only mode:

```console
$ nemoclaw start --discover-chat-id
```

Then send any message to the bot. The bridge replies with your chat ID and does not forward the message to the agent.

## Verify the Services

Expand All @@ -71,15 +83,22 @@ The output shows the status of all auxiliary services.
Open Telegram, find your bot, and send a message.
The bridge forwards the message to the OpenClaw agent inside the sandbox and returns the agent response.

## Restrict Access by Chat ID
## Allow Telegram Chats by Chat ID

To restrict which Telegram chats can interact with the agent, set the `ALLOWED_CHAT_IDS` environment variable to a comma-separated list of Telegram chat IDs:
Save the Telegram chat IDs allowed to interact with the agent:

```console
$ export ALLOWED_CHAT_IDS="123456789,987654321"
$ nemoclaw telegram allow 123456789,987654321
$ nemoclaw start
```

To inspect or clear the saved allowlist:

```console
$ nemoclaw telegram show
$ nemoclaw telegram clear
```

## Stop the Services

To stop the Telegram bridge and all other auxiliary services:
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ The following environment variables configure optional services and local access
| Variable | Purpose |
|---|---|
| `TELEGRAM_BOT_TOKEN` | Bot token for the Telegram bridge. |
| `ALLOWED_CHAT_IDS` | Comma-separated list of Telegram chat IDs allowed to message the agent. |
| `ALLOWED_CHAT_IDS` | Comma-separated list of Telegram chat IDs allowed to message the agent. Required for normal Telegram bridge forwarding. |
| `CHAT_UI_URL` | URL for the optional chat UI endpoint. |

For normal setup and reconfiguration, prefer `nemoclaw onboard` over editing these files by hand.
19 changes: 18 additions & 1 deletion docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,24 @@ Start auxiliary services, such as the Telegram bridge and cloudflared tunnel.
$ nemoclaw start
```

Requires `TELEGRAM_BOT_TOKEN` for the Telegram bridge.
Use discovery-only mode to have the bot reply with your Telegram chat ID without forwarding messages to the agent:

```console
$ nemoclaw start --discover-chat-id
```

The Telegram bridge requires `TELEGRAM_BOT_TOKEN`, `NVIDIA_API_KEY`, and an `ALLOWED_CHAT_IDS` allowlist for normal operation.

### `nemoclaw telegram`

Manage the Telegram bridge allowlist.

```console
$ nemoclaw telegram allow 123456789
$ nemoclaw telegram show
$ nemoclaw telegram clear
$ nemoclaw telegram discover
```

### `nemoclaw stop`

Expand Down
37 changes: 34 additions & 3 deletions scripts/start-services.sh
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,24 @@ do_stop() {
}

do_start() {
local telegram_status="not started (no token)"

if [ -z "${TELEGRAM_BOT_TOKEN:-}" ]; then
warn "TELEGRAM_BOT_TOKEN not set — Telegram bridge will not start."
warn "Create a bot via @BotFather on Telegram and set the token."
telegram_status="not started (no token)"
elif [ -z "${NVIDIA_API_KEY:-}" ]; then
warn "NVIDIA_API_KEY not set — Telegram bridge will not start."
warn "Set NVIDIA_API_KEY if you want Telegram requests to reach inference."
telegram_status="not started (missing NVIDIA_API_KEY)"
elif [ -z "${ALLOWED_CHAT_IDS:-}" ] && [ "${NEMOCLAW_TELEGRAM_DISCOVERY:-0}" != "1" ]; then
warn "ALLOWED_CHAT_IDS not set — Telegram bridge will not start."
warn "Run 'nemoclaw start --discover-chat-id' to return your Telegram chat ID safely."
warn "Then run 'nemoclaw telegram allow <chat-id>' and start services again."
telegram_status="not started (allowlist required)"
elif [ "${NEMOCLAW_TELEGRAM_DISCOVERY:-0}" = "1" ] && [ -z "${ALLOWED_CHAT_IDS:-}" ]; then
info "Telegram discovery mode enabled — messages will return their chat ID only."
telegram_status="discovery only"
fi

command -v node >/dev/null || fail "node not found. Install Node.js first."
Expand All @@ -152,9 +164,14 @@ do_start() {
mkdir -p "$PIDDIR"

# Telegram bridge (only if token provided)
if [ -n "${TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${NVIDIA_API_KEY:-}" ]; then
if [ -n "${TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${NVIDIA_API_KEY:-}" ] && { [ -n "${ALLOWED_CHAT_IDS:-}" ] || [ "${NEMOCLAW_TELEGRAM_DISCOVERY:-0}" = "1" ]; }; then
SANDBOX_NAME="$SANDBOX_NAME" start_service telegram-bridge \
node "$REPO_DIR/scripts/telegram-bridge.js"
if [ "${NEMOCLAW_TELEGRAM_DISCOVERY:-0}" = "1" ] && [ -z "${ALLOWED_CHAT_IDS:-}" ]; then
telegram_status="discovery only"
else
telegram_status="bridge running"
fi
fi

# 3. cloudflared tunnel
Expand Down Expand Up @@ -194,9 +211,23 @@ do_start() {
fi

if is_running telegram-bridge; then
echo " │ Telegram: bridge running │"
if [ "${NEMOCLAW_TELEGRAM_DISCOVERY:-0}" = "1" ] && [ -z "${ALLOWED_CHAT_IDS:-}" ]; then
echo " │ Telegram: discovery only │"
else
echo " │ Telegram: bridge running │"
fi
else
echo " │ Telegram: not started (no token) │"
case "$telegram_status" in
"not started (missing NVIDIA_API_KEY)")
echo " │ Telegram: not started (missing API key) │"
;;
"not started (allowlist required)")
echo " │ Telegram: not started (allowlist required) │"
;;
*)
echo " │ Telegram: not started (no token) │"
;;
esac
fi

echo " │ │"
Expand Down
Loading