Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
dda17f2
chore: unify credential passing pattern across all integrations
ericksoa Mar 22, 2026
67cc12b
feat: host-side bridge framework with Discord support
ericksoa Mar 22, 2026
80afba3
Merge branch 'feat/host-side-bridge-framework' into chore/unify-crede…
ericksoa Mar 22, 2026
04537f1
fix: add host-side Slack bridge using bridge-core
ericksoa Mar 22, 2026
ea3fb46
refactor: yaml-driven bridge architecture with adapter pattern
ericksoa Mar 22, 2026
bff2a60
fix: address CodeRabbit findings on bridge architecture
ericksoa Mar 22, 2026
b875742
fix: log metadata only in bridge message flow, never raw content
ericksoa Mar 22, 2026
223b650
fix: replace hand-rolled yaml parser with js-yaml
ericksoa Mar 22, 2026
c019a7c
refactor: move bridge configs into blueprint.yaml components
ericksoa Mar 22, 2026
2690fab
fix: add backwards-compatible telegram-bridge.js wrapper
ericksoa Mar 22, 2026
09bc811
fix: auto-start messaging bridges after onboard when tokens detected
ericksoa Mar 22, 2026
c729c14
test: add regression tests for bridge architecture and migration path
ericksoa Mar 22, 2026
44a0fe0
fix: catch execFileSync failure in bridge-core sandbox relay
ericksoa Mar 22, 2026
8143bfd
fix: address CodeRabbit round 4 findings
ericksoa Mar 22, 2026
1fa10d3
fix: reject tgApi promise on Telegram ok:false responses
ericksoa Mar 22, 2026
b970dcf
merge: resolve conflict with vitest migration on main
ericksoa Mar 22, 2026
29a4696
fix: preserve /start and /reset bot commands in telegram adapter
ericksoa Mar 22, 2026
7c97109
Merge upstream/main into chore/unify-credential-passing
cv Mar 23, 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
48 changes: 46 additions & 2 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -555,9 +555,15 @@ async function createSandbox(gpu) {

console.log(` Creating sandbox '${sandboxName}' (this takes a few minutes on first run)...`);
const chatUiUrl = process.env.CHAT_UI_URL || 'http://127.0.0.1:18789';
// Pass user-provided secrets into the sandbox as environment variables.
// All tokens follow the same pattern: getCredential() checks env first,
// then ~/.nemoclaw/credentials.json. OpenClaw auto-enables channels when
// it detects the corresponding env var (e.g., DISCORD_BOT_TOKEN).
// See docs/reference/architecture.md "Credential Handling" for the full inventory.
const envArgs = [`CHAT_UI_URL=${shellQuote(chatUiUrl)}`];
if (process.env.NVIDIA_API_KEY) {
envArgs.push(`NVIDIA_API_KEY=${shellQuote(process.env.NVIDIA_API_KEY)}`);
const apiKey = getCredential("NVIDIA_API_KEY") || process.env.NVIDIA_API_KEY;
if (apiKey) {
envArgs.push(`NVIDIA_API_KEY=${shellQuote(apiKey)}`);
}
Comment on lines +564 to 567
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Finish the NVIDIA_API_KEY migration beyond envArgs.

These lines resolve the key only for sandbox env injection. Line 701 and Line 728 still read process.env.NVIDIA_API_KEY, so a key stored only in ~/.nemoclaw/credentials.json still breaks the non-interactive cloud path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/onboard.js` around lines 452 - 455, The code only adds the resolved
NVIDIA API key to envArgs but leaves process.env.NVIDIA_API_KEY untouched, so
later reads still fail for keys stored in credentials.json; modify the
onboarding flow so that when getCredential("NVIDIA_API_KEY") returns a value you
also set process.env.NVIDIA_API_KEY (or otherwise replace later direct
process.env reads with the resolved value) so the non-interactive cloud path
uses the credential. Ensure this change is applied where envArgs is built and
will cover subsequent usages that currently access process.env.NVIDIA_API_KEY
(refer to getCredential, envArgs and the later reads of
process.env.NVIDIA_API_KEY).

const discordToken = getCredential("DISCORD_BOT_TOKEN") || process.env.DISCORD_BOT_TOKEN;
if (discordToken) {
Expand All @@ -567,6 +573,10 @@ async function createSandbox(gpu) {
if (slackToken) {
envArgs.push(`SLACK_BOT_TOKEN=${shellQuote(slackToken)}`);
}
const tgToken = getCredential("TELEGRAM_BOT_TOKEN") || process.env.TELEGRAM_BOT_TOKEN;
if (tgToken) {
envArgs.push(`TELEGRAM_BOT_TOKEN=${shellQuote(tgToken)}`);
}

// Run without piping through awk — the pipe masked non-zero exit codes
// from openshell because bash returns the status of the last pipeline
Expand Down Expand Up @@ -1091,9 +1101,43 @@ async function onboard(opts = {}) {
await setupInference(sandboxName, model, provider);
await setupOpenclaw(sandboxName, model, provider);
await setupPolicies(sandboxName);
startMessagingBridges(sandboxName);
printDashboard(sandboxName, model, provider);
}

// ── Auto-start messaging bridges ────────────────────────────────
// RISKY CHANGE: This is a migration path to standardize messaging
// configuration. We auto-start host-side bridges when tokens are
// detected, which takes over from the in-sandbox OpenClaw plugin
// (Discord/Slack enforce single gateway connections per token).
// The in-sandbox env var passthrough (#601) is kept for backwards
// compatibility during this transition.

function startMessagingBridges(sandboxName) {
// Rehydrate file-backed credentials into process.env so start-services.sh
// and bridge.js can see them (they only check process.env, not credentials.json).
const bridgeTokens = ["TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN", "SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"];
for (const key of bridgeTokens) {
if (!process.env[key]) {
const val = getCredential(key);
if (val) process.env[key] = val;
}
}

const hasMessagingToken =
process.env.TELEGRAM_BOT_TOKEN ||
process.env.DISCORD_BOT_TOKEN ||
process.env.SLACK_BOT_TOKEN;

if (!hasMessagingToken) return;

console.log("");
console.log(" Starting messaging bridges...");
const safeName = sandboxName && /^[a-zA-Z0-9._-]+$/.test(sandboxName) ? sandboxName : null;
const sandboxEnv = safeName ? `SANDBOX_NAME=${shellQuote(safeName)}` : "";
run(`${sandboxEnv} bash "${SCRIPTS}/start-services.sh"`, { ignoreError: true });
Comment on lines +1134 to +1138
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't silently swallow bridge startup failures.

ignoreError: true keeps onboarding non-fatal, but dropping the result means a broken start-services.sh launch is invisible. Users can finish onboarding thinking messaging is live when it isn't.

Proposed fix
 console.log("");
 console.log("  Starting messaging bridges...");
 const safeName = sandboxName && /^[a-zA-Z0-9._-]+$/.test(sandboxName) ? sandboxName : null;
 const sandboxEnv = safeName ? `SANDBOX_NAME=${shellQuote(safeName)}` : "";
-run(`${sandboxEnv} bash "${SCRIPTS}/start-services.sh"`, { ignoreError: true });
+const result = run(`${sandboxEnv} bash "${SCRIPTS}/start-services.sh"`, { ignoreError: true });
+if (result.status !== 0) {
+  console.warn("  Warning: messaging bridge autostart failed.");
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log("");
console.log(" Starting messaging bridges...");
const safeName = sandboxName && /^[a-zA-Z0-9._-]+$/.test(sandboxName) ? sandboxName : null;
const sandboxEnv = safeName ? `SANDBOX_NAME=${shellQuote(safeName)}` : "";
run(`${sandboxEnv} bash "${SCRIPTS}/start-services.sh"`, { ignoreError: true });
console.log("");
console.log(" Starting messaging bridges...");
const safeName = sandboxName && /^[a-zA-Z0-9._-]+$/.test(sandboxName) ? sandboxName : null;
const sandboxEnv = safeName ? `SANDBOX_NAME=${shellQuote(safeName)}` : "";
const result = run(`${sandboxEnv} bash "${SCRIPTS}/start-services.sh"`, { ignoreError: true });
if (result.status !== 0) {
console.warn(" Warning: messaging bridge autostart failed.");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/onboard.js` around lines 1090 - 1094, The onboarding currently calls
run(`${sandboxEnv} bash "${SCRIPTS}/start-services.sh"`, { ignoreError: true })
which swallows failures; change this so the result/error from run is captured
and surfaced: call run without silently ignoring errors (or keep ignoreError but
assign the return value to a variable), then check the returned result or caught
exception for non-zero exit/code and log a clear error including sandboxEnv and
the command (e.g., using console.error or process.exit with a message) so
failures starting start-services.sh are visible; update the code around
safeName/sandboxEnv and the run call to perform that check and fail loudly when
appropriate.

}

module.exports = {
buildSandboxConfigSyncScript,
getFutureShellPathHint,
Expand Down
28 changes: 28 additions & 0 deletions docs/reference/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,31 @@ Agent (sandbox) ──▶ OpenShell gateway ──▶ NVIDIA Endpoint (build
```

Refer to [Inference Profiles](../reference/inference-profiles.md) for provider configuration details.

## Credential Handling

All user-provided secrets follow the same pattern:

1. Stored on the host in `~/.nemoclaw/credentials.json` (mode 0600) or as environment variables.
2. Retrieved via `getCredential(key)` which checks env vars first, then the credentials file.
3. Passed into the sandbox as environment variables at creation time.
4. Never written to `openclaw.json` (immutable at runtime).

### Credential Inventory

| Credential | Passed to sandbox | Used inside sandbox | Used on host | Notes |
|---|---|---|---|---|
| `NVIDIA_API_KEY` | Yes (env var) | Yes — startup script writes `auth-profiles.json` | Yes — deploy, Telegram bridge | OpenClaw requires a specific JSON file format; `nemoclaw-start.sh` handles the translation |
| `DISCORD_BOT_TOKEN` | Yes (env var) | Yes — OpenClaw reads env var directly, auto-enables Discord channel | No | |
| `SLACK_BOT_TOKEN` | Yes (env var) | Yes — OpenClaw reads env var directly, auto-enables Slack channel | No | |
| `TELEGRAM_BOT_TOKEN` | Yes (env var) | Available but unused — bridge runs on host | Yes — Telegram bridge (host-side) | Bridge uses SSH to relay messages into sandbox |
| `GITHUB_TOKEN` | Deploy path only | No | Yes — private repo access | Not needed inside sandbox |
| Gateway auth token | No — baked at build time | Yes — `openclaw.json` (root:root 444) | N/A | Per-build unique, immutable security boundary |

### Why Telegram runs on the host

The Telegram bridge (`scripts/telegram-bridge.js`) runs on the host and communicates with the sandbox via OpenShell SSH. This is intentional:

- It avoids giving the sandbox direct Telegram API access beyond what the network policy allows.
- It allows the bridge to manage multiple sandboxes from one host process.
- The `TELEGRAM_BOT_TOKEN` is still passed into the sandbox for future OpenClaw channel plugin support.
27 changes: 27 additions & 0 deletions nemoclaw-blueprint/blueprint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,33 @@ components:
credential_env: "OPENAI_API_KEY"
credential_default: "dummy"

bridges:
telegram:
type: messaging
adapter: telegram
credential_env: "TELEGRAM_BOT_TOKEN"
allowed_env: "ALLOWED_TELEGRAM_CHAT_IDS"
session_prefix: tg
max_chunk_size: 4000

discord:
type: messaging
adapter: discord
credential_env: "DISCORD_BOT_TOKEN"
allowed_env: "ALLOWED_DISCORD_CHANNEL_IDS"
session_prefix: dc
max_chunk_size: 1900

slack:
type: messaging
adapter: slack
credential_env: "SLACK_BOT_TOKEN"
extra_credential_env:
- "SLACK_APP_TOKEN"
allowed_env: "ALLOWED_SLACK_CHANNEL_IDS"
session_prefix: sl
max_chunk_size: 3000

policy:
base: "sandboxes/openclaw/policy.yaml"
additions:
Expand Down
Loading
Loading