Skip to content
Merged
Show file tree
Hide file tree
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 Aug 4, 2026
d7d2c17
test(hermes): keep session patch cases linear
sandl99 Aug 4, 2026
c3e80d3
refactor(messaging): use runtime manifest for Hermes WhatsApp
sandl99 Aug 4, 2026
2cc4096
fix(build): resolve Hermes runtime preload types
sandl99 Aug 4, 2026
57f4e94
fix(messaging): address runtime preload review
sandl99 Aug 4, 2026
8b2aa86
test(messaging): keep secret scan setup linear
sandl99 Aug 4, 2026
af461ff
test(messaging): always clean secret scan fixtures
sandl99 Aug 4, 2026
02e10e0
test(hermes): repair runtime preload fixtures
sandl99 Aug 4, 2026
ea3a556
Merge branch 'main' into fix/hermes-whatsapp-session-path
apurvvkumaria Aug 4, 2026
a5c3e2b
Merge branch 'main' into fix/hermes-whatsapp-session-path
senthilr-nv Aug 5, 2026
350724e
Merge branch 'main' into fix/hermes-whatsapp-session-path
senthilr-nv Aug 5, 2026
2216e9c
fix(messaging): scope Hermes session preload
apurvvkumaria Aug 5, 2026
5ecaa0c
Merge branch 'main' into fix/hermes-whatsapp-session-path
senthilr-nv Aug 5, 2026
b9f1345
fix(messaging): apply Hermes runtime aliases
apurvvkumaria Aug 5, 2026
8269976
fix(messaging): reject missing Hermes session values
senthilr-nv Aug 5, 2026
1f3bb77
merge(main): refresh PR #8229
senthilr-nv Aug 5, 2026
3e1a70a
merge(main): refresh PR #8229
senthilr-nv Aug 5, 2026
280951d
Merge branch 'main' into fix/hermes-whatsapp-session-path
sandl99 Aug 5, 2026
e994ddc
fix(messaging): reject unsafe runtime preload paths
apurvvkumaria Aug 5, 2026
9ee45be
Merge remote-tracking branch 'origin/main' into codex/pr8229-traversa…
apurvvkumaria Aug 5, 2026
8ea4a4d
test(messaging): cover Hermes session preload wiring
apurvvkumaria Aug 5, 2026
344fbc0
merge(messaging): refresh PR 8229 with main
apurvvkumaria Aug 5, 2026
4fdb936
test(messaging): keep runtime setup harness linear
apurvvkumaria Aug 5, 2026
0634898
merge: refresh PR #8229 with current main
apurvvkumaria Aug 5, 2026
24c4688
fix(messaging): preserve Hermes WhatsApp config integrity
sandl99 Aug 5, 2026
0a3f3fe
test(messaging): keep command route fixture linear
sandl99 Aug 5, 2026
bf7a892
fix(whatsapp): keep Hermes CLI pairing unchanged
sandl99 Aug 5, 2026
2e84a3e
docs(whatsapp): describe dashboard session storage
sandl99 Aug 5, 2026
d309d0e
Merge remote-tracking branch 'origin/main' into fix/hermes-whatsapp-s…
sandl99 Aug 5, 2026
1ff42e8
fix(whatsapp): scope session patch to dashboard
sandl99 Aug 5, 2026
a742997
chore(whatsapp): revert broad session implementation
sandl99 Aug 5, 2026
a7a7f3e
fix(hermes): align dashboard WhatsApp session path
sandl99 Aug 5, 2026
545c490
fix(hermes): pin WhatsApp session base image
sandl99 Aug 5, 2026
1daba19
Merge remote-tracking branch 'origin/main' into fix/hermes-whatsapp-s…
sandl99 Aug 5, 2026
9dcef91
docs(security): record Hermes WhatsApp base image
sandl99 Aug 5, 2026
49fce0a
merge(main): refresh PR #8229
apurvvkumaria Aug 5, 2026
f9c4e82
merge: resolve conflicts with main
github-actions[bot] Aug 6, 2026
4963d1c
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d Aug 6, 2026
589c492
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d Aug 6, 2026
5401ca9
Merge branch 'main' into fix/hermes-whatsapp-session-path
sandl99 Aug 6, 2026
ea18717
Merge remote-tracking branch 'origin/main' into fix/hermes-whatsapp-s…
apurvvkumaria Aug 6, 2026
3c1f7fd
docs(whatsapp): remove obsolete Hermes session workaround
apurvvkumaria Aug 6, 2026
6d8bb3d
docs(whatsapp): document legacy Hermes re-pairing
apurvvkumaria Aug 6, 2026
8197641
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d Aug 6, 2026
868a2cc
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d Aug 7, 2026
e879cb3
Merge branch 'main' into fix/hermes-whatsapp-session-path
sandl99 Aug 7, 2026
634d631
Merge branch 'main' into fix/hermes-whatsapp-session-path
sandl99 Aug 7, 2026
96ee9e0
test(docs): align Hermes WhatsApp rendering contract
sandl99 Aug 7, 2026
9060fc6
fix(messaging): update Hermes session recovery hint
apurvvkumaria Aug 7, 2026
e9b1ead
docs(messaging): align Hermes session recovery guidance
apurvvkumaria Aug 7, 2026
cbdf76e
Merge branch 'main' into fix/hermes-whatsapp-session-path
cv Aug 7, 2026
f39d67e
fix(hermes): address WhatsApp review findings
sandl99 Aug 7, 2026
e1dd866
Merge branch 'main' into fix/hermes-whatsapp-session-path
cv Aug 7, 2026
c3f631a
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d Aug 7, 2026
fc60ee0
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d Aug 7, 2026
3d3ce74
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d Aug 7, 2026
b69f733
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d Aug 8, 2026
110c16f
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d Aug 8, 2026
2ced3af
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d Aug 9, 2026
d642cac
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d Aug 9, 2026
ef14268
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d Aug 9, 2026
33a5fa1
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d Aug 10, 2026
6908108
Merge branch 'main' into fix/hermes-whatsapp-session-path
hunglp6d Aug 10, 2026
f4038cd
Merge branch 'main' into fix/hermes-whatsapp-session-path
cv Aug 10, 2026
eda36e8
Merge branch 'main' into fix/hermes-whatsapp-session-path
apurvvkumaria Aug 10, 2026
b84247e
docs(messaging): name the supported cleanup for legacy WhatsApp sessions
hunglp6d Aug 11, 2026
d9a381d
fix(messaging): send the WhatsApp session split through channel removal
hunglp6d Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ COPY agents/hermes/patch-gateway-runtime-metadata.py /opt/nemoclaw-hermes-config
COPY agents/hermes/patch-gateway-process-identity.py /opt/nemoclaw-hermes-config/patch-gateway-process-identity.py
COPY agents/hermes/patch-cron-execution-runtime.py /opt/nemoclaw-hermes-config/patch-cron-execution-runtime.py
COPY agents/hermes/patch-neutral-platform-env-activation.py /opt/nemoclaw-hermes-config/patch-neutral-platform-env-activation.py
COPY agents/hermes/patch-whatsapp-session-path.mts /opt/nemoclaw-hermes-config/patch-whatsapp-session-path.mts
COPY agents/hermes/host/managed-tool-gateway-matrix.json /opt/nemoclaw-hermes-config/managed-tool-gateway-matrix.json
COPY src/lib/tool-disclosure.ts /src/lib/tool-disclosure.ts
COPY src/lib/messaging/ /src/lib/messaging/
Expand Down Expand Up @@ -381,6 +382,39 @@ RUN /usr/bin/python3 -I \
"$NEMOCLAW_HERMES_NEUTRAL_PLATFORM_OUTPUT_SHA256" /opt/hermes/gateway/config.py \
| sha256sum -c -

# Hermes v0.19.0 resolves WhatsApp state relative to each process's
# HERMES_HOME. NemoClaw isolates the dashboard home, and the CLI still uses a
# legacy path, so successful pairing can write credentials outside the
# manifest-owned durable directory. Bind the three consumers to their reviewed
# sources before making the existing /sandbox/.hermes/platforms/whatsapp/session
# directory authoritative for pairing and gateway startup. Dashboard and CLI
# pairing child processes use umask 0007 so their state remains accessible to
# the separate Hermes gateway user through the shared sandbox group. Other
# processes retain their inherited umask.
ARG NEMOCLAW_HERMES_WHATSAPP_SESSION_PATCHER_SHA256=20e768d4617108180246c531475e21f00738272fcc0a8448d598534aa1eb16cb
ARG NEMOCLAW_HERMES_WHATSAPP_ADAPTER_SOURCE_SHA256=96730b2261eed2eb34affd1cec980a039c8cb8a7cc864b798b6e1b7f17a354a9
ARG NEMOCLAW_HERMES_WHATSAPP_WEB_SERVER_SOURCE_SHA256=0bf9d4dd17a1b7c3d96c94dacea9884426e7bbb5c8818685b43ef68f2465b3f2
ARG NEMOCLAW_HERMES_WHATSAPP_MAIN_SOURCE_SHA256=18bab193cf86e1198bcea7ca09f62b88f1d30615aeed367b88388de21c3723c0
ARG NEMOCLAW_HERMES_WHATSAPP_ADAPTER_OUTPUT_SHA256=4e480d766267478a3dac7a0cdb2ab3c2264b33bec29860dc21e93f1422e742fd
ARG NEMOCLAW_HERMES_WHATSAPP_WEB_SERVER_OUTPUT_SHA256=4a773388ab1cd38ace0a8c7e21388aaa6c4a775ddbf6d7f1623cca461a09f3b3
ARG NEMOCLAW_HERMES_WHATSAPP_MAIN_OUTPUT_SHA256=5c175a2dbeb23ccad66a3ce1cdbaa12f424cb925abc4fcf5e4d0064240d4f400
# hadolint ignore=DL4006
RUN printf '%s %s\n' \
"$NEMOCLAW_HERMES_WHATSAPP_SESSION_PATCHER_SHA256" /opt/nemoclaw-hermes-config/patch-whatsapp-session-path.mts \
"$NEMOCLAW_HERMES_WHATSAPP_ADAPTER_SOURCE_SHA256" /opt/hermes/plugins/platforms/whatsapp/adapter.py \
"$NEMOCLAW_HERMES_WHATSAPP_WEB_SERVER_SOURCE_SHA256" /opt/hermes/hermes_cli/web_server.py \
"$NEMOCLAW_HERMES_WHATSAPP_MAIN_SOURCE_SHA256" /opt/hermes/hermes_cli/main.py \
| sha256sum -c - \
|| { echo "ERROR: Hermes WhatsApp session source identity mismatch" >&2; exit 1; }
# hadolint ignore=DL4006
RUN node --experimental-strip-types \
/opt/nemoclaw-hermes-config/patch-whatsapp-session-path.mts \
&& printf '%s %s\n' \
"$NEMOCLAW_HERMES_WHATSAPP_ADAPTER_OUTPUT_SHA256" /opt/hermes/plugins/platforms/whatsapp/adapter.py \
"$NEMOCLAW_HERMES_WHATSAPP_WEB_SERVER_OUTPUT_SHA256" /opt/hermes/hermes_cli/web_server.py \
"$NEMOCLAW_HERMES_WHATSAPP_MAIN_OUTPUT_SHA256" /opt/hermes/hermes_cli/main.py \
| sha256sum -c -

# Prove the source-level fallback on the real installed runtime through the
# user-facing profile creation path. The new home must remain config-less while
# all security, privacy, update, browser, and session defaults fail safe.
Expand Down
171 changes: 171 additions & 0 deletions agents/hermes/patch-whatsapp-session-path.mts
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;
}
}
6 changes: 6 additions & 0 deletions test/hermes-final-image-layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ const HERMES_INTEGRITY_FILES = [
source: "agents/hermes/patch-neutral-platform-env-activation.py",
target: "/opt/nemoclaw-hermes-config/patch-neutral-platform-env-activation.py",
},
{
arg: "NEMOCLAW_HERMES_WHATSAPP_SESSION_PATCHER_SHA256",
source: "agents/hermes/patch-whatsapp-session-path.mts",
target: "/opt/nemoclaw-hermes-config/patch-whatsapp-session-path.mts",
},
] as const;

type LegacyDataFixture =
Expand Down Expand Up @@ -239,6 +244,7 @@ describe("Hermes final image layout", () => {
"COPY agents/hermes/patch-gateway-process-identity.py /opt/nemoclaw-hermes-config/patch-gateway-process-identity.py",
"COPY agents/hermes/patch-cron-execution-runtime.py /opt/nemoclaw-hermes-config/patch-cron-execution-runtime.py",
"COPY agents/hermes/patch-neutral-platform-env-activation.py /opt/nemoclaw-hermes-config/patch-neutral-platform-env-activation.py",
"COPY agents/hermes/patch-whatsapp-session-path.mts /opt/nemoclaw-hermes-config/patch-whatsapp-session-path.mts",
"COPY agents/hermes/host/managed-tool-gateway-matrix.json /opt/nemoclaw-hermes-config/managed-tool-gateway-matrix.json",
"COPY src/lib/tool-disclosure.ts /src/lib/tool-disclosure.ts",
"COPY src/lib/messaging/ /src/lib/messaging/",
Expand Down
88 changes: 88 additions & 0 deletions test/hermes-whatsapp-session-path-patch.test.ts
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) => {
Comment thread
sandl99 marked this conversation as resolved.
Resolved
expect(() => patchers[kind](fixture)).toThrow("source shape changed");
});
});
Loading