Repository navigation
fix(hermes): align dashboard WhatsApp session path #8229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
67 commits
Select commit
Hold shift + click to select a range
fd5a8a2
fix(hermes): unify WhatsApp session state
sandl99 d7d2c17
test(hermes): keep session patch cases linear
sandl99 c3e80d3
refactor(messaging): use runtime manifest for Hermes WhatsApp
sandl99 2cc4096
fix(build): resolve Hermes runtime preload types
sandl99 57f4e94
fix(messaging): address runtime preload review
sandl99 8b2aa86
test(messaging): keep secret scan setup linear
sandl99 af461ff
test(messaging): always clean secret scan fixtures
sandl99 02e10e0
test(hermes): repair runtime preload fixtures
sandl99 ea3a556
Merge branch 'main' into fix/hermes-whatsapp-session-path
apurvvkumaria a5c3e2b
Merge branch 'main' into fix/hermes-whatsapp-session-path
senthilr-nv 350724e
Merge branch 'main' into fix/hermes-whatsapp-session-path
senthilr-nv 2216e9c
fix(messaging): scope Hermes session preload
apurvvkumaria 5ecaa0c
Merge branch 'main' into fix/hermes-whatsapp-session-path
senthilr-nv b9f1345
fix(messaging): apply Hermes runtime aliases
apurvvkumaria 8269976
fix(messaging): reject missing Hermes session values
senthilr-nv 1f3bb77
merge(main): refresh PR #8229
senthilr-nv 3e1a70a
merge(main): refresh PR #8229
senthilr-nv 280951d
Merge branch 'main' into fix/hermes-whatsapp-session-path
sandl99 e994ddc
fix(messaging): reject unsafe runtime preload paths
apurvvkumaria 9ee45be
Merge remote-tracking branch 'origin/main' into codex/pr8229-traversa…
apurvvkumaria 8ea4a4d
test(messaging): cover Hermes session preload wiring
apurvvkumaria 344fbc0
merge(messaging): refresh PR 8229 with main
apurvvkumaria 4fdb936
test(messaging): keep runtime setup harness linear
apurvvkumaria 0634898
merge: refresh PR #8229 with current main
apurvvkumaria 24c4688
fix(messaging): preserve Hermes WhatsApp config integrity
sandl99 0a3f3fe
test(messaging): keep command route fixture linear
sandl99 bf7a892
fix(whatsapp): keep Hermes CLI pairing unchanged
sandl99 2e84a3e
docs(whatsapp): describe dashboard session storage
sandl99 d309d0e
Merge remote-tracking branch 'origin/main' into fix/hermes-whatsapp-s…
sandl99 1ff42e8
fix(whatsapp): scope session patch to dashboard
sandl99 a742997
chore(whatsapp): revert broad session implementation
sandl99 a7a7f3e
fix(hermes): align dashboard WhatsApp session path
sandl99 545c490
fix(hermes): pin WhatsApp session base image
sandl99 1daba19
Merge remote-tracking branch 'origin/main' into fix/hermes-whatsapp-s…
sandl99 9dcef91
docs(security): record Hermes WhatsApp base image
sandl99 49fce0a
merge(main): refresh PR #8229
apurvvkumaria f9c4e82
merge: resolve conflicts with main
github-actions[bot] 4963d1c
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d 589c492
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d 5401ca9
Merge branch 'main' into fix/hermes-whatsapp-session-path
sandl99 ea18717
Merge remote-tracking branch 'origin/main' into fix/hermes-whatsapp-s…
apurvvkumaria 3c1f7fd
docs(whatsapp): remove obsolete Hermes session workaround
apurvvkumaria 6d8bb3d
docs(whatsapp): document legacy Hermes re-pairing
apurvvkumaria 8197641
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d 868a2cc
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d e879cb3
Merge branch 'main' into fix/hermes-whatsapp-session-path
sandl99 634d631
Merge branch 'main' into fix/hermes-whatsapp-session-path
sandl99 96ee9e0
test(docs): align Hermes WhatsApp rendering contract
sandl99 9060fc6
fix(messaging): update Hermes session recovery hint
apurvvkumaria e9b1ead
docs(messaging): align Hermes session recovery guidance
apurvvkumaria cbdf76e
Merge branch 'main' into fix/hermes-whatsapp-session-path
cv f39d67e
fix(hermes): address WhatsApp review findings
sandl99 e1dd866
Merge branch 'main' into fix/hermes-whatsapp-session-path
cv c3f631a
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d fc60ee0
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d 3d3ce74
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d b69f733
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d 110c16f
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d 2ced3af
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d d642cac
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d ef14268
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d 33a5fa1
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d 6908108
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d f4038cd
Merge branch 'main' into fix/hermes-whatsapp-session-path
cv eda36e8
Merge branch 'main' into fix/hermes-whatsapp-session-path
apurvvkumaria b84247e
docs(messaging): name the supported cleanup for legacy WhatsApp sessions
hunglp6d d9a381d
fix(messaging): send the WhatsApp session split through channel removal
hunglp6d File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| /** Keep pinned Hermes WhatsApp pairing state in one durable directory. | ||
| * | ||
| * Hermes v0.19.0 resolves WhatsApp session state relative to each process's | ||
| * `HERMES_HOME`. NemoClaw gives the dashboard an isolated home, so dashboard QR | ||
| * pairing writes credentials that the gateway cannot read. The interactive | ||
| * `hermes whatsapp` command also uses the legacy `whatsapp/session` path. | ||
| * | ||
| * Patch the dashboard onboarding handler, gateway adapter, and CLI command to use | ||
| * the manifest-owned `/sandbox/.hermes/platforms/whatsapp/session` directory. | ||
| * Dashboard and CLI pairing child processes use umask 0007 so the Hermes gateway | ||
| * user can read pairing state through the shared sandbox group. Other processes | ||
| * keep their inherited umask. | ||
| * The Dockerfile binds each input to its reviewed source hash. Remove this patch | ||
| * when the minimum supported Hermes release provides one authoritative WhatsApp | ||
| * session path for these three consumers. | ||
| */ | ||
|
|
||
| import { createHash } from "node:crypto"; | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
| import { parseArgs } from "node:util"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| export const CANONICAL_SESSION_PATH = "/sandbox/.hermes/platforms/whatsapp/session"; | ||
|
|
||
| const EXPECTED_SOURCE_SHA256 = { | ||
| adapter: "96730b2261eed2eb34affd1cec980a039c8cb8a7cc864b798b6e1b7f17a354a9", | ||
| webServer: "0bf9d4dd17a1b7c3d96c94dacea9884426e7bbb5c8818685b43ef68f2465b3f2", | ||
| // patch-profile-policy-defaults.py changes independent update-policy code first. | ||
| main: "18bab193cf86e1198bcea7ca09f62b88f1d30615aeed367b88388de21c3723c0", | ||
| } as const; | ||
|
|
||
| const ADAPTER_OLD = ` self._session_path: Path = Path(config.extra.get( | ||
| "session_path", | ||
| get_hermes_dir("platforms/whatsapp/session", "whatsapp/session") | ||
| )) | ||
| `; | ||
| const ADAPTER_NEW = ` self._session_path: Path = Path(config.extra.get( | ||
| "session_path", | ||
| # NemoClaw keeps WhatsApp pairing credentials in manifest-owned durable state. | ||
| "${CANONICAL_SESSION_PATH}" | ||
| )) | ||
| `; | ||
|
|
||
| const WEB_SERVER_OLD = `def _whatsapp_session_path() -> Path: | ||
| from hermes_constants import get_hermes_dir | ||
|
|
||
| return get_hermes_dir("platforms/whatsapp/session", "whatsapp/session") | ||
| `; | ||
| const WEB_SERVER_NEW = `def _whatsapp_session_path() -> Path: | ||
| # NemoClaw isolates the dashboard home from gateway configuration and state. | ||
| return Path("${CANONICAL_SESSION_PATH}") | ||
| `; | ||
| const WEB_SERVER_POPEN_OLD = ` start_new_session=True, | ||
| env=env, | ||
| creationflags=windows_hide_flags(), | ||
| `; | ||
| const WEB_SERVER_POPEN_NEW = ` start_new_session=True, | ||
| env=env, | ||
| # Preserve pairing-state group access for the Hermes gateway user in the shared sandbox group. | ||
| umask=0o007, | ||
| creationflags=windows_hide_flags(), | ||
| `; | ||
|
|
||
| const MAIN_OLD = ` session_dir = get_hermes_home() / "whatsapp" / "session" | ||
| `; | ||
| const MAIN_NEW = ` # NemoClaw keeps CLI and dashboard pairing in the gateway's durable state. | ||
| session_dir = Path("${CANONICAL_SESSION_PATH}") | ||
| `; | ||
| const MAIN_PAIRING_PROCESS_OLD = ` str(session_dir), | ||
| ], | ||
| cwd=str(bridge_dir), | ||
| env=with_hermes_node_path(), | ||
| ) | ||
| `; | ||
| const MAIN_PAIRING_PROCESS_NEW = ` str(session_dir), | ||
| ], | ||
| cwd=str(bridge_dir), | ||
| env=with_hermes_node_path(), | ||
| # Preserve pairing-state group access for the Hermes gateway user in the shared sandbox group. | ||
| umask=0o007, | ||
| ) | ||
| `; | ||
|
|
||
| function sha256(source: string): string { | ||
| return createHash("sha256").update(source).digest("hex"); | ||
| } | ||
|
|
||
| function replaceExact(source: string, oldValue: string, newValue: string, label: string): string { | ||
| const oldCount = source.split(oldValue).length - 1; | ||
| const newCount = source.split(newValue).length - 1; | ||
| if (oldCount !== 1 || newCount !== 0) { | ||
| throw new Error( | ||
| `Hermes ${label} source shape changed: expected one unpatched occurrence, found ${oldCount}; prepatched occurrences: ${newCount}`, | ||
| ); | ||
| } | ||
| return source.replace(oldValue, newValue); | ||
| } | ||
|
|
||
| export function patchAdapterSource(source: string): string { | ||
| return replaceExact(source, ADAPTER_OLD, ADAPTER_NEW, "WhatsApp adapter"); | ||
| } | ||
|
|
||
| export function patchWebServerSource(source: string): string { | ||
| const patchedPath = replaceExact(source, WEB_SERVER_OLD, WEB_SERVER_NEW, "WhatsApp dashboard"); | ||
| return replaceExact( | ||
| patchedPath, | ||
| WEB_SERVER_POPEN_OLD, | ||
| WEB_SERVER_POPEN_NEW, | ||
| "WhatsApp dashboard pairing process", | ||
| ); | ||
| } | ||
|
|
||
| export function patchMainSource(source: string): string { | ||
| const patchedPath = replaceExact(source, MAIN_OLD, MAIN_NEW, "WhatsApp CLI"); | ||
| return replaceExact( | ||
| patchedPath, | ||
| MAIN_PAIRING_PROCESS_OLD, | ||
| MAIN_PAIRING_PROCESS_NEW, | ||
| "WhatsApp CLI pairing process", | ||
| ); | ||
| } | ||
|
|
||
| function patchFile( | ||
| sourcePath: string, | ||
| kind: keyof typeof EXPECTED_SOURCE_SHA256, | ||
| patcher: (source: string) => string, | ||
| ): void { | ||
| const source = fs.readFileSync(sourcePath, "utf8"); | ||
| const actualSha256 = sha256(source); | ||
| const expectedSha256 = EXPECTED_SOURCE_SHA256[kind]; | ||
| if (actualSha256 !== expectedSha256) { | ||
| throw new Error( | ||
| `${sourcePath} is not the reviewed Hermes v2026.7.20 ${kind} source; expected sha256 ${expectedSha256}, got ${actualSha256}`, | ||
| ); | ||
| } | ||
| fs.writeFileSync(sourcePath, patcher(source), "utf8"); | ||
| } | ||
|
|
||
| function main(): void { | ||
| const { values } = parseArgs({ | ||
| options: { | ||
| adapter: { | ||
| type: "string", | ||
| default: "/opt/hermes/plugins/platforms/whatsapp/adapter.py", | ||
| }, | ||
| "web-server": { | ||
| type: "string", | ||
| default: "/opt/hermes/hermes_cli/web_server.py", | ||
| }, | ||
| main: { type: "string", default: "/opt/hermes/hermes_cli/main.py" }, | ||
| }, | ||
| strict: true, | ||
| }); | ||
|
|
||
| patchFile(values.adapter, "adapter", patchAdapterSource); | ||
| patchFile(values["web-server"], "webServer", patchWebServerSource); | ||
| patchFile(values.main, "main", patchMainSource); | ||
| } | ||
|
|
||
| if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { | ||
| try { | ||
| main(); | ||
| } catch (error) { | ||
| console.error(`ERROR: ${error instanceof Error ? error.message : String(error)}`); | ||
| process.exitCode = 1; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { describe, expect, it } from "vitest"; | ||
| import { | ||
| CANONICAL_SESSION_PATH, | ||
| patchAdapterSource, | ||
| patchMainSource, | ||
| patchWebServerSource, | ||
| } from "../agents/hermes/patch-whatsapp-session-path.mts"; | ||
|
|
||
| const ADAPTER_FIXTURE = ` self._session_path: Path = Path(config.extra.get( | ||
| "session_path", | ||
| get_hermes_dir("platforms/whatsapp/session", "whatsapp/session") | ||
| )) | ||
| `; | ||
|
|
||
| const WEB_SERVER_FIXTURE = `def _whatsapp_session_path() -> Path: | ||
| from hermes_constants import get_hermes_dir | ||
|
|
||
| return get_hermes_dir("platforms/whatsapp/session", "whatsapp/session") | ||
|
|
||
| def _spawn(): | ||
| return subprocess.Popen( | ||
| ["node", "bridge.js"], | ||
| start_new_session=True, | ||
| env=env, | ||
| creationflags=windows_hide_flags(), | ||
| ) | ||
| `; | ||
|
|
||
| const MAIN_FIXTURE = `def cmd_whatsapp(): | ||
| session_dir = get_hermes_home() / "whatsapp" / "session" | ||
| try: | ||
| subprocess.run( | ||
| [ | ||
| "node", | ||
| "bridge.js", | ||
| "--session", | ||
| str(session_dir), | ||
| ], | ||
| cwd=str(bridge_dir), | ||
| env=with_hermes_node_path(), | ||
| ) | ||
| `; | ||
|
|
||
| const patchers = { | ||
| adapter: patchAdapterSource, | ||
| webServer: patchWebServerSource, | ||
| main: patchMainSource, | ||
| }; | ||
|
|
||
| describe("Hermes WhatsApp session location", () => { | ||
| it.each([ | ||
| ["gateway adapter", "adapter", ADAPTER_FIXTURE], | ||
| ["dashboard QR pairing", "webServer", WEB_SERVER_FIXTURE], | ||
| ["CLI QR pairing", "main", MAIN_FIXTURE], | ||
| ] as const)("uses the manifest-owned durable directory for %s (#8184)", (_label, kind, fixture) => { | ||
| const result = patchers[kind](fixture); | ||
|
|
||
| expect(result).toContain(CANONICAL_SESSION_PATH); | ||
| expect(result).not.toContain('get_hermes_dir("platforms/whatsapp/session"'); | ||
| expect(result).not.toContain('get_hermes_home() / "whatsapp" / "session"'); | ||
| }); | ||
|
|
||
| it("preserves the explicit adapter session path override (#8184)", () => { | ||
| expect(patchAdapterSource(ADAPTER_FIXTURE)).toContain( | ||
| 'config.extra.get(\n "session_path"', | ||
| ); | ||
| }); | ||
|
|
||
| it.each([ | ||
| ["dashboard", patchWebServerSource, WEB_SERVER_FIXTURE], | ||
| ["CLI", patchMainSource, MAIN_FIXTURE], | ||
| ] as const)("preserves shared-group access when %s pairing creates session state (#8184)", (_label, patcher, fixture) => { | ||
| expect(patcher(fixture)).toContain("umask=0o007"); | ||
| }); | ||
|
|
||
| it.each([ | ||
| ["adapter", ADAPTER_FIXTURE.replace('"whatsapp/session"', '"changed/session"')], | ||
| ["webServer", WEB_SERVER_FIXTURE.replace('"whatsapp/session"', '"changed/session"')], | ||
| ["webServer", WEB_SERVER_FIXTURE.replace("creationflags=", "changed_creationflags=")], | ||
| ["main", MAIN_FIXTURE.replace('"whatsapp"', '"changed"')], | ||
| ["main", MAIN_FIXTURE.replace("env=with_hermes_node_path()", "changed_env=True")], | ||
| ] as const)("fails closed when the pinned %s pairing source changes", (kind, fixture) => { | ||
| expect(() => patchers[kind](fixture)).toThrow("source shape changed"); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.