Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
43 changes: 43 additions & 0 deletions .agents/skills/nemoclaw-user-reference/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,49 @@ $ nemoclaw my-assistant policy-remove

Unchecking a preset in the onboard TUI checkbox also removes it from the sandbox.

### `nemoclaw <name> channels list`

List the messaging channels NemoClaw knows about (`telegram`, `discord`, `slack`) with a short description.
The command is a static reference; it does not consult credentials or the running sandbox.

```console
$ nemoclaw my-assistant channels list
```

### `nemoclaw <name> channels add <channel>`

Store credentials for a messaging channel (`telegram`, `discord`, or `slack`) and rebuild the sandbox so the image picks up the new channel.
The command prompts for any missing token, persists it under `~/.nemoclaw/credentials.json`, then asks whether to rebuild immediately.
Running `add` for an already-configured channel simply overwrites the stored tokens — the operation is idempotent.

```console
$ nemoclaw my-assistant channels add telegram
```

| Flag | Description |
|------|-------------|
| `--dry-run` | Validate the channel and token inputs without saving credentials or rebuilding |

Slack requires both `SLACK_BOT_TOKEN` (bot user OAuth) and `SLACK_APP_TOKEN` (app-level Socket Mode token); the command prompts for each in turn.
When `NEMOCLAW_NON_INTERACTIVE=1` is set, any missing token fails fast and no rebuild prompt is shown — instead, the change is queued and you are told to run `nemoclaw <name> rebuild` manually.

### `nemoclaw <name> channels remove <channel>`

Clear the stored credentials for a messaging channel and rebuild the sandbox so the image drops the channel.
Running `remove` for a channel that was never configured is a no-op against the credentials file and still triggers the rebuild prompt.

```console
$ nemoclaw my-assistant channels remove telegram
```

| Flag | Description |
|------|-------------|
| `--dry-run` | Report the channel that would be removed without clearing credentials or rebuilding |

As with `channels add`, `NEMOCLAW_NON_INTERACTIVE=1` skips the rebuild prompt and queues the change for a manual `nemoclaw <name> rebuild`.

Host-side removal is the supported path because `/sandbox/.openclaw/openclaw.json` is read-only at runtime; `openclaw channels remove` cannot modify the baked config from inside the sandbox.

### `nemoclaw <name> skill install <path>`

Deploy a skill directory to a running sandbox.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,24 @@ Changing or exporting it later does not rewrite the baked `openclaw.json` inside
If you need a different device-auth setting, rerun onboarding so NemoClaw rebuilds the sandbox image with the desired configuration.
For the security trade-offs, refer to Security Best Practices (see the `nemoclaw-user-configure-security` skill).

### `openclaw channels add` or `remove` is blocked inside the sandbox

This is expected.
The messaging channel list is frozen into the sandbox's container image when the image is built during `nemoclaw onboard` or `nemoclaw rebuild` (the selected channel names are passed to the `docker build` as `NEMOCLAW_MESSAGING_CHANNELS_B64` and written into `/sandbox/.openclaw/openclaw.json` as part of the image).
At runtime the sandbox mounts that path read-only and layers Landlock + filesystem hardening on top, so `openclaw channels` commands that mutate the config cannot write there.
NemoClaw's sandbox entrypoint installs a guard that intercepts `openclaw channels <add|remove>` and prints an actionable error pointing at the host-side commands below, instead of letting the call fail deep in the binary with a raw `EACCES` trace.

Run the equivalent host-side command instead:

```console
$ nemoclaw <sandbox> channels list
$ nemoclaw <sandbox> channels add <telegram|discord|slack>
$ nemoclaw <sandbox> channels remove <telegram|discord|slack>
```

`channels add` stores credentials under `~/.nemoclaw/credentials.json` and `channels remove` clears them; both offer to rebuild the sandbox so the image reflects the new channel set.
In non-interactive mode (`NEMOCLAW_NON_INTERACTIVE=1`), the commands stage the change and leave the rebuild to a follow-up `nemoclaw <sandbox> rebuild`.

### `openclaw doctor --fix` cannot repair Discord channel config inside the sandbox

This is expected in NemoClaw-managed sandboxes.
Expand Down
43 changes: 43 additions & 0 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,49 @@ $ nemoclaw my-assistant policy-remove

Unchecking a preset in the onboard TUI checkbox also removes it from the sandbox.

### `nemoclaw <name> channels list`

List the messaging channels NemoClaw knows about (`telegram`, `discord`, `slack`) with a short description.
The command is a static reference; it does not consult credentials or the running sandbox.

```console
$ nemoclaw my-assistant channels list
```

### `nemoclaw <name> channels add <channel>`

Store credentials for a messaging channel (`telegram`, `discord`, or `slack`) and rebuild the sandbox so the image picks up the new channel.
The command prompts for any missing token, persists it under `~/.nemoclaw/credentials.json`, then asks whether to rebuild immediately.
Running `add` for an already-configured channel simply overwrites the stored tokens — the operation is idempotent.

```console
$ nemoclaw my-assistant channels add telegram
```

| Flag | Description |
|------|-------------|
| `--dry-run` | Validate the channel and token inputs without saving credentials or rebuilding |

Slack requires both `SLACK_BOT_TOKEN` (bot user OAuth) and `SLACK_APP_TOKEN` (app-level Socket Mode token); the command prompts for each in turn.
When `NEMOCLAW_NON_INTERACTIVE=1` is set, any missing token fails fast and no rebuild prompt is shown — instead, the change is queued and you are told to run `nemoclaw <name> rebuild` manually.

### `nemoclaw <name> channels remove <channel>`

Clear the stored credentials for a messaging channel and rebuild the sandbox so the image drops the channel.
Running `remove` for a channel that was never configured is a no-op against the credentials file and still triggers the rebuild prompt.

```console
$ nemoclaw my-assistant channels remove telegram
```

| Flag | Description |
|------|-------------|
| `--dry-run` | Report the channel that would be removed without clearing credentials or rebuilding |

As with `channels add`, `NEMOCLAW_NON_INTERACTIVE=1` skips the rebuild prompt and queues the change for a manual `nemoclaw <name> rebuild`.

Host-side removal is the supported path because `/sandbox/.openclaw/openclaw.json` is read-only at runtime; `openclaw channels remove` cannot modify the baked config from inside the sandbox.

### `nemoclaw <name> skill install <path>`

Deploy a skill directory to a running sandbox.
Expand Down
18 changes: 18 additions & 0 deletions docs/reference/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,24 @@ Changing or exporting it later does not rewrite the baked `openclaw.json` inside
If you need a different device-auth setting, rerun onboarding so NemoClaw rebuilds the sandbox image with the desired configuration.
For the security trade-offs, refer to [Security Best Practices](../security/best-practices.md).

### `openclaw channels add` or `remove` is blocked inside the sandbox

This is expected.
The messaging channel list is frozen into the sandbox's container image when the image is built during `nemoclaw onboard` or `nemoclaw rebuild` (the selected channel names are passed to the `docker build` as `NEMOCLAW_MESSAGING_CHANNELS_B64` and written into `/sandbox/.openclaw/openclaw.json` as part of the image).
At runtime the sandbox mounts that path read-only and layers Landlock + filesystem hardening on top, so `openclaw channels` commands that mutate the config cannot write there.
NemoClaw's sandbox entrypoint installs a guard that intercepts `openclaw channels <add|remove>` and prints an actionable error pointing at the host-side commands below, instead of letting the call fail deep in the binary with a raw `EACCES` trace.

Run the equivalent host-side command instead:

```console
$ nemoclaw <sandbox> channels list
$ nemoclaw <sandbox> channels add <telegram|discord|slack>
$ nemoclaw <sandbox> channels remove <telegram|discord|slack>
```

`channels add` stores credentials under `~/.nemoclaw/credentials.json` and `channels remove` clears them; both offer to rebuild the sandbox so the image reflects the new channel set.
In non-interactive mode (`NEMOCLAW_NON_INTERACTIVE=1`), the commands stage the change and leave the rebuild to a follow-up `nemoclaw <sandbox> rebuild`.

### `openclaw doctor --fix` cannot repair Discord channel config inside the sandbox

This is expected in NemoClaw-managed sandboxes.
Expand Down
16 changes: 16 additions & 0 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,22 @@ openclaw() {
;;
esac
;;
channels)
case "$2" in
list | "" | -h | --help) ;;
*)
echo "Error: 'openclaw channels $2' cannot modify channels inside the sandbox." >&2
echo "The sandbox config is read-only (Landlock enforced) for security." >&2
echo "" >&2
echo "To add or remove messaging channels, exit the sandbox and run:" >&2
echo " nemoclaw <sandbox> channels add <telegram|discord|slack>" >&2
echo " nemoclaw <sandbox> channels remove <telegram|discord|slack>" >&2
echo "" >&2
echo "These stage the change and rebuild the sandbox to apply it." >&2
return 1
;;
esac
;;
agent)
# Block --local inside sandbox — it bypasses gateway protections and can
# crash the container's main process, bricking the sandbox. Ref: #1632, #2016
Expand Down
50 changes: 6 additions & 44 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ const sandboxCreateStream = require("./sandbox-create-stream");
const validationRecovery = require("./validation-recovery");
const webSearch = require("./web-search");

import { listChannels } from "./sandbox-channels";

/**
* Create a temp file inside a directory with a cryptographically random name.
* Uses fs.mkdtempSync (OS-level mkdtemp) to avoid predictable filenames that
Expand Down Expand Up @@ -4498,48 +4500,7 @@ async function setupInference(

// ── Step 6: Messaging channels ───────────────────────────────────

const MESSAGING_CHANNELS = [
{
name: "telegram",
envKey: "TELEGRAM_BOT_TOKEN",
description: "Telegram bot messaging",
help: "Create a bot via @BotFather on Telegram, then copy the token.",
label: "Telegram Bot Token",
userIdEnvKey: "TELEGRAM_ALLOWED_IDS",
userIdHelp: "Send /start to @userinfobot on Telegram to get your numeric user ID.",
userIdLabel: "Telegram User ID (for DM access)",
allowIdsMode: "dm",
},
{
name: "discord",
envKey: "DISCORD_BOT_TOKEN",
description: "Discord bot messaging",
help: "Discord Developer Portal → Applications → Bot → Reset/Copy Token.",
label: "Discord Bot Token",
serverIdEnvKey: "DISCORD_SERVER_ID",
serverIdHelp:
"Enable Developer Mode in Discord, then right-click your server and copy the Server ID.",
serverIdLabel: "Discord Server ID (for guild workspace access)",
requireMentionEnvKey: "DISCORD_REQUIRE_MENTION",
requireMentionHelp:
"Choose whether the bot should reply only when @mentioned or to all messages in this server.",
userIdEnvKey: "DISCORD_USER_ID",
userIdHelp:
"Optional: enable Developer Mode in Discord, then right-click your user/avatar and copy the User ID. Leave blank to allow any member of the configured server to message the bot.",
userIdLabel: "Discord User ID (optional guild allowlist)",
allowIdsMode: "guild",
},
{
name: "slack",
envKey: "SLACK_BOT_TOKEN",
description: "Slack bot messaging",
help: "Slack API → Your Apps → OAuth & Permissions → Bot User OAuth Token (xoxb-...).",
label: "Slack Bot Token",
appTokenEnvKey: "SLACK_APP_TOKEN",
appTokenHelp: "Slack API → Your Apps → Basic Information → App-Level Tokens (xapp-...).",
appTokenLabel: "Slack App Token (Socket Mode)",
},
];
const MESSAGING_CHANNELS = listChannels();

// Curl exit codes that indicate a network-level failure (not a token problem).
// 35 (TLS handshake failure) covers corporate proxies that MITM HTTPS.
Expand Down Expand Up @@ -4617,7 +4578,7 @@ async function setupMessagingChannels() {
}

// Single-keypress toggle selector — pre-select channels that already have tokens.
// Press 1/2/3 to instantly toggle a channel; press Enter to continue.
// Press a channel number to toggle; press Enter to continue.
const enabled = new Set(
MESSAGING_CHANNELS.filter((c) => getMessagingToken(c.envKey)).map((c) => c.name),
);
Expand All @@ -4640,7 +4601,7 @@ async function setupMessagingChannels() {
output.write(` [${i + 1}] ${marker} ${ch.name} — ${ch.description}${status}\n`);
});
output.write("\n");
output.write(" Press 1-3 to toggle, Enter when done: ");
output.write(` Press 1-${MESSAGING_CHANNELS.length} to toggle, Enter when done: `);
};

showList();
Expand Down Expand Up @@ -6419,6 +6380,7 @@ module.exports = {
setupMessagingChannels,
setupNim,
isInferenceRouteReady,
isNonInteractive,
isOpenclawReady,
arePolicyPresetsApplied,
getSuggestedPolicyPresets,
Expand Down
64 changes: 64 additions & 0 deletions src/lib/sandbox-channels.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";

import {
KNOWN_CHANNELS,
getChannelDef,
getChannelTokenKeys,
knownChannelNames,
listChannels,
} from "../../dist/lib/sandbox-channels";

describe("sandbox-channels KNOWN_CHANNELS", () => {
it("covers telegram, discord, and slack", () => {
expect(knownChannelNames()).toEqual(["telegram", "discord", "slack"]);
});

it("exposes the primary bot-token env var for each channel", () => {
expect(getChannelDef("telegram")?.envKey).toBe("TELEGRAM_BOT_TOKEN");
expect(getChannelDef("discord")?.envKey).toBe("DISCORD_BOT_TOKEN");
expect(getChannelDef("slack")?.envKey).toBe("SLACK_BOT_TOKEN");
});

it("only slack declares a secondary app-token env var", () => {
expect(getChannelDef("telegram")?.appTokenEnvKey).toBeUndefined();
expect(getChannelDef("discord")?.appTokenEnvKey).toBeUndefined();
expect(getChannelDef("slack")?.appTokenEnvKey).toBe("SLACK_APP_TOKEN");
});

it("normalises case and whitespace when resolving a channel name", () => {
expect(getChannelDef(" Telegram ")).toBe(KNOWN_CHANNELS.telegram);
expect(getChannelDef("DISCORD")).toBe(KNOWN_CHANNELS.discord);
});

it("returns undefined for unknown channel names", () => {
expect(getChannelDef("mattermost")).toBeUndefined();
expect(getChannelDef("")).toBeUndefined();
});
});

describe("sandbox-channels getChannelTokenKeys", () => {
it("returns just the primary token key for single-token channels", () => {
expect(getChannelTokenKeys(KNOWN_CHANNELS.telegram)).toEqual(["TELEGRAM_BOT_TOKEN"]);
expect(getChannelTokenKeys(KNOWN_CHANNELS.discord)).toEqual(["DISCORD_BOT_TOKEN"]);
});

it("returns primary then app token for slack", () => {
expect(getChannelTokenKeys(KNOWN_CHANNELS.slack)).toEqual([
"SLACK_BOT_TOKEN",
"SLACK_APP_TOKEN",
]);
});
});

describe("sandbox-channels listChannels", () => {
it("materialises an array with the name merged into each entry", () => {
const list = listChannels();
expect(list.map((c) => c.name)).toEqual(["telegram", "discord", "slack"]);
const telegram = list.find((c) => c.name === "telegram");
expect(telegram?.envKey).toBe("TELEGRAM_BOT_TOKEN");
expect(telegram?.allowIdsMode).toBe("dm");
});
});
Loading
Loading