@@ -3342,6 +3342,103 @@ _nemoclaw_messaging_connect_node_options() {
33423342 done < "/tmp/nemoclaw-messaging-connect-preloads.list"
33433343 printf '%s' "$_nemoclaw_options"
33443344}
3345+ # Read gateway.auth.token from the mutable OpenClaw config. Mirrors the
3346+ # entrypoint's _read_gateway_token, which is not emitted into this sourced
3347+ # file; keep the JSON5 fallback in sync with it.
3348+ _nemoclaw_whatsapp_gateway_token() {
3349+ node - "${OPENCLAW_STATE_DIR:-/sandbox/.openclaw}/openclaw.json" <<'NODEWATOKEN'
3350+ const fs = require("fs");
3351+
3352+ const configPath = process.argv[2];
3353+
3354+ function loadJson5() {
3355+ try {
3356+ const JSON5 = require("/opt/nemoclaw/node_modules/json5");
3357+ if (JSON5 && typeof JSON5.parse === "function") {
3358+ return JSON5;
3359+ }
3360+ } catch {
3361+ // Fall through to the caller's empty-token behavior.
3362+ }
3363+ return undefined;
3364+ }
3365+
3366+ try {
3367+ const text = fs.readFileSync(configPath, "utf8");
3368+ let cfg;
3369+ try {
3370+ cfg = JSON.parse(text);
3371+ } catch (jsonError) {
3372+ const JSON5 = loadJson5();
3373+ if (!JSON5) {
3374+ throw jsonError;
3375+ }
3376+ cfg = JSON5.parse(text);
3377+ }
3378+ console.log(cfg?.gateway?.auth?.token || "");
3379+ } catch {
3380+ console.log("");
3381+ }
3382+ NODEWATOKEN
3383+ }
3384+ # NemoClaw#6413: OpenClaw's `channels login` saves WhatsApp credentials
3385+ # locally and then asks the running gateway to restart the channel through the
3386+ # `channels.start` RPC, which the gateway gates behind `operator.admin`. When
3387+ # the login runs without the ambient gateway token, the client falls back to
3388+ # device auth, the device approval policy deliberately never grants
3389+ # `operator.admin`, and the post-pair restart is denied: credentials are saved
3390+ # but the running channel keeps its old session. Re-issue that same bounded
3391+ # RPC as a NemoClaw-owned one-shot with gateway-token auth after a successful
3392+ # login. The token stays scoped to this single fixed-argv child; per the
3393+ # runtime-env contract, removing the token from ordinary caller argv "is not a
3394+ # secrecy boundary against a command that deliberately reads the file", and
3395+ # this helper is exactly such an owned reader. Auto-approving `operator.admin`
3396+ # for ordinary devices would weaken the reviewed #6291 boundary and is
3397+ # deliberately not done here. Never fails the login: credentials are already
3398+ # saved, so a reconcile failure downgrades to host-side recovery guidance.
3399+ # Removal condition: drop this helper when the pinned OpenClaw lets a local
3400+ # login (re)start its own channel account without `operator.admin`, or ships
3401+ # its own bounded post-login reconcile.
3402+ _nemoclaw_whatsapp_postpair_start() {
3403+ local _nemoclaw_wa_gateway_url="$1" _nemoclaw_wa_insecure_ws="$2" _nemoclaw_wa_account="$3"
3404+ local _nemoclaw_wa_token _nemoclaw_wa_params _nemoclaw_wa_call_output
3405+ # The account id is caller input embedded in JSON params; allowlist it
3406+ # instead of quoting it (mirrors the approval policy's request-id pattern).
3407+ # Evaluate the range under the C locale so [A-Za-z0-9._:-] stays ASCII and is
3408+ # not widened by the caller's LC_COLLATE/LC_CTYPE. Scoped to this function
3409+ # subshell-style: the assignment cannot leak into the interactive shell
3410+ # because the helper only runs inside command substitution / the reconcile.
3411+ local LC_ALL=C
3412+ case "$_nemoclaw_wa_account" in
3413+ "") _nemoclaw_wa_params='{"channel":"whatsapp"}' ;;
3414+ *[!A-Za-z0-9._:-]*)
3415+ echo "[whatsapp] Credentials saved, but the account id contains unsupported characters, so the" >&2
3416+ echo "[whatsapp] running gateway was not asked to restart the channel. Exit the sandbox and run" >&2
3417+ echo "[whatsapp] 'nemoclaw <sandbox> channels status --channel whatsapp' to confirm it reconnects." >&2
3418+ return 0
3419+ ;;
3420+ *) _nemoclaw_wa_params='{"channel":"whatsapp","accountId":"'"$_nemoclaw_wa_account"'"}' ;;
3421+ esac
3422+ _nemoclaw_wa_token="$(_nemoclaw_whatsapp_gateway_token)"
3423+ if [ -z "$_nemoclaw_wa_token" ]; then
3424+ echo "[whatsapp] Credentials saved, but the gateway token is unavailable in this shell, so the" >&2
3425+ echo "[whatsapp] running gateway was not asked to restart the channel. Exit the sandbox and run" >&2
3426+ echo "[whatsapp] 'nemoclaw <sandbox> channels status --channel whatsapp' to confirm it reconnects." >&2
3427+ return 0
3428+ fi
3429+ if _nemoclaw_wa_call_output="$(OPENCLAW_GATEWAY_URL="$_nemoclaw_wa_gateway_url" \
3430+ OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="$_nemoclaw_wa_insecure_ws" \
3431+ OPENCLAW_GATEWAY_TOKEN="$_nemoclaw_wa_token" \
3432+ command openclaw gateway call channels.start --params "$_nemoclaw_wa_params" --json 2>&1)"; then
3433+ echo "[whatsapp] Restarted the WhatsApp channel on the running gateway with the new credentials." >&2
3434+ else
3435+ echo "[whatsapp] Credentials saved, but restarting the channel on the running gateway failed:" >&2
3436+ printf '%s\n' "$_nemoclaw_wa_call_output" | tail -n 3 | sed 's/^/[whatsapp] /' >&2
3437+ echo "[whatsapp] Exit the sandbox and run 'nemoclaw <sandbox> channels status --channel whatsapp'" >&2
3438+ echo "[whatsapp] to check the channel; re-run the login from a fresh connect shell if it stays down." >&2
3439+ fi
3440+ return 0
3441+ }
33453442openclaw() {
33463443 # NemoClaw#4462: approval calls temporarily drop the gateway URL/port/token
33473444 # so OpenClaw resolves the local loopback gateway and device token. The
@@ -3392,8 +3489,10 @@ openclaw() {
33923489 list | status | "" | -h | --help) ;;
33933490 login)
33943491 _login_channel=""
3492+ _login_account=""
33953493 _login_help=0
33963494 _prev_arg_was_channel_flag=0
3495+ _prev_arg_was_account_flag=0
33973496 _seen_login_subcommand=0
33983497 for _arg in "$@"; do
33993498 if [ "$_seen_login_subcommand" = "0" ]; then
@@ -3405,13 +3504,24 @@ openclaw() {
34053504 _prev_arg_was_channel_flag=0
34063505 continue
34073506 fi
3507+ if [ "$_prev_arg_was_account_flag" = "1" ]; then
3508+ _login_account="$_arg"
3509+ _prev_arg_was_account_flag=0
3510+ continue
3511+ fi
34083512 case "$_arg" in
34093513 --channel)
34103514 _prev_arg_was_channel_flag=1
34113515 ;;
34123516 --channel=*)
34133517 _login_channel="${_arg#--channel=}"
34143518 ;;
3519+ --account)
3520+ _prev_arg_was_account_flag=1
3521+ ;;
3522+ --account=*)
3523+ _login_account="${_arg#--account=}"
3524+ ;;
34153525 -h | --help)
34163526 _login_help=1
34173527 ;;
@@ -3482,16 +3592,47 @@ openclaw() {
34823592 # injecting them again here covers non-connect shells. Runtime
34833593 # preload modules are idempotent, so a double --require is harmless.
34843594 _nemoclaw_connect_node_options="$(_nemoclaw_messaging_connect_node_options)"
3485- if [ -n "$_nemoclaw_connect_node_options" ]; then
3486- OPENCLAW_GATEWAY_URL="$_nemoclaw_whatsapp_gateway_url" \
3487- OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="$_nemoclaw_whatsapp_insecure_ws" \
3488- NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }$_nemoclaw_connect_node_options" \
3489- command openclaw "$@"
3490- else
3491- OPENCLAW_GATEWAY_URL="$_nemoclaw_whatsapp_gateway_url" \
3492- OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="$_nemoclaw_whatsapp_insecure_ws" \
3493- command openclaw "$@"
3595+ # The shared gateway token (ambient in connect shells; see the
3596+ # runtime env at OPENCLAW_GATEWAY_TOKEN) must never reach a
3597+ # caller-selected gateway. NemoClaw injects its trusted private URL
3598+ # as NEMOCLAW_OPENCLAW_GATEWAY_URL; the login tolerates a
3599+ # caller-supplied OPENCLAW_GATEWAY_URL override, so classify whether
3600+ # this login is aimed at the trusted URL. Only the trusted URL may
3601+ # carry the token — for the login itself and for the reconcile.
3602+ _nemoclaw_whatsapp_trusted_url="${NEMOCLAW_OPENCLAW_GATEWAY_URL:-}"
3603+ _nemoclaw_whatsapp_url_is_trusted=0
3604+ if [ -n "$_nemoclaw_whatsapp_trusted_url" ] &&
3605+ [ "$_nemoclaw_whatsapp_gateway_url" = "$_nemoclaw_whatsapp_trusted_url" ]; then
3606+ _nemoclaw_whatsapp_url_is_trusted=1
3607+ fi
3608+ # Whether the login could authenticate with the gateway token (only
3609+ # when it is both present and pointed at the trusted URL). If so,
3610+ # OpenClaw's own post-pair `channels.start` succeeds and no NemoClaw
3611+ # reconcile is needed; otherwise that restart is denied for lack of
3612+ # operator.admin and NemoClaw reconciles it below (NemoClaw#6413).
3613+ _nemoclaw_whatsapp_login_had_token=0
3614+ if [ "$_nemoclaw_whatsapp_url_is_trusted" = "1" ] && [ -n "${OPENCLAW_GATEWAY_TOKEN:-}" ]; then
3615+ _nemoclaw_whatsapp_login_had_token=1
34943616 fi
3617+ # Run the login with errexit disabled so its exit status is always
3618+ # captured (and the post-login guidance/reconcile always runs) even
3619+ # when the caller shell has `set -e`; mirrors the devices-approve
3620+ # and configure-guard branches. Restored before returning.
3621+ _nemoclaw_whatsapp_login_errexit=0
3622+ case $- in *e*) _nemoclaw_whatsapp_login_errexit=1 ;; esac
3623+ set +e
3624+ (
3625+ # A non-trusted (caller-selected) target must not receive the
3626+ # shared gateway token; strip it so the login there falls back to
3627+ # device auth, matching the #6291 WhatsApp-login boundary.
3628+ [ "$_nemoclaw_whatsapp_url_is_trusted" = "1" ] || builtin unset OPENCLAW_GATEWAY_TOKEN
3629+ export OPENCLAW_GATEWAY_URL="$_nemoclaw_whatsapp_gateway_url"
3630+ export OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="$_nemoclaw_whatsapp_insecure_ws"
3631+ if [ -n "$_nemoclaw_connect_node_options" ]; then
3632+ export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }$_nemoclaw_connect_node_options"
3633+ fi
3634+ command openclaw "$@"
3635+ )
34953636 _whatsapp_login_exit=$?
34963637 if [ "$_whatsapp_login_exit" -ne 0 ]; then
34973638 echo "" >&2
@@ -3500,7 +3641,22 @@ openclaw() {
35003641 echo "issue, not a QR-size issue — the QR above rendered independently of the gateway." >&2
35013642 echo "[whatsapp] Re-run 'openclaw channels login --channel whatsapp' to retry. If it keeps" >&2
35023643 echo "closing, exit the sandbox and run 'nemoclaw <sandbox> channels status --channel whatsapp'." >&2
3644+ elif [ "$_nemoclaw_whatsapp_url_is_trusted" != "1" ]; then
3645+ # Login targeted a caller-supplied gateway URL rather than
3646+ # NemoClaw's injected private URL; the token was withheld above,
3647+ # and the reconcile must not send it there either.
3648+ echo "[whatsapp] Credentials saved. The channel was not auto-restarted because pairing used a" >&2
3649+ echo "[whatsapp] custom gateway URL; exit the sandbox and run" >&2
3650+ echo "[whatsapp] 'nemoclaw <sandbox> channels status --channel whatsapp' to confirm it reconnects." >&2
3651+ elif [ "$_nemoclaw_whatsapp_login_had_token" = "1" ]; then
3652+ : # Login authenticated with the gateway token, so its own
3653+ # post-pair channels.start already restarted the channel; a
3654+ # second restart would only bounce the freshly started session.
3655+ else
3656+ _nemoclaw_whatsapp_postpair_start "$_nemoclaw_whatsapp_trusted_url" \
3657+ "$_nemoclaw_whatsapp_insecure_ws" "$_login_account"
35033658 fi
3659+ [ "$_nemoclaw_whatsapp_login_errexit" = "1" ] && set -e
35043660 return $_whatsapp_login_exit
35053661 fi
35063662 ;;
0 commit comments