diff --git a/agents/hermes/Dockerfile b/agents/hermes/Dockerfile index f13fdc5b000..dd8fd896ed4 100644 --- a/agents/hermes/Dockerfile +++ b/agents/hermes/Dockerfile @@ -6,7 +6,7 @@ # Layers PR-specific code (plugin, config, startup script) on top of the # pre-built Hermes base image. Mirrors the OpenClaw Dockerfile structure. -ARG BASE_IMAGE=ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@sha256:57c091ab9b31c924eac0050e66c834c37df875154a254964302a31b119b50b96 +ARG BASE_IMAGE=ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@sha256:3d54b928baef9df403227e846f73079d13ca8424a27cd5268ca97bac3f030b27 ARG NEMOCLAW_CORPORATE_CA_B64= ARG NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=0 diff --git a/agents/hermes/whatsapp-proxy.patch b/agents/hermes/whatsapp-proxy.patch index c154223e380..5c53947f6af 100644 --- a/agents/hermes/whatsapp-proxy.patch +++ b/agents/hermes/whatsapp-proxy.patch @@ -2,8 +2,22 @@ # SPDX-License-Identifier: Apache-2.0 # # Routes the pinned Hermes v2026.7.20 WhatsApp bridge through the OpenShell -# proxy. Remove this patch when the minimum supported Hermes release passes -# an HTTPS proxy agent to Baileys natively. +# proxy and keeps dashboard pairing state in the gateway's session directory. +# Remove the web_server.py hunk when the minimum supported Hermes release stores +# Dashboard pairing state in the gateway session directory natively. +# Remove the scripts/whatsapp-bridge hunks when that release routes the WhatsApp +# bridge through HTTPS_PROXY natively. +diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py +index d0c78a6b3..5f6d801b5 100644 +--- a/hermes_cli/web_server.py ++++ b/hermes_cli/web_server.py +@@ -8110,5 +8110,3 @@ def _normalize_whatsapp_allowed_users(value: Any) -> str: + def _whatsapp_session_path() -> Path: +- from hermes_constants import get_hermes_dir +- +- return get_hermes_dir("platforms/whatsapp/session", "whatsapp/session") ++ return Path("/sandbox/.hermes/platforms/whatsapp/session") + diff --git a/scripts/whatsapp-bridge/bridge.js b/scripts/whatsapp-bridge/bridge.js index 4b5733d16..22f1ec3e1 100644 --- a/scripts/whatsapp-bridge/bridge.js diff --git a/docs/manage-sandboxes/set-up-whatsapp.mdx b/docs/manage-sandboxes/set-up-whatsapp.mdx index 8d9954da76e..ba91730b594 100644 --- a/docs/manage-sandboxes/set-up-whatsapp.mdx +++ b/docs/manage-sandboxes/set-up-whatsapp.mdx @@ -33,25 +33,22 @@ NemoClaw validates the gateway URL before pairing and renders the WhatsApp QR co If pairing exits with a gateway close such as `1008`, rerun the login command once, then check `$$nemoclaw channels status --channel whatsapp` to diagnose the gateway and session path separately from QR rendering. -Hermes can pair from the Dashboard profile while the gateway reads the default `platforms/whatsapp/session` path. -After scanning a dashboard QR code, run `$$nemoclaw channels status --channel whatsapp`. -If the report says `dashboard-home has WhatsApp credentials, but the Hermes gateway session path is empty`, point the gateway at the dashboard session with the supported config command: +Hermes dashboard pairing and the gateway share `/sandbox/.hermes/platforms/whatsapp/session`. +After scanning a dashboard QR code, run `$$nemoclaw channels status --channel whatsapp` to confirm that the shared session is available to the gateway. + +If `channels status` reports credentials only under the legacy `profiles/dashboard-home` path, the sandbox has credentials from an image that wrote dashboard sessions to that path. +Rebuilding and pairing again writes new credentials to the shared path, but it leaves the legacy credentials in durable state, and sandbox startup treats that copy as migration input. +Clear every WhatsApp session path first with the supported removal command, then re-add the channel: ```bash -$$nemoclaw shields down --reason "repair Hermes WhatsApp session path" -$$nemoclaw config set --key platforms.whatsapp.extra.session_path --value /sandbox/.hermes/profiles/dashboard-home/platforms/whatsapp/session --restart --config-accept-new-path -$$nemoclaw shields up +$$nemoclaw channels remove whatsapp +$$nemoclaw channels add whatsapp ``` -Keep Shields down until `config set` finishes, then restore the restrictive posture with `shields up`. -The `--config-accept-new-path` flag lets the command write the `platforms.whatsapp.extra.session_path` configuration key when the generated config does not already contain it. -Dashboard pairing writes session credentials under `profiles/dashboard-home`; this bounded compatibility repair redirects the gateway to that session path while it still reads the default path otherwise. -The repair stays documented until Hermes dashboard pairing state and gateway startup converge on one shared session path; NemoClaw can redirect the configured path but does not rewrite Hermes pairing state. -Before removing the override, run `$$nemoclaw channels status --channel whatsapp` after dashboard pairing and gateway startup and confirm the report no longer shows a dashboard-home session with an empty gateway session path. -Remove the override only after Hermes uses one shared WhatsApp session path for dashboard pairing and gateway startup. -Do not edit `/sandbox/.hermes/.env` or `/sandbox/.hermes/config.yaml` by hand. -Manual edits can cause the Hermes config-integrity guard to reject restart with `hash-mismatch`. -Supervisor quarantine happens after repeated relaunch failures or other integrity failures. +`channels remove` clears `/sandbox/.hermes/platforms/whatsapp`, the dashboard profile session at `/sandbox/.hermes/profiles/dashboard-home/platforms/whatsapp/session`, and the legacy migration source at `/sandbox/.hermes/dashboard-home/platforms/whatsapp/session`. +The sandbox must be running for that cleanup to succeed; `channels remove` refuses to proceed when it cannot reach the sandbox. +Pair again from the dashboard so the credentials are written to `/sandbox/.hermes/platforms/whatsapp/session`. +Rerun `$$nemoclaw channels status --channel whatsapp` and confirm that the gateway session path holds the credentials and that the report no longer lists a `profiles/dashboard-home` session. ## Protect Paired Session State @@ -60,9 +57,13 @@ Supervisor quarantine happens after repeated relaunch failures or other integrit The sandbox stores mutable session credentials in durable `whatsapp` state. -The sandbox stores mutable session credentials in durable `platforms/whatsapp` state for the gateway and `profiles/dashboard-home/platforms/whatsapp` state for Dashboard pairing. +The sandbox stores mutable session credentials in durable `platforms/whatsapp` state for both dashboard pairing and the gateway. The credentials survive rebuilds without re-pairing. + +A sandbox that also holds legacy `profiles/dashboard-home` or `dashboard-home` credentials keeps both copies across a rebuild, because both paths are part of the durable state that a rebuild restores. +Run `$$nemoclaw channels remove whatsapp` to clear every WhatsApp session path, then re-add the channel and pair again. + 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. NemoClaw cannot detect cross-sandbox WhatsApp conflicts the way it does for token-based channels. diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index 00f1b62364f..8f88f428246 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -2494,7 +2494,9 @@ For an OpenClaw WhatsApp sandbox, `--channel whatsapp` probes the sandbox to sep A paired channel with no observed inbound delivery exits non-zero with verdict `idle` so an unhealthy bridge cannot pass as healthy. The detailed WhatsApp probe stays focused on QR/session runtime diagnostics and does not include rendered-config comparison lines. For a Hermes WhatsApp sandbox, `--channel whatsapp` probes only whether the default gateway session path and the `profiles/dashboard-home` session path contain `creds.json`. -If the dashboard path has credentials and the gateway path is empty, the report prints the supported `config set` command for the `platforms.whatsapp.extra.session_path` configuration key, including the new-configuration-key opt-in when generated configs do not already contain that key. +If the dashboard path has credentials and the gateway path is empty, the report tells you to run `$$nemoclaw channels remove whatsapp` and then `$$nemoclaw channels add whatsapp`, because a rebuild restores the legacy session instead of dropping it. +Pair again from the dashboard so credentials use `/sandbox/.hermes/platforms/whatsapp/session`. +Rerun `$$nemoclaw channels status --channel whatsapp`. NemoClaw does not treat a Hermes session file as live inbound-health evidence. For Telegram, `--channel telegram` probes the sandbox to report the gateway process, Bot API reachability, and inbound delivery alongside the config comparison. diff --git a/docs/security/hermes-0.19.0-dependency-review.md b/docs/security/hermes-0.19.0-dependency-review.md index 3fff63507cd..7d666a6fa80 100644 --- a/docs/security/hermes-0.19.0-dependency-review.md +++ b/docs/security/hermes-0.19.0-dependency-review.md @@ -29,13 +29,11 @@ The Hermes sandbox image build fails if either installed dependency has another The base image also replaces the published `python-multipart==0.0.27` lock resolution with the hash-verified and attested `python-multipart==0.0.32`. The base image overlays checksum-pinned Node.js `24.18.1` archives for both supported architectures and installs exact uv `0.11.33`; build-time assertions reject version drift before Hermes is installed. -The `BASE_IMAGE` argument in `agents/hermes/Dockerfile` pins patched multi-platform Open Container Initiative (OCI) index `sha256:57c091ab9b31c924eac0050e66c834c37df875154a254964302a31b119b50b96`. -GitHub Actions workflow `.github/workflows/base-image.yaml` run `31006872948`, attempt 1, built and published that base image index from source commit `bd668121e918e7b1dda13062bed728f18150360e` after a push to `main`. -The `linux/amd64` and `linux/arm64` manifests point to image configurations whose `org.opencontainers.image.source` labels identify repository `NVIDIA/NemoClaw`. -The `org.opencontainers.image.revision` labels identify source commit `bd668121e918e7b1dda13062bed728f18150360e`. -Each platform base image has Supply-chain Levels for Software Artifacts (SLSA) provenance tied to run `31006872948`, attempt 1, and that source commit. -The OCI index has no software bill of materials (SBOM) attestation. -GitHub Actions must build the Hermes sandbox image from that base image, and the required live end-to-end (E2E) checks must pass before approval. +The `BASE_IMAGE` argument in `agents/hermes/Dockerfile` pins the patched multi-platform Open Container Initiative (OCI) index `sha256:3d54b928baef9df403227e846f73079d13ca8424a27cd5268ca97bac3f030b27`. +GitHub Actions workflow `.github/workflows/base-image.yaml` run `31031662054`, attempt 1, built and published that replacement image from source commit `a7a7f3e470a75c404d316d2054445e16bb63b48c`, including the exact-source dashboard WhatsApp session-path patch. +It supersedes index `sha256:57c091ab9b31c924eac0050e66c834c37df875154a254964302a31b119b50b96`, which workflow run `31006872948`, attempt 1, published from source commit `bd668121e918e7b1dda13062bed728f18150360e` after a push to `main`. +The selected image's `linux/amd64` and `linux/arm64` base-image builds passed the exact-source patch guard, locked bridge install, bridge-to-Baileys option assertions, and controlled-proxy WebSocket `CONNECT` regression. +GitHub Actions must build the Hermes sandbox image from the selected base image, and the required live end-to-end (E2E) checks must pass before approval. ## Reviewed identities @@ -248,21 +246,41 @@ The `BASE_IMAGE` argument in `agents/hermes/Dockerfile` pins the following publi | Repository | `NVIDIA/NemoClaw` | | Workflow | `Images / Base Images` | | Workflow path | `.github/workflows/base-image.yaml` | -| Trigger | Push to `main` | -| Producer run | `31006872948`, attempt 1, completed successfully | -| Source commit | `bd668121e918e7b1dda13062bed728f18150360e` | -| OCI index | `sha256:57c091ab9b31c924eac0050e66c834c37df875154a254964302a31b119b50b96` | +| Trigger | `workflow_dispatch` from `fix/hermes-whatsapp-session-path` | +| Producer run | `31031662054`, attempt 1, completed successfully | +| Source commit | `a7a7f3e470a75c404d316d2054445e16bb63b48c` | +| OCI index | `sha256:3d54b928baef9df403227e846f73079d13ca8424a27cd5268ca97bac3f030b27` | + +The selected index resolves to these platform manifests and image-configuration labels: + +| Platform | Child manifest | `org.opencontainers.image.source` | `org.opencontainers.image.revision` | +| --- | --- | --- | --- | +| `linux/amd64` | `sha256:63b5a1944a4613f90fa8f9f92f6a3ccb2ceac6df23bcefad0282ed4167e6c09c` | `https://github.com/NVIDIA/NemoClaw` | `a7a7f3e470a75c404d316d2054445e16bb63b48c` | +| `linux/arm64` | `sha256:f30b5c2529bbb5c06de38d0c1a7018c27d6c08d5a917503fa354300f12e5568c` | `https://github.com/NVIDIA/NemoClaw` | `a7a7f3e470a75c404d316d2054445e16bb63b48c` | + +Each child manifest has the following per-platform Supply-chain Levels for Software Artifacts (SLSA) provenance: + +| Platform | Attestation manifest | SLSA provenance layer | Builder ID | +| --- | --- | --- | --- | +| `linux/amd64` | `sha256:6797db760d6c894b624a4e40614b33a611ca02ed772323dd0b097f8ca782beab` | `sha256:f95cea632fa5006fc78c193f7e550d869423818e8ba3ab2b48a6518746a7a43c` | `https://github.com/NVIDIA/NemoClaw/actions/runs/31031662054/attempts/1` | +| `linux/arm64` | `sha256:3237354f9fe66b71b2e5e2fb9f4c1983c9c9151c53ecfdf7cf3ecbfee4029944` | `sha256:1ca19a6f81b3709df3e05fc39fc40a0425ce6473036909be61fa274569e858c5` | `https://github.com/NVIDIA/NemoClaw/actions/runs/31031662054/attempts/1` | + +Both in-toto layers use predicate type `https://slsa.dev/provenance/v1` and bind source `https://github.com/NVIDIA/NemoClaw` to revision `a7a7f3e470a75c404d316d2054445e16bb63b48c`. +The selected index has no index-level attestation manifest. +Each platform attestation manifest contains only its SLSA provenance layer and has no software bill of materials (SBOM) layer. + +The selected build includes the exact-source dashboard WhatsApp session-path patch. Its amd64 and arm64 base-image builds passed the exact-source patch guard, locked bridge install, bridge-to-Baileys option assertions, and controlled-proxy WebSocket `CONNECT` regression. -| Architecture | Child manifest | +The replacement follows the security-refreshed multi-platform index published by run `31006872948`, attempt 1, from source commit `bd668121e918e7b1dda13062bed728f18150360e`: + +| Evidence | Value | | --- | --- | -| amd64 | `sha256:cf6e95640faac8e5099cc9d267a6eb9b1f9192abbfcc9552a81a8ae22b4a47bb` | -| arm64 | `sha256:92e7c982bc5106f4c3f551032418fb72375b4d37bff50e21baa3d0e861d7519e` | +| OCI index | `sha256:57c091ab9b31c924eac0050e66c834c37df875154a254964302a31b119b50b96` | +| amd64 child manifest | `sha256:cf6e95640faac8e5099cc9d267a6eb9b1f9192abbfcc9552a81a8ae22b4a47bb` | +| arm64 child manifest | `sha256:92e7c982bc5106f4c3f551032418fb72375b4d37bff50e21baa3d0e861d7519e` | -Each platform manifest points to an image configuration whose `org.opencontainers.image.source` label identifies repository `NVIDIA/NemoClaw` and whose `org.opencontainers.image.revision` label identifies source commit `bd668121e918e7b1dda13062bed728f18150360e`. -Each platform base image also has SLSA provenance whose builder ID is `https://github.com/NVIDIA/NemoClaw/actions/runs/31006872948/attempts/1` and whose source is that repository and commit. -The `linux/amd64` and `linux/arm64` base image histories each contain installed-version assertions for `aiohttp==3.14.3` and `cryptography==50.0.0`. -The base image OCI index has no SBOM attestation. -This absence is separate from the incomplete SBOM in the Hermes source archive. +That security-refreshed image's platform manifests identify repository `NVIDIA/NemoClaw` and source commit `bd668121e918e7b1dda13062bed728f18150360e`; its platform histories contain installed-version assertions for `aiohttp==3.14.3` and `cryptography==50.0.0`. +The `57c091ab` OCI index has no SBOM attestation. This absence is separate from the incomplete SBOM in the Hermes source archive. The Dockerfile change replaces the base image built from source commit `340c47857596e7cc347541a0b32fe9e24f201bcd` and identified by OCI index `sha256:956c3d0c812ee6caa56f3b6e307819925d920604adcf73c4a9e6229788967634`: @@ -312,7 +330,7 @@ Each reviewed commit in the following table is an ancestor of `bd668121e918e7b1d | `HERMES-9` | High | Pin and test | The selected Python delta adds no advisory regression, and the affected multipart parser is replaced with attested `0.0.32` plus hash and runtime probes. | | `HERMES-10` | High | Pin and test | The exact-source patch updates Hermes metadata and its frozen lock together, selects `aiohttp==3.14.3`, `cryptography==50.0.0`, `mcp==1.28.1`, `Pillow==12.3.0`, `starlette==1.3.1`, and `tornado==6.5.7`, and fails the base image build on dependency inconsistency or installed-version drift. The `agents/hermes/Dockerfile` build checks `aiohttp==3.14.3` and `cryptography==50.0.0` in the Hermes sandbox image after messaging package installation. The check runs when `NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION` is `0` or `1`. The base image separately checksum-pins Node.js `24.18.1` and checks uv `0.11.33`. | | `HERMES-11` | High | Migrate, test, and runtime-proof | Root npm audit reports zero production findings, and the WhatsApp bridge removes the Baileys RC9 critical, high, and medium advisory entries. Both architectures still require native bridge and message-path evidence. | -| `HERMES-12` | High | Pin and runtime-proof | The `BASE_IMAGE` argument in `agents/hermes/Dockerfile` pins base image OCI index `sha256:57c091ab9b31c924eac0050e66c834c37df875154a254964302a31b119b50b96`. GitHub Actions workflow `.github/workflows/base-image.yaml` run `31006872948`, attempt 1, published that index from source commit `bd668121e918e7b1dda13062bed728f18150360e` after a push to `main`. The `linux/amd64` and `linux/arm64` image configurations identify repository `NVIDIA/NemoClaw` and source commit `bd668121e918e7b1dda13062bed728f18150360e`. Both platform base images have SLSA provenance tied to that workflow run and source commit. The `linux/amd64` and `linux/arm64` base image histories each check `aiohttp==3.14.3` and `cryptography==50.0.0`. The OCI index has no SBOM attestation. Source commit `bd668121e918e7b1dda13062bed728f18150360e` descends from WhatsApp commit `3f3eb6139e089c24397d6a499a10fcde4bdc84da` and base image input commits `265c18f856263c20cd4a3e89ca189fc102dbc95b`, `15069f9262d52b74d8916b7a0d7969a9ae4d3ee1`, `efc34999dd185c2e14ff5dc6997d75db26537f3a`, and `b6df720eebf5a01928dbed2f588691ba8de794f8`. `HERMES-22` requires live WhatsApp evidence from the Hermes sandbox image. | +| `HERMES-12` | High | Pin and runtime-proof | Trusted workflow run `30779271312`, attempt 1, built source commit `340c47857596e7cc347541a0b32fe9e24f201bcd` and published OCI index `sha256:956c3d0c812ee6caa56f3b6e307819925d920604adcf73c4a9e6229788967634`. Run `31006872948`, attempt 1, published security-refreshed index `sha256:57c091ab9b31c924eac0050e66c834c37df875154a254964302a31b119b50b96` from source commit `bd668121e918e7b1dda13062bed728f18150360e`, whose platform histories check `aiohttp==3.14.3` and `cryptography==50.0.0`. Trusted workflow run `31031662054`, attempt 1, rebuilt source commit `a7a7f3e470a75c404d316d2054445e16bb63b48c` with the exact-source dashboard WhatsApp session-path patch and published replacement OCI index `sha256:3d54b928baef9df403227e846f73079d13ca8424a27cd5268ca97bac3f030b27`. The final Dockerfile pins that replacement index. The amd64 and arm64 base-image builds in the reviewed runs passed the exact-source patch guard, locked bridge install, bridge-to-Baileys option assertions, and the controlled-proxy WebSocket `CONNECT` regression; the live final-image WhatsApp evidence is recorded under `HERMES-22`. | | `HERMES-13` | Medium | Document bounded residual | Static `state_files` entries online-back up the default profile only. Cron or Discord ledgers created by a process launched under `profiles/` remain in the raw `profiles` tar capture and can be inconsistent during a concurrent snapshot. Dynamic profile-local SQLite discovery is generic snapshot work outside this upgrade PR. | | `HERMES-14` | High | Migrate and test | The browser evaluation denylist changed from default-on to opt-in. Generated configuration explicitly writes `browser.restrict_evaluate: true`, including when managed browser-gateway settings are merged, so the upgrade does not broaden page-context access. | | `HERMES-15` | Medium | Migrate and test | The omitted gateway session-reset policy changed from bounded daily and idle expiry to no automatic reset. Generated configuration explicitly writes the complete outgoing reset and notification policy to preserve the retention bound without inheriting mutable dependency defaults. | @@ -322,7 +340,7 @@ Each reviewed commit in the following table is an ancestor of `bd668121e918e7b1d | `HERMES-19` | High | Migrate and test | The dashboard has an isolated `HERMES_HOME`, so its allowlisted routing and policy mirror is a startup security boundary. A missing gateway config remains a benign cold-start no-op, while malformed, non-mapping, unreadable, or routing-free source config and invalid existing dashboard config fail startup without changing stale dashboard bytes. Sanitized errors never include raw PyYAML parser context or credential-bearing source lines. | | `HERMES-20` | High | Retarget, guard, test, and runtime-proof | Base SHA `fa96c91f` adds a Hermes 0.18 gateway-runtime-metadata patch whose central helper shape does not match Hermes 0.19. The retargeted exact-source guard preserves `_get_process_hermes_home()` while moving the managed default gateway's central PID, lock, and status helpers below `runtime`, hash-binds the patcher, and adds unit and Hermes sandbox image probes. The managed-gateway restart E2E remains the PR SHA runtime gate. | | `HERMES-21` | Medium | Document inherited bounded residual | The base workaround does not retarget direct upstream `--replace` cleanup, planned-stop/takeover markers, named-profile and multiplexer readers, service/boot/web/Windows consumers, or upstream backup and Docker paths. With Shields up, those direct paths can fail or observe stale state, but the same limitation exists on base SHA `fa96c91f`; the 0.19 selector retarget adds no regression to NemoClaw's supported host-managed default-gateway lifecycle. A complete relocation needs separate exact-source patches and runtime proof for every explicit consumer. | -| `HERMES-22` | High | Patch, pin, test, and runtime-proof | Issue `#8087` showed that the Hermes WhatsApp WebSocket ignored the injected `HTTPS_PROXY`, attempted direct DNS resolution, and failed before OpenShell produced an Open Cybersecurity Schema Framework (OCSF) record. NemoClaw exact-source patches both Baileys proxy fields, locks the added proxy dependency graph, and fails the base image build when the patch drifts, a bridge-level `makeWASocket` mock does not receive the same proxy agent as `agent` and `fetchAgent`, or the pinned Baileys WebSocket transport does not send a `CONNECT web.whatsapp.com:443` request to a controlled HTTPS proxy. The mock also proves that both options remain unset without `HTTPS_PROXY`. Live Hermes WhatsApp E2E evidence for QR pairing, connected status, and audited WebSocket traffic through the OpenShell proxy remains a merge gate. | +| `HERMES-22` | High | Patch, pin, test, and runtime-proof | Issue `#8087` showed that the Hermes WhatsApp WebSocket ignored the injected `HTTPS_PROXY`, attempted direct DNS resolution, and failed before OpenShell produced an Open Cybersecurity Schema Framework (OCSF) record. NemoClaw exact-source patches both Baileys proxy fields, locks the added proxy dependency graph, and fails the base image build when the patch drifts, a bridge-level `makeWASocket` mock does not receive the same proxy agent as `agent` and `fetchAgent`, or the pinned Baileys WebSocket transport does not send a `CONNECT web.whatsapp.com:443` request to a controlled HTTPS proxy. The mock also proves that both options remain unset without `HTTPS_PROXY`. Live Hermes WhatsApp evidence was captured manually on a final image built from this branch: dashboard QR pairing wrote credentials to `/sandbox/.hermes/platforms/whatsapp/session`, the bridge reported `{"status":"connected"}`, an inbound message from an allowlisted sender received an agent reply, and the OpenShell proxy audit admitted every WhatsApp flow under `policy:whatsapp`. Pull request `#8229` records that run and its reproduction steps. No target in this repository pairs a live WhatsApp account, so the trusted manual pull request E2E run remains the merge gate for that evidence. | Unresolved upgrade-created high-impact concerns: `0`. One Medium upgrade-created instance of the pre-existing named-profile raw-capture limitation and one inherited Medium direct-runtime-consumer limitation remain explicitly accepted for this upgrade scope. @@ -350,11 +368,11 @@ The review records the following source and test evidence. The review records the following publication and registry evidence. - Hermes CI run `29768400292`, PyPI publication run `29768427462`, and Docker publication run `29768440304` completed successfully. -- GitHub Actions workflow `.github/workflows/base-image.yaml` run `31006872948`, attempt 1, published the patched `linux/amd64` and `linux/arm64` base images. +- GitHub Actions workflow `.github/workflows/base-image.yaml` run `31031662054`, attempt 1, published the selected patched `linux/amd64` and `linux/arm64` base images; run `31006872948`, attempt 1, published the preceding security-refreshed index. - PyPI Trusted Publisher attestations bind both `hermes-agent==0.19.0` artifacts to source commit `3ef6bbd201263d354fd83ec55b3c306ded2eb72a`. - The npm registry-integrity check matches the `hermes-agent==0.19.0` cross-check value recorded in this review. -- OCI inspection records the immutable index, image-configuration source and revision labels, SLSA provenance, and build histories. -- The OCI index has no SBOM attestation. +- OCI inspection records the immutable index, both child manifests, image-configuration source and revision labels, per-platform SLSA provenance, and build histories. +- The selected index has no index-level attestation manifest, and neither platform attestation manifest contains an SBOM layer. Before merge, these checks must pass: diff --git a/src/lib/agent/base-image-hermes.test.ts b/src/lib/agent/base-image-hermes.test.ts index dfcb3fc4cfc..fa0e5a05b2b 100644 --- a/src/lib/agent/base-image-hermes.test.ts +++ b/src/lib/agent/base-image-hermes.test.ts @@ -49,7 +49,7 @@ describe("agent base image provisioning", () => { ); expect(trackedRef).not.toBeNull(); expect(trackedRef?.[1]).toBe( - "ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@sha256:57c091ab9b31c924eac0050e66c834c37df875154a254964302a31b119b50b96", + "ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@sha256:3d54b928baef9df403227e846f73079d13ca8424a27cd5268ca97bac3f030b27", ); const messagingInstallIndex = dockerfile.indexOf("RUN unset SSL_CERT_FILE REQUESTS_CA_BUNDLE"); diff --git a/src/lib/messaging/channels/whatsapp/hooks/status-health-eval.test.ts b/src/lib/messaging/channels/whatsapp/hooks/status-health-eval.test.ts index 09da557c522..f29844870c3 100644 --- a/src/lib/messaging/channels/whatsapp/hooks/status-health-eval.test.ts +++ b/src/lib/messaging/channels/whatsapp/hooks/status-health-eval.test.ts @@ -77,7 +77,7 @@ describe("evaluateWhatsappDiagnostics", () => { expect(report.hints.join(" ")).toMatch(/hermes whatsapp/); }); - it("reports the Hermes dashboard-only session path split with the supported repair", () => { + it("guides legacy Hermes dashboard sessions through channel removal and re-pairing (#8184)", () => { const report = evaluateWhatsappDiagnostics( baseInput({ agent: "hermes", @@ -89,15 +89,18 @@ describe("evaluateWhatsappDiagnostics", () => { }), ); const session = report.signals.find((s) => s.label === "Session location"); + const pairing = report.signals.find((s) => s.label === "Pairing / session"); + const hints = report.hints.join(" "); expect(report.verdict).toBe("unpaired"); expect(session?.severity).toBe("warn"); expect(session?.detail).toMatch(/dashboard-home has WhatsApp credentials/); - expect(session?.hint).toContain("platforms.whatsapp.extra.session_path"); - expect(session?.hint).toContain("--config-accept-new-path"); - expect(report.hints.join(" ")).toContain( - "/sandbox/.hermes/profiles/dashboard-home/platforms/whatsapp/session", - ); - expect(report.hints.join(" ")).toContain("--config-accept-new-path"); + expect(pairing?.hint).toContain("`nemoclaw channels remove whatsapp`"); + expect(session?.hint).toContain("`nemoclaw channels add whatsapp`"); + expect(session?.hint).toMatch(/Pair again from the dashboard/); + expect(hints).toContain("/sandbox/.hermes/platforms/whatsapp/session"); + expect(hints).toContain("`nemoclaw channels status --channel whatsapp`"); + expect(hints).not.toContain("platforms.whatsapp.extra.session_path"); + expect(hints).not.toContain("profiles/dashboard-home/platforms/whatsapp/session"); }); it("keeps Hermes gateway session file evidence out of the live-health verdict", () => { diff --git a/src/lib/messaging/channels/whatsapp/hooks/status-health-eval.ts b/src/lib/messaging/channels/whatsapp/hooks/status-health-eval.ts index d0a92183e68..b16a2afdf5b 100644 --- a/src/lib/messaging/channels/whatsapp/hooks/status-health-eval.ts +++ b/src/lib/messaging/channels/whatsapp/hooks/status-health-eval.ts @@ -130,6 +130,12 @@ function isParseableTimestamp(value: string | null): value is string { return value !== null && Number.isFinite(Date.parse(value)); } +// A rebuild restores the legacy dashboard session instead of dropping it, so +// re-pairing alone leaves the sandbox holding two credential sets. Send the +// operator through `channels remove`, which clears every WhatsApp session path. +const HERMES_LEGACY_DASHBOARD_SESSION_HINT = + "Run `nemoclaw channels remove whatsapp` to clear every WhatsApp session path, then `nemoclaw channels add whatsapp`. Pair again from the dashboard so credentials are stored in `/sandbox/.hermes/platforms/whatsapp/session`. Rerun `nemoclaw channels status --channel whatsapp`."; + function pairingSignal(input: WhatsappProbeInput): DiagnosticSignal { if (!input.probeReachable) { return { @@ -154,7 +160,7 @@ function pairingSignal(input: WhatsappProbeInput): DiagnosticSignal { severity: "warn", detail: "Hermes gateway session path has no WhatsApp credentials", hint: dashboardOnly - ? "point the Hermes WhatsApp session_path at the dashboard-home session path, then restart the gateway" + ? HERMES_LEGACY_DASHBOARD_SESSION_HINT : "run `hermes whatsapp` inside the sandbox to display a QR code", }; } @@ -195,7 +201,7 @@ function sessionLocationSignal(input: WhatsappProbeInput): DiagnosticSignal | nu severity: "warn", detail: "dashboard-home has WhatsApp credentials, but the Hermes gateway session path is empty", - hint: "run `nemoclaw config set --key platforms.whatsapp.extra.session_path --value /sandbox/.hermes/profiles/dashboard-home/platforms/whatsapp/session --restart --config-accept-new-path`", + hint: HERMES_LEGACY_DASHBOARD_SESSION_HINT, }; } if (gateway === true && dashboard === false) { @@ -473,7 +479,7 @@ function buildHints(verdict: WhatsappVerdict, input: WhatsappProbeInput): string if (hermesDashboardOnlySession(input)) { hints.push( "Hermes dashboard pairing wrote credentials under dashboard-home, but the gateway reads the default platforms path.", - "Use `nemoclaw config set --key platforms.whatsapp.extra.session_path --value /sandbox/.hermes/profiles/dashboard-home/platforms/whatsapp/session --restart --config-accept-new-path` to point the gateway at that session.", + HERMES_LEGACY_DASHBOARD_SESSION_HINT, ); break; } diff --git a/src/lib/messaging/channels/whatsapp/hooks/status-health.test.ts b/src/lib/messaging/channels/whatsapp/hooks/status-health.test.ts index ab27d6bad51..a65582ee96d 100644 --- a/src/lib/messaging/channels/whatsapp/hooks/status-health.test.ts +++ b/src/lib/messaging/channels/whatsapp/hooks/status-health.test.ts @@ -392,7 +392,7 @@ describe("whatsapp.statusHealth openclaw CLI probe", () => { expect(reportOf(run())?.verdict).toBeDefined(); }); - it("reports the Hermes dashboard-only session path split from fixed session roots", () => { + it("guides the Hermes dashboard-only session split through re-pairing (#8184)", () => { const exec = makeExec({ status: 0, stdout: hermesSessionProbeOutput({ @@ -406,13 +406,14 @@ describe("whatsapp.statusHealth openclaw CLI probe", () => { ); const report = reportOf(result); const command = String(exec.mock.calls[0]?.[1] ?? ""); + const hint = report?.signals.find((s) => s.label === "Session location")?.hint; expect(report?.verdict).toBe("unpaired"); - expect(report?.signals.find((s) => s.label === "Session location")?.hint).toContain( - "platforms.whatsapp.extra.session_path", - ); - expect(report?.signals.find((s) => s.label === "Session location")?.hint).toContain( - "--config-accept-new-path", - ); + expect(hint).toContain("`nemoclaw channels remove whatsapp`"); + expect(hint).toContain("`nemoclaw channels add whatsapp`"); + expect(hint).toMatch(/Pair again from the dashboard/); + expect(hint).toContain("/sandbox/.hermes/platforms/whatsapp/session"); + expect(hint).toContain("`nemoclaw channels status --channel whatsapp`"); + expect(hint).not.toContain("platforms.whatsapp.extra.session_path"); expect(command).toContain("/sandbox/.hermes/platforms/whatsapp/session/creds.json"); expect(command).toContain( "/sandbox/.hermes/profiles/dashboard-home/platforms/whatsapp/session/creds.json", diff --git a/test/agent-variant-docs.test.ts b/test/agent-variant-docs.test.ts index 5c40fd7eac1..da942947723 100644 --- a/test/agent-variant-docs.test.ts +++ b/test/agent-variant-docs.test.ts @@ -330,7 +330,7 @@ import { AgentOnly } from "../_components/AgentGuide"; expect(hermes.split(baselineExplanation)).toHaveLength(2); }); - it("keeps the Hermes WhatsApp repair inside a shields maintenance window (#8184)", () => { + it("renders the shared Hermes WhatsApp session path without a configuration repair (#8184)", () => { const whatsapp = readFileSync( new URL("../docs/manage-sandboxes/set-up-whatsapp.mdx", import.meta.url), "utf8", @@ -338,17 +338,13 @@ import { AgentOnly } from "../_components/AgentGuide"; const hermes = renderAgentVariantPage(whatsapp, "hermes", { sourcePath: "/repo/docs/manage-sandboxes/set-up-whatsapp.mdx", }); - const shieldsDown = hermes.indexOf( - 'nemohermes shields down --reason "repair Hermes WhatsApp session path"', - ); - const configSet = hermes.indexOf( - "nemohermes config set --key platforms.whatsapp.extra.session_path", - ); - const shieldsUp = hermes.indexOf("nemohermes shields up", configSet); - expect(shieldsDown).toBeGreaterThanOrEqual(0); - expect(configSet).toBeGreaterThan(shieldsDown); - expect(shieldsUp).toBeGreaterThan(configSet); + expect(hermes).toContain( + "Hermes dashboard pairing and the gateway share `/sandbox/.hermes/platforms/whatsapp/session`", + ); + expect(hermes).toContain("nemohermes channels status --channel whatsapp"); + expect(hermes).toContain("nemohermes channels remove whatsapp"); + expect(hermes).not.toContain("platforms.whatsapp.extra.session_path"); }); it("keeps the troubleshooting security review link within each agent guide (#6558)", () => { diff --git a/test/hermes-whatsapp-dashboard-session-patch.test.ts b/test/hermes-whatsapp-dashboard-session-patch.test.ts new file mode 100644 index 00000000000..ee5182bb87c --- /dev/null +++ b/test/hermes-whatsapp-dashboard-session-patch.test.ts @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { expect, it } from "vitest"; + +const ROOT = path.resolve(import.meta.dirname, ".."); +const PATCH = path.join(ROOT, "agents", "hermes", "whatsapp-proxy.patch"); + +it("stores Hermes dashboard pairing state in the gateway session directory (#8184)", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-hermes-whatsapp-dashboard-")); + const source = path.join(tmp, "hermes_cli", "web_server.py"); + fs.mkdirSync(path.dirname(source), { recursive: true }); + fs.writeFileSync( + source, + `${"\n".repeat(8109)}def _whatsapp_session_path() -> Path:\n` + + " from hermes_constants import get_hermes_dir\n\n" + + ' return get_hermes_dir("platforms/whatsapp/session", "whatsapp/session")\n\n', + ); + + try { + const applied = spawnSync("git", ["apply", "--include=hermes_cli/web_server.py", PATCH], { + cwd: tmp, + encoding: "utf8", + }); + expect(applied.status, applied.stderr).toBe(0); + const patched = fs.readFileSync(source, "utf8"); + expect(patched).toContain('return Path("/sandbox/.hermes/platforms/whatsapp/session")'); + expect(patched).not.toContain("get_hermes_dir"); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } +}); + +it("leaves the Hermes CLI and gateway unpatched (#8184)", () => { + const patch = fs.readFileSync(PATCH, "utf8"); + expect(patch).not.toContain("diff --git a/hermes_cli/main.py"); + expect(patch).not.toContain("diff --git a/gateway/"); +});