fix(messaging): let the operator choose the Hermes WhatsApp mode - #8423
fix(messaging): let the operator choose the Hermes WhatsApp mode#8423hunglp6d wants to merge 25 commits into
Conversation
The manifest rendered `WHATSAPP_MODE=bot` as a constant, so neither mode was
reachable. Bot mode rejects every sender when the allowlist is empty, and
onboarding never collects one, so a freshly onboarded Hermes sandbox with
WhatsApp paired could not deliver a message.
The value is also an override. The Hermes adapter reads
`os.getenv("WHATSAPP_MODE", "self-chat")`, and self-chat reads no allowlist at
all, so leaving the key unset is the configuration that works. NemoClaw replaced
that with the one mode that needs a value it never asks for.
Make the mode an input carrying that same default, with no prompt, matching the
shape `allowedIds` already has. Onboarding asks nothing extra and a paired
sandbox replies. Selecting bot stays a deliberate act:
WHATSAPP_MODE=bot WHATSAPP_ALLOWED_IDS=<number> nemoclaw <sandbox> channels add whatsapp
Verified live on a Hermes sandbox before the change: with self-chat and no
allowlist, a message to the paired account's own chat reached the agent and was
answered; with bot mode and `WHATSAPP_ALLOWED_IDS='*'`, a message from a second
number was answered as well.
`channels status` now lists the mode beside the allowlist, since the mode
decides whether an empty allowlist matters.
Signed-off-by: Hung Le <hple@nvidia.com>
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughHermes WhatsApp mode is configurable through ChangesHermes WhatsApp mode configuration
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Onboarding
participant WhatsAppManifest
participant HostState
participant HermesConfig
Onboarding->>WhatsAppManifest: seed mode self-chat
Onboarding->>HostState: store mode and allowlist
HostState->>HermesConfig: render WhatsApp settings
HermesConfig-->>HermesConfig: use self-chat or bot mode
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
|
🌿 Preview your docs: https://nvidia-preview-pr-8423.docs.buildwithfern.com/nemoclaw |
Code Coverage OverviewLanguages: TypeScript TypeScript / code-coverage/pluginThe overall coverage in commit fdb182e in the TypeScript / code-coverage/cliThe overall coverage in commit fdb182e in the Show a code coverage summary of the most impacted files.
Updated |
PR Review Advisor — No blocking findings reportedAdvisor assessment: No blocking advisor findings reported Model lanes
6 terminology differences from the second opinionAdvisory only. These are normalized differences from the primary terminology receipt.
1 additional E2E selection from the second opinionAdvisory only. The primary lane did not select these E2E jobs or targets.
Second-opinion terminology and E2E selections are advisory. Live E2E does not run automatically for pull requests. 3 semantic terminology decisionsTerminology decisions are advisory. They affect the assessment only when a separate finding identifies concrete semantic impact.
E2E guidanceAdvisory only. A maintainer can dispatch the default E2E suite against this exact revision. Recommended E2E: This automated review informs maintainers. Warnings and suggestions do not require a response. A maintainer decides whether to merge. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/manage-sandboxes/set-up-whatsapp.mdx`:
- Line 90: In the WhatsApp adapter sentence, replace the American-English
spelling “afterwards” with “afterward,” preserving the rest of the wording and
meaning.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 57bf1b12-3439-465a-9313-127f69b703af
📒 Files selected for processing (12)
docs/manage-sandboxes/set-up-whatsapp.mdxsrc/lib/messaging-channel-config.test.tssrc/lib/messaging/applier/host-state-applier.test.tssrc/lib/messaging/channels/metadata.test.tssrc/lib/messaging/channels/whatsapp/manifest.tssrc/lib/messaging/channels/whatsapp/rendered-config-parser.tssrc/lib/messaging/channels/whatsapp/template-resolver.test.tssrc/lib/messaging/channels/whatsapp/template-resolver.tssrc/lib/onboard/messaging-channel-setup.test.tstest/e2e/live/channels-stop-start-helpers.tstest/generate-hermes-config.test.tstest/messaging-plan-test-helper.ts
Signed-off-by: Hung Le <hple@nvidia.com>
A rebuild renders from the persisted plan, and neither persistence path re-applies the input's validValues: normalizeFullInputs copies `value` verbatim, and inputReferenceFromManifest copies `persisted.value` onto the manifest spec. A stale or hand-edited registry entry could therefore render a WHATSAPP_MODE the bundled bridge cannot serve, leaving a channel that reports itself configured and answers nobody. Re-check the value where it is rendered and fall back to the mode the Hermes adapter already defaults to. The earlier fallback test exercised the env-to-compiler path, where validValues already drops an unusable value, so it could not have caught this. Both persistence paths now have a case. Signed-off-by: Hung Le <hple@nvidia.com>
"Refer to the section below" and "set both values together" both left the reader to infer the target. Name the section and the two environment variables instead. Signed-off-by: Hung Le <hple@nvidia.com>
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
Signed-off-by: Apurv Kumaria <akumaria@nvidia.com>
|
Maintainer security and gate audit completed for revision
No actionable security defect was found. Before approval, a maintainer should explicitly accept both the legacy-mode behavior and the narrower issue scope. I am changing the PR description from auto-closing #8312 to describing this as a partial implementation, so the remaining work is not closed accidentally. GitHub Actions jobs are currently queued or cancelled during the reported service degradation. I have not rerun them. |
Signed-off-by: Apurv Kumaria <akumaria@nvidia.com>
|
Documentation follow-up completed in revision
|
cv
left a comment
There was a problem hiding this comment.
Cover explicit Hermes bot mode through the complete channel lifecycle. Seed WHATSAPP_MODE=bot with a nonempty sender allowlist, then assert the persisted input and rendered WHATSAPP_MODE=bot before stop, after rebuild, and after start. This mode changes sender authorization, so complete the nine-category security review for the final head. Refresh onto current main and rerun the managed-runtime activation and required checks after adding the lifecycle evidence.
Signed-off-by: Apurv Kumaria <akumaria@nvidia.com>
Signed-off-by: Apurv Kumaria <akumaria@nvidia.com>
|
Addressed the requested Hermes
Fresh CI hit an unrelated 5-second timeout in the untouched The two product choices already called out in the description remain unchanged for maintainer direction: legacy plans without stored mode fall back to |
|
The rerun of CLI shard 6 passed. The prior host-local vLLM timeout is confirmed transient: the isolated test passed locally (3/3), the same CI shard passed on rerun, and every required check is now green. The remaining blockers are the two current human change requests concerning the legacy-plan fallback and the intended scope relative to #8312; those require product direction rather than another test or CI change. |
<!-- markdownlint-disable MD041 --> ## Summary `rebuild` starts the Hermes gateway during sandbox recreation and restores durable workspace state later. A gateway that remains alive across that restore can keep serving state it read beforehand. This change restarts the gateway after restoration and verifies the replacement before the rebuild reports success. The Hermes cron dispatch gate now remains active through MCP reconciliation, gateway restart, and bounded health or recovery checks. Dispatch resumes only after the controller revalidates the acquisition token and the same replacement process that passed the health boundary. Process substitution, MCP failure, or an unverifiable replacement fails closed and leaves recovery guidance instead of releasing dispatch. A separate root-owned release recovery record now survives a failed marker rollback. Recovery reconciles that record and reacquires the cron gate before any gateway repair, then validates restored cron state before clearing NemoClaw-owned recovery state. ## Related Issue Part of #8184, item 4. No closing keyword is used because #8184 is an umbrella report with other independently tracked items. ## Changes - Extended the root-owned Hermes cron restore controller with replacement observation and completion receipts bound to the acquisition token, replacement process ID, and start time. - Kept the cron marker active across state restoration, MCP reconciliation, gateway restart, and post-restart health or recovery. - Required matching observations before and after health verification. A second gateway replacement during that interval is unverified and cannot release the marker. - Revalidated the replacement under the controller lock immediately before marker removal. Release errors restore the marker; an exceptional marker-rollback failure reports the gate state as unverified and directs immediate recovery. - Added a durable root-owned release recovery record so `recover` can reacquire the gate before gateway repair when marker rollback fails. - Synchronized protected state-directory entry transitions and made publication, deletion, resynchronization, and rollback durability failures fail closed. - Preserved fail-closed ordering for MCP reconciliation failures and printed MCP repair before sandbox recovery. - Updated the recovery documentation for the gate lifecycle, identity-bound verification, and rollback-uncertainty guidance. - Added unit, integration, harness, and live-E2E coverage for ordering, substitution races, release rollback, MCP failure, replacement identity, and preservation of an independent operator drain across every negative completion path. No credential flow, external dependency, network policy, configuration input, or compatibility fallback is added. ## Type of Change - [ ] Code change (feature, bug fix, or refactor) - [x] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) ## Quality Gates - [x] Tests added or updated for changed behavior - [ ] Existing tests cover changed behavior — justification: - [ ] Tests not applicable — justification: - [x] Docs updated for user-facing behavior changes - [ ] Docs not applicable — justification: - [x] Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging) - [x] Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval link/justification: [Independent maintainer receipt](#8472 (comment)). All nine security categories passed with no findings, including token ownership, root-owned marker and recovery-record validation, gate-before-repair ordering, process-identity substitution races, durable fail-closed release, and rollback behavior. - [ ] Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue: ## Documentation Writer Review - [x] Documentation writer subagent reviewed the completed changes - Result: `docs-updated` - Evidence: `docs/manage-sandboxes/recover-rebuild-sandboxes.mdx` documents the retained cron gate, root-owned release recovery record, pre-repair gate reacquisition, replacement identity verification, MCP repair ordering, operator-drain preservation, and retry behavior. Independent documentation review passed, and the refreshed documentation build completed with 0 errors and 2 existing Fern warnings. - Agent: Codex Desktop <!-- docs-review-head-sha: 9ed6cd7 --> <!-- docs-review-agents-blob-sha: 12ad395 --> ## DGX Station Hardware Evidence - [ ] Tested on DGX Station - Tested commit: - Station profile/scenario: - Result: - Supporting evidence: ## Verification - [x] PR description includes a `Signed-off-by:` line and every commit appears as `Verified` in GitHub - [x] Normal `pre-commit`, `commit-msg`, and `pre-push` hooks passed, or `npm run validate:pr` passed after refreshing `origin/main` when hooks were skipped or unavailable - [x] Targeted behavior tests pass for the current change set, or tests are marked not applicable above — after refreshing from current `main`, 57 focused CLI tests and 57 integration tests pass with one intentional platform skip. CLI type checking, controller hash verification, Biome, test-title policy, documentation build, targeted hooks, secret scan, source-shape, test-size, and whitespace checks pass. - [ ] Applicable broad gate passed — `npm test` for broad runtime/test-harness changes; `npm run check` for repo-wide validation/coverage changes — command/result: - [x] Quality Gates section completed with required justifications or waivers - [x] No secrets, API keys, or credentials committed - [ ] `npm run docs` builds without warnings (doc changes only) — build passed with 0 errors and 2 existing warnings - [x] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) ### Live evidence The contributor measured the original restore/restart behavior four times on three hosts. Each sandbox came from a real non-interactive onboarding run, and a marker in the `memories` state directory proved that durable state was restored. The branch runs showed the gateway starting after restoration, while `main` showed the gateway starting before it. | Run | Build | Gateway start vs restore | `gateway-starts.log` | |---|---|---|---| | 1 | `main` | **−67 s** (before) | 1 entry | | 2 | `main` | **−27 s** (before) | 1 entry | | 3 | `main` | **−49 s** (before) | 1 entry | | 1 | this branch | **+6 s** (after) | 2 entries | | 2 | this branch | **+9 s** (after) | 2 entries | | 3 | this branch | **+9 s** (after) | 2 entries | The second log entry makes the restart observable inside the sandbox. The later cron-gate changes are covered by deterministic controller and host tests that prove dispatch remains blocked until the restored-state replacement is verified. Environments for the live measurements: Ubuntu 24.04, kernels 6.8 and 6.17, x86_64, Docker 29.7.2, OpenShell 0.0.85, Hermes Agent 0.19.0, Node 22.23.2, and NVIDIA hosted inference. ### Scope This behavior is Hermes-specific. `ensureHermesGatewayAfterStateRestoreForCronGate` returns `not-applicable` for other agents. The change corrects the ordering between restored state, gateway replacement, and cron dispatch. Other Hermes WhatsApp defects remain tracked separately under #8184. ### Merge-order note for #8423 PR #8423 adds documentation instructing users to run `gateway restart` after `rebuild`, with rationale this change removes. That text is not on `main`, so whichever PR merges second must remove the obsolete manual restart instruction and its explanation. --- Signed-off-by: Hung Le <hple@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Rebuilds now restart and verify the gateway after restoring state. - Improved recovery handles gateway failures and replacement gateways safely. - Cron dispatch remains blocked until restoration and verification complete successfully. - Failed restores preserve safeguards and provide targeted gateway and MCP recovery guidance. - Rebuild errors now report gateway replacement, MCP restoration, and rollback failures clearly. - Gateway identity validation helps prevent releasing the wrong restored process. - **Documentation** - Updated sandbox recovery guidance with revised restart, verification, and troubleshooting steps. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Hung Le <hple@nvidia.com> Signed-off-by: Apurv Kumaria <akumaria@nvidia.com> Co-authored-by: Apurv Kumaria <akumaria@nvidia.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Carlos Villela <cvillela@nvidia.com>
`channels add whatsapp` required the operator to know `WHATSAPP_MODE` before running it, and choosing `bot` without an allowlist produced a sandbox nobody could reach: the bridge rejects every sender while bot mode also stops replies to the paired account's own self-chat. Ask for the mode during enrollment instead, and derive `WHATSAPP_DM_POLICY` from the selected mode and the allowlist. `bot` without an allowlist now renders `pairing`, so an unknown sender receives a pairing code the operator approves with `hermes pairing approve whatsapp <code>`. `bot` with an allowlist renders `allowlist`. `self-chat` drops the key, because the bridge rejects every non-self message before it reads a policy. Rendering that key is what keeps bot mode reachable: the Node bridge defaults it to `open` while the Python adapter defaults it to `pairing`, so leaving it unset left the bridge enforcing an empty allowlist. The prompt is limited to Hermes. The OpenClaw fragment carries no sender policy and OpenClaw never reads the Hermes env, so an OpenClaw operator would answer a question nothing consumes. Signed-off-by: Hung Le <hple@nvidia.com>
Signed-off-by: Hung Le <hple@nvidia.com>
Summary
The WhatsApp channel manifest rendered
WHATSAPP_MODE=botas a constant, so neither Hermes mode was reachable and a freshly onboarded sandbox could not deliver a message.channels add whatsappnow asks the operator which senders the sandbox answers, and derivesWHATSAPP_DM_POLICYfrom that answer and the allowlist. A blank answer keepsself-chat, so a paired sandbox works with nothing else configured. Answeringbotwithout an allowlist renderspairing, so an unknown sender receives a pairing code the operator approves rather than being rejected outright. OpenClaw is unchanged.Related Issue
Addresses part of #8312
Changes
src/lib/messaging/channels/whatsapp/manifest.ts— add amodeinput (envKey: WHATSAPP_MODE,statePath: whatsappConfig.mode,validValues: ["self-chat", "bot"],defaultValue: "self-chat") with an operator prompt; wire the sharedcommon.configPromptenroll hook limited toagents: ["hermes"]; renderWHATSAPP_MODE={{whatsappConfig.mode}}andWHATSAPP_DM_POLICY={{whatsappConfig.dmPolicy}}instead of the constant.src/lib/messaging/channels/whatsapp/template-resolver.ts— resolvewhatsappConfig.mode, following thetelegramConfig.groupPolicyprecedent, and addwhatsappConfig.dmPolicy, which reads the mode as well as the allowlist. Remove the unreferencedallowedIds.whatsapp.dmPolicycase it replaces.src/lib/messaging/channels/whatsapp/rendered-config-parser.ts— list the mode beside the allowlist sochannels statusshows both. The mode decides whether an empty allowlist matters, and the existingWHATSAPP_ALLOWED_IDS: not setline carried no such context.docs/manage-sandboxes/set-up-whatsapp.mdx— rewrite the Hermes-only "Reply to Other Senders" section around the prompt, document approval and revocation, and qualify the existing allowlist sentence, which was written whenbotwas the only mode..envassertions ingenerate-hermes-config.test.ts, and the live E2E probe now requires the derived policy.Why the DM policy is rendered rather than left unset
The two halves of the Hermes WhatsApp adapter disagree on this key's default: the Node bridge reads
process.env.WHATSAPP_DM_POLICY || 'open'while the Python adapter readsos.getenv("WHATSAPP_DM_POLICY", "pairing"). Leaving the key unset therefore left the bridge enforcing an empty allowlist and rejecting every sender, whilebotmode also stops replies to the paired account's own self-chat — a sandbox nobody could reach. Rendering the value is what makesbotusable without an allowlist. This compensates for an upstream default divergence on the pinned runtime; a Hermes-side report is planned separately.The derivation is:
WHATSAPP_DM_POLICYself-chatbotallowlistbotpairingScope
The prompt is limited to Hermes through the hook's
agentslist.WHATSAPP_MODEandWHATSAPP_DM_POLICYare rendered into the Hermes env, and the OpenClaw fragment carries no sender policy, so an OpenClaw operator would answer a question nothing consumes. WhatsApp and WeChat are the only two channels whose OpenClaw fragment omitsallowFromanddmPolicy, andgit log -S"allowFrom"on this manifest is empty, so that omission is longstanding rather than lost here.Two behavior changes worth calling out:
botrendersself-chatafter its next rebuild. Its persisted plan has nomodeinput, so the manifest default applies. A migration rule keyed on "allowlist present implies bot" was considered and rejected: the documented way to select WhatsApp non-interactively is to exportWHATSAPP_ALLOWED_IDS, so that rule would silently place new sandboxes into the mode this issue is about. Affected operators re-runchannels add whatsappand answerbot.WHATSAPP_MODEstill selects the channel non-interactively for either agent, including OpenClaw, where the mode is not rendered.WHATSAPP_ALLOWED_IDSalready behaves this way; inputs cannot be scoped per agent today (agents:exists on hooks, not onChannelInputBaseSpec). An operator who exports the variable also skips the prompt, which keeps the previously documented non-interactive path working.Type of Change
Quality Gates
WHATSAPP_DM_POLICY, so re-review is requested.Documentation Writer Review
docs-updateddocs/manage-sandboxes/set-up-whatsapp.mdx. The review returned no blocking findings and ten non-blocking suggestions; seven writing suggestions were applied, the wildcard-accuracy question was resolved by running the bridge's ownparseAllowedUsers/matchesAllowedUseragainst*on a live sandbox, and the companion-page suggestion was left out because the reviewer scoped it outside this PR.npm run docspassed with 0 errors and 2 repository warnings. Two later corrections came from manual testing and are included: self-chat also issues a pairing code for the operator's own first message, and answering the in-sandboxhermes whatsappallowlist prompt writes.envoutside the sealed configuration.DGX Station Hardware Evidence
Verification
Signed-off-by:line and every commit appears asVerifiedin GitHubpre-commit,commit-msg, andpre-pushhooks passed, ornpm run validate:prpassed after refreshingorigin/mainwhen hooks were skipped or unavailablenpx vitest run --project cli src/lib/messaging/channels/whatsapp/template-resolver.test.ts src/lib/messaging/hooks/common/config-prompt.test.ts src/lib/messaging/compiler/manifest-compiler.test.ts— 50 passed;npx vitest run --project integration test/generate-hermes-config.test.ts— 55 passed;npx vitest run --project cli src/lib/onboard/messaging-channel-setup.test.ts src/lib/actions/sandbox/policy-channel-remove-flow.test.ts— 47 passed. Adding an enroll hook to this manifest broke four assertions in those last two files, which were updated: one pinned the previous "onboarding asks nothing" contract, and three built a planner without a hook registry.npm testfor broad runtime/test-harness changes;npm run checkfor repo-wide validation/coverage changes — command/result: not applicable; this change is scoped to one channel manifest, the shared config-prompt hook's manifest wiring, and their tests.npm run checks:repositorypassed.npx vitest run --project cliafternpm run catalog:compilereports 1158 of 1161 files passing; the three failures need Python 3.11 fortomlliband are unrelated.npx vitest run --project integrationafternpm run build:clireports 720 of 734 files passing; the eight failures need Python 3.11 fortyping.NotRequired, a locally absentqrcodepackage, or an external terminology tool, and none reference this change.npm run docsbuilds without warnings (doc changes only) —Found 0 errors and 2 warnings. Fern does not enumerate the two warnings in the build output; they are not attributable to this page from the build alone.Live verification
Built from this branch on a CPU host (Hermes v0.19.0, OpenShell 0.0.99, docker driver) and exercised through the normal commands. Both modes were onboarded interactively so the prompt was answered rather than exported.
The prompt, and the default answer:
Pressing Enter, on sandbox
wa-self:The policy line is absent, which is the intended result for
self-chat. Hermes read the mode back asMode: personal number (self-chat), QR pairing completed, and aftergateway restartthe bridge ran with--mode self-chatand the agent replied to a message the paired account sent to itself.Answering
bot, on sandboxwa-bot:Hermes read that mode back as
Mode: separate bot number. On the same sandbox, the in-sandboxhermes whatsappwizard was then used to write an allowlist, which appendedWHATSAPP_ALLOWED_USERS=*outside the configuration NemoClaw seals; the nextgateway restartrefused to relaunch on the integrity guard.rebuild --yesrestored the rendered configuration and dropped the hand-written line, leaving exactly the three lines above. That confirms the rendered value is authoritative over an out-of-band edit, and the documented warning about that wizard prompt comes from this run.An inbound message was not exchanged in
botmode on this host: the bridge did not spawn after that rebuild. The same code spawns the bridge inself-chaton the same branch, so the failure is downstream of the rendered value this change decides. Two known rebuild-time defects and the session-path behavior in #8229 are candidates; the diagnosis is separate from this PR.Signed-off-by: Hung Le hple@nvidia.com