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
11 changes: 9 additions & 2 deletions docs/manage-sandboxes/messaging-channels.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,15 @@ openclaw channels login --channel whatsapp # OpenClaw sandboxes
hermes whatsapp # Hermes sandboxes
```

For OpenClaw sandboxes, NemoClaw validates the gateway URL before pairing and renders the WhatsApp QR code in a compact terminal form so it fits in smaller terminal windows.
If pairing exits with a gateway close such as `1008`, rerun the login command one time and then check `nemoclaw <sandbox> channels status --channel whatsapp` so you can diagnose the gateway/session path separately from QR rendering.
<AgentOnly variant="openclaw">
NemoClaw validates the gateway URL before pairing and renders the WhatsApp QR code in a compact terminal form so it fits in smaller terminal windows.
After a successful QR login, NemoClaw restarts the WhatsApp channel through a bounded call to the sandbox's generated private gateway URL so the new session takes effect without granting `operator.admin` to the pairing device.
NemoClaw sends the gateway token only to that generated private URL.
If you set a different `OPENCLAW_GATEWAY_URL`, the login can save credentials, but NemoClaw does not automatically restart the channel.
Exit the sandbox and run `$$nemoclaw <sandbox> channels status --channel whatsapp` to confirm that it reconnects.
If the bounded restart fails for another reason, the pairing remains saved and the same status command reports the channel state.
If pairing exits with a gateway close such as `1008`, rerun the login command one time and then check `$$nemoclaw <sandbox> channels status --channel whatsapp` so you can diagnose the gateway/session path separately from QR rendering.
</AgentOnly>

The sandbox generates and stores session credentials inside durable agent state (`whatsapp` for OpenClaw, `platforms/whatsapp` for Hermes), so they survive rebuilds without re-pairing.
This is the runtime tradeoff of enabling WhatsApp without a host bridge: a paired sandbox can use that WhatsApp account until you unpair it or clear the durable state.
Expand Down
203 changes: 194 additions & 9 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3265,6 +3265,22 @@ PROXYEOF
# Preserve NemoClaw's sandbox-interface dial-back URL for the few
# NemoClaw-owned commands that require it without forcing ordinary
# OpenClaw CLI clients onto the explicit remote-gateway pairing path.
# Keep a separate readonly anchor for token-bearing helpers. The exported
# compatibility alias is intentionally caller-mutable, so it must never
# decide where a gateway token may be sent. Assign and verify the anchor
# in one fail-closed gate while keeping repeated sourcing idempotent. If
# the caller predeclared a conflicting readonly value, clear any ambient
# token and stop before token-bearing helpers are installed.
printf "if { [ \"\${_NEMOCLAW_TRUSTED_OPENCLAW_GATEWAY_URL:-}\" != '%s' ] &&\\n" "$_escaped_gateway_url"
printf " ! builtin readonly _NEMOCLAW_TRUSTED_OPENCLAW_GATEWAY_URL='%s' 2>/dev/null; } ||\\n" "$_escaped_gateway_url"
printf " [ \"\${_NEMOCLAW_TRUSTED_OPENCLAW_GATEWAY_URL:-}\" != '%s' ]; then\\n" "$_escaped_gateway_url"
printf " if ! builtin unset OPENCLAW_GATEWAY_TOKEN 2>/dev/null; then\\n"
printf " echo 'Error: NemoClaw rejected a conflicting gateway trust anchor, and the ambient gateway token could not be cleared.' >&2\\n"
printf " exit 1\\n"
printf " fi\\n"
printf " echo 'Error: NemoClaw rejected a conflicting gateway trust anchor; gateway-token helpers were disabled.' >&2\\n"
printf " return 1 2>/dev/null || exit 1\\n"
printf "fi\\nbuiltin readonly _NEMOCLAW_TRUSTED_OPENCLAW_GATEWAY_URL\\n"
printf "export NEMOCLAW_OPENCLAW_GATEWAY_URL='%s'\n" "$_escaped_gateway_url"
cat <<'GATEWAYURLENVEOF'
# Equality identifies NemoClaw's inherited private-interface value. A different
Expand Down Expand Up @@ -3342,6 +3358,111 @@ _nemoclaw_messaging_connect_node_options() {
done < "/tmp/nemoclaw-messaging-connect-preloads.list"
printf '%s' "$_nemoclaw_options"
}
# Read gateway.auth.token from the mutable OpenClaw config. Mirrors the
# entrypoint's _read_gateway_token, which is not emitted into this sourced
# file; keep the JSON5 fallback in sync with it.
_nemoclaw_whatsapp_gateway_token() {
node - "${OPENCLAW_STATE_DIR:-/sandbox/.openclaw}/openclaw.json" <<'NODEWATOKEN'
const fs = require("fs");

const configPath = process.argv[2];

function loadJson5() {
try {
const JSON5 = require("/opt/nemoclaw/node_modules/json5");
if (JSON5 && typeof JSON5.parse === "function") {
return JSON5;
}
} catch {
// Fall through to the caller's empty-token behavior.
}
return undefined;
}

try {
const text = fs.readFileSync(configPath, "utf8");
let cfg;
try {
cfg = JSON.parse(text);
} catch (jsonError) {
const JSON5 = loadJson5();
if (!JSON5) {
throw jsonError;
}
cfg = JSON5.parse(text);
}
console.log(cfg?.gateway?.auth?.token || "");
} catch {
console.log("");
}
NODEWATOKEN
}
# NemoClaw#6413: OpenClaw's `channels login` saves WhatsApp credentials
# locally and then asks the running gateway to restart the channel through the
# `channels.start` RPC, which the gateway gates behind `operator.admin`. When
# the login runs without the ambient gateway token, the client falls back to
# device auth, the device approval policy deliberately never grants
# `operator.admin`, and the post-pair restart is denied: credentials are saved
# but the running channel keeps its old session. Re-issue that same bounded
# RPC as a NemoClaw-owned one-shot with gateway-token auth after a successful
# login. The token stays scoped to this single fixed-argv child; per the
# runtime-env contract, removing the token from ordinary caller argv "is not a
# secrecy boundary against a command that deliberately reads the file", and
# this helper is exactly such an owned reader. Auto-approving `operator.admin`
# for ordinary devices would weaken the reviewed #6291 boundary and is
# deliberately not done here. Never fails the login: credentials are already
# saved, so a reconcile failure downgrades to host-side recovery guidance.
# Removal condition: drop this helper when the pinned OpenClaw lets a local
# login (re)start its own channel account without `operator.admin`, or ships
# its own bounded post-login reconcile.
_nemoclaw_whatsapp_postpair_start() {
local _nemoclaw_wa_gateway_url="$1" _nemoclaw_wa_insecure_ws="$2" _nemoclaw_wa_account="$3"
local _nemoclaw_wa_token _nemoclaw_wa_params _nemoclaw_wa_call_output
# The account id is caller input embedded in JSON params; allowlist it
# instead of quoting it (mirrors the approval policy's request-id pattern).
# Evaluate the range under the C locale so [A-Za-z0-9._:-] stays ASCII and is
# not widened by the caller's LC_COLLATE/LC_CTYPE. Scoped to this function
# subshell-style: the assignment cannot leak into the interactive shell
# because the helper only runs inside command substitution / the reconcile.
local LC_ALL=C
case "$_nemoclaw_wa_account" in
"") _nemoclaw_wa_params='{"channel":"whatsapp"}' ;;
*[!A-Za-z0-9._:-]*)
echo "[whatsapp] Credentials saved, but the account id contains unsupported characters, so the" >&2
echo "[whatsapp] running gateway was not asked to restart the channel. Exit the sandbox and run" >&2
echo "[whatsapp] 'nemoclaw <sandbox> channels status --channel whatsapp' to confirm it reconnects." >&2
return 0
;;
*)
if [ "${#_nemoclaw_wa_account}" -gt 128 ]; then
echo "[whatsapp] Credentials saved, but the account id exceeds 128 characters, so the" >&2
echo "[whatsapp] running gateway was not asked to restart the channel. Exit the sandbox and run" >&2
echo "[whatsapp] 'nemoclaw <sandbox> channels status --channel whatsapp' to confirm it reconnects." >&2
return 0
fi
_nemoclaw_wa_params='{"channel":"whatsapp","accountId":"'"$_nemoclaw_wa_account"'"}'
;;
esac
_nemoclaw_wa_token="$(_nemoclaw_whatsapp_gateway_token)"
if [ -z "$_nemoclaw_wa_token" ]; then
echo "[whatsapp] Credentials saved, but the gateway token is unavailable in this shell, so the" >&2
echo "[whatsapp] running gateway was not asked to restart the channel. Exit the sandbox and run" >&2
echo "[whatsapp] 'nemoclaw <sandbox> channels status --channel whatsapp' to confirm it reconnects." >&2
return 0
fi
if _nemoclaw_wa_call_output="$(OPENCLAW_GATEWAY_URL="$_nemoclaw_wa_gateway_url" \
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="$_nemoclaw_wa_insecure_ws" \
OPENCLAW_GATEWAY_TOKEN="$_nemoclaw_wa_token" \
command openclaw gateway call channels.start --params "$_nemoclaw_wa_params" --json 2>&1)"; then
echo "[whatsapp] Restarted the WhatsApp channel on the running gateway with the new credentials." >&2
else
echo "[whatsapp] Credentials saved, but restarting the channel on the running gateway failed:" >&2
printf '%s\n' "$_nemoclaw_wa_call_output" | tail -n 3 | sed 's/^/[whatsapp] /' >&2
echo "[whatsapp] Exit the sandbox and run 'nemoclaw <sandbox> channels status --channel whatsapp'" >&2
echo "[whatsapp] to check the channel; re-run the login from a fresh connect shell if it stays down." >&2
fi
return 0
}
openclaw() {
# NemoClaw#4462: approval calls temporarily drop the gateway URL/port/token
# so OpenClaw resolves the local loopback gateway and device token. The
Expand Down Expand Up @@ -3392,8 +3513,10 @@ openclaw() {
list | status | "" | -h | --help) ;;
login)
_login_channel=""
_login_account=""
_login_help=0
_prev_arg_was_channel_flag=0
_prev_arg_was_account_flag=0
_seen_login_subcommand=0
for _arg in "$@"; do
if [ "$_seen_login_subcommand" = "0" ]; then
Expand All @@ -3405,13 +3528,24 @@ openclaw() {
_prev_arg_was_channel_flag=0
continue
fi
if [ "$_prev_arg_was_account_flag" = "1" ]; then
_login_account="$_arg"
_prev_arg_was_account_flag=0
continue
fi
case "$_arg" in
--channel)
_prev_arg_was_channel_flag=1
;;
--channel=*)
_login_channel="${_arg#--channel=}"
;;
--account)
_prev_arg_was_account_flag=1
;;
--account=*)
_login_account="${_arg#--account=}"
;;
-h | --help)
_login_help=1
;;
Expand Down Expand Up @@ -3482,16 +3616,52 @@ openclaw() {
# injecting them again here covers non-connect shells. Runtime
# preload modules are idempotent, so a double --require is harmless.
_nemoclaw_connect_node_options="$(_nemoclaw_messaging_connect_node_options)"
if [ -n "$_nemoclaw_connect_node_options" ]; then
OPENCLAW_GATEWAY_URL="$_nemoclaw_whatsapp_gateway_url" \
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="$_nemoclaw_whatsapp_insecure_ws" \
NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }$_nemoclaw_connect_node_options" \
command openclaw "$@"
else
OPENCLAW_GATEWAY_URL="$_nemoclaw_whatsapp_gateway_url" \
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="$_nemoclaw_whatsapp_insecure_ws" \
command openclaw "$@"
# The shared gateway token (ambient in connect shells; see the
# runtime env at OPENCLAW_GATEWAY_TOKEN) must never reach a
# caller-selected gateway. The public NEMOCLAW_* compatibility
# alias is caller-mutable, so only the readonly value baked into the
# generated root-owned runtime file may act as the trust anchor.
# The login tolerates a caller-supplied OPENCLAW_GATEWAY_URL
# override; only the baked URL may carry the token, for either the
# login itself or the reconcile.
_nemoclaw_whatsapp_trusted_url="${_NEMOCLAW_TRUSTED_OPENCLAW_GATEWAY_URL:-}"
_nemoclaw_whatsapp_url_is_trusted=0
if [ -n "$_nemoclaw_whatsapp_trusted_url" ] &&
[ "$_nemoclaw_whatsapp_gateway_url" = "$_nemoclaw_whatsapp_trusted_url" ]; then
_nemoclaw_whatsapp_url_is_trusted=1
fi
# Whether the login could authenticate with the gateway token (only
# when it is both present and pointed at the trusted URL). If so,
# OpenClaw's own post-pair `channels.start` succeeds and no NemoClaw
# reconcile is needed; otherwise that restart is denied for lack of
# operator.admin and NemoClaw reconciles it below (NemoClaw#6413).
_nemoclaw_whatsapp_login_had_token=0
if [ "$_nemoclaw_whatsapp_url_is_trusted" = "1" ] && [ -n "${OPENCLAW_GATEWAY_TOKEN:-}" ]; then
_nemoclaw_whatsapp_login_had_token=1
fi
# Run the login with errexit disabled so its exit status is always
# captured (and the post-login guidance/reconcile always runs) even
# when the caller shell has `set -e`; mirrors the devices-approve
# and configure-guard branches. Restored before returning.
_nemoclaw_whatsapp_login_errexit=0
case $- in *e*) _nemoclaw_whatsapp_login_errexit=1 ;; esac
set +e
(
# A non-trusted (caller-selected) target must not receive the
# shared gateway token; strip it so the login there falls back to
# device auth, matching the #6291 WhatsApp-login boundary.
[ "$_nemoclaw_whatsapp_url_is_trusted" = "1" ] || builtin unset OPENCLAW_GATEWAY_TOKEN
if [ -n "$_nemoclaw_connect_node_options" ]; then
OPENCLAW_GATEWAY_URL="$_nemoclaw_whatsapp_gateway_url" \
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="$_nemoclaw_whatsapp_insecure_ws" \
NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }$_nemoclaw_connect_node_options" \
command openclaw "$@"
else
OPENCLAW_GATEWAY_URL="$_nemoclaw_whatsapp_gateway_url" \
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="$_nemoclaw_whatsapp_insecure_ws" \
command openclaw "$@"
fi
)
_whatsapp_login_exit=$?
if [ "$_whatsapp_login_exit" -ne 0 ]; then
echo "" >&2
Expand All @@ -3500,7 +3670,22 @@ openclaw() {
echo "issue, not a QR-size issue — the QR above rendered independently of the gateway." >&2
echo "[whatsapp] Re-run 'openclaw channels login --channel whatsapp' to retry. If it keeps" >&2
echo "closing, exit the sandbox and run 'nemoclaw <sandbox> channels status --channel whatsapp'." >&2
elif [ "$_nemoclaw_whatsapp_url_is_trusted" != "1" ]; then
# Login targeted a caller-supplied gateway URL rather than
# NemoClaw's injected private URL; the token was withheld above,
# and the reconcile must not send it there either.
echo "[whatsapp] Credentials saved. The channel was not auto-restarted because pairing used a" >&2
echo "[whatsapp] custom gateway URL; exit the sandbox and run" >&2
echo "[whatsapp] 'nemoclaw <sandbox> channels status --channel whatsapp' to confirm it reconnects." >&2
elif [ "$_nemoclaw_whatsapp_login_had_token" = "1" ]; then
: # Login authenticated with the gateway token, so its own
# post-pair channels.start already restarted the channel; a
# second restart would only bounce the freshly started session.
else
_nemoclaw_whatsapp_postpair_start "$_nemoclaw_whatsapp_trusted_url" \
"$_nemoclaw_whatsapp_insecure_ws" "$_login_account"
fi
[ "$_nemoclaw_whatsapp_login_errexit" = "1" ] && set -e
return $_whatsapp_login_exit
fi
;;
Expand Down
Loading
Loading