Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e59b877
feat(messaging): support Google Chat on Hermes over keyless Pub/Sub R…
hunglp6d Aug 17, 2026
e55ca9b
Merge branch 'main' of github.com:NVIDIA/NemoClaw into feat/hermes-go…
hunglp6d Aug 17, 2026
a546a77
fix(messaging): allow only pull and acknowledge on the Hermes Pub/Sub…
hunglp6d Aug 18, 2026
e947a2a
fix(messaging): close the proc handle in the Google Chat proxy lookup
hunglp6d Aug 18, 2026
0457298
Merge remote-tracking branch 'origin/main' into feat/hermes-googlechat
hunglp6d Aug 18, 2026
5d365d5
Merge remote-tracking branch 'origin/main' into feat/hermes-googlechat
hunglp6d Aug 18, 2026
1b97989
refactor(messaging): attach the Google Chat override through the Herm…
hunglp6d Aug 18, 2026
8428a68
fix(hermes): vendor the Google Chat wheels the managed union installs…
hunglp6d Aug 18, 2026
f7c76c8
fix(hermes): vendor opentelemetry-api for the managed union install
hunglp6d Aug 18, 2026
2d7867d
test(messaging): cover the Hermes Google Chat REST pull boundary
hunglp6d Aug 18, 2026
7f83e8a
refactor(messaging): tighten the Google Chat override prose and test …
hunglp6d Aug 18, 2026
8c5e6c4
test(messaging): cover the Google Chat bridge scopes and Hermes egres…
hunglp6d Aug 18, 2026
9cd2fcc
fix(messaging): replace the Google Chat pull loop on a repeat connect
hunglp6d Aug 18, 2026
44eab71
refactor(messaging): drop the unreachable reconnect path and centrali…
hunglp6d Aug 18, 2026
62d6e5d
fix(onboard): match the bridge binding before reusing a gateway provider
hunglp6d Aug 19, 2026
c0758e2
Merge branch 'main' into feat/hermes-googlechat
hunglp6d Aug 19, 2026
f0495c4
fix(messaging): retire an unreadable Google Chat delivery
hunglp6d Aug 19, 2026
3668e10
fix(hermes): admit the Google Chat asset into the portable context
hunglp6d Aug 19, 2026
06b846d
Merge branch 'main' into feat/hermes-googlechat
hunglp6d Aug 19, 2026
7441b43
docs(messaging): scope Google Chat guidance to the agent that needs it
hunglp6d Aug 19, 2026
6444058
Merge branch 'feat/hermes-googlechat' of github.com:NVIDIA/NemoClaw i…
hunglp6d Aug 19, 2026
609a989
Merge branch 'main' into feat/hermes-googlechat
hunglp6d Aug 19, 2026
ce31d08
Merge branch 'main' into feat/hermes-googlechat
sandl99 Aug 20, 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
38 changes: 36 additions & 2 deletions agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ COPY agents/hermes/security-dependencies.patch /scripts/hermes-security-dependen
FROM scratch AS hermes-agent-payload

COPY agents/hermes/plugin/ /opt/nemoclaw-hermes-plugin/
# Channel-owned Google Chat runtime asset, loaded by the plugin beside its own
# __init__.py.
COPY src/lib/messaging/channels/googlechat/runtime/hermes-adapter.py /opt/nemoclaw-hermes-plugin/googlechat_adapter.py
COPY agents/hermes/generate-config.ts /opt/nemoclaw-hermes-config/generate-config.ts
COPY agents/hermes/config/ /opt/nemoclaw-hermes-config/config/
COPY agents/hermes/image-build-probes.py /opt/nemoclaw-hermes-config/image-build-probes.py
Expand Down Expand Up @@ -137,7 +140,7 @@ COPY agents/hermes/validate-cli-adapter.py /usr/local/lib/nemoclaw/validate-herm
COPY agents/hermes/hermes-cli-adapter-v1.json /usr/local/share/nemoclaw/hermes-cli-adapter-v1.json


# Fetch the exact managed Teams capability wheels outside RUN instructions so
# Fetch the exact managed capability wheels outside RUN instructions so
# both the standard build and the protected --network none rebuild install the
# reviewed union from immutable inputs rather than a reusable networked layer.
FROM scratch AS hermes-managed-teams-common-wheels
Expand All @@ -148,13 +151,39 @@ ADD --chmod=0444 --checksum=sha256:e2b0257d9b8782830df61eb6aa993a1ddc0349daddd84
ADD --chmod=0444 --checksum=sha256:c61057695b9f1a97de9b6f54f0c66206903f56c22427b0bca31e0fc34da49311 https://files.pythonhosted.org/packages/8d/91/01e6aeddd78639c74489c24785f2cec0f842ba16f3b594db202386bd721d/microsoft_teams_common-2.0.15-py3-none-any.whl /microsoft_teams_common-2.0.15-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:dd17e95a7c71bce75e8108113438ba7c4a086b3bcad4f57a8c09b7af3d753c2d https://files.pythonhosted.org/packages/94/b0/d807279f4b55d16d1f120d5ac4344c6e39b56732e2a224d40bded7fd67ad/msal-1.37.0-py3-none-any.whl /msal-1.37.0-py3-none-any.whl

# Google Chat needs the google SDKs, which Hermes does not package: its
# pyproject declares no google_chat extra, and `_load_google_modules()`
# imports pubsub, googleapiclient, and grpc all-or-nothing even in REST-pull
# mode. Vendor only what the base venv lacks; uv satisfies the rest from the
# installed distributions.
ADD --chmod=0444 --checksum=sha256:cdf9c67e7ca2402d86ccbfde5f2503fc83e3cc3f58cc78456ae96cad24a6d2de https://files.pythonhosted.org/packages/bc/c1/a8a92ae1bc4b1a8f804c776d7d3f0c771b78a62c3ad4df1be41b3fd8c767/google_api_core-2.34.0-py3-none-any.whl /google_api_core-2.34.0-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:61eaaac3b8fc8fdf11c08af87abc3d1342d1b37319cc1b57405f86ef7697e717 https://files.pythonhosted.org/packages/b0/34/5a624e49f179aa5b0cb87b2ce8093960299030ff40423bfbde09360eb908/google_api_python_client-2.194.0-py3-none-any.whl /google_api_python_client-2.194.0-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:eada68dfd52b3b81191827601e2a0c3fa12540c818534b630ddc5355769c3995 https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl /google_auth-2.55.1-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:67088a8387dc3f66016a690634f1cb9b7a48a31406f8998e088efc64203e7a12 https://files.pythonhosted.org/packages/68/8a/96410d0c3e02b584d1e7f5d4000b2e1710855650b226cd0601f2b154940a/google_auth_httplib2-0.4.1-py3-none-any.whl /google_auth_httplib2-0.4.1-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:7210d691a46d7a66559696899ebe6eb731e63de29b624964b3be4dd2d12d3e19 https://files.pythonhosted.org/packages/93/20/dd0b27d4ad4577c062e77ff968ca3e2d404186cd78c8a2a53a0ef5fe5389/google_cloud_pubsub-2.39.0-py3-none-any.whl /google_cloud_pubsub-2.39.0-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:28a1934bcd33b9c9da66ac301a0a4227e3367f095a17d0375cb98f0a09d93b79 https://files.pythonhosted.org/packages/9a/51/186c02b8549b69ccda44429cf6ff5081e4b61a602ddfe6a8020d1be31d1b/googleapis_common_protos-1.75.1-py3-none-any.whl /googleapis_common_protos-1.75.1-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:0f5e680b20aa0a9441e68c769da04d94d70fca4e43751a82d8abb8aa6a7181ca https://files.pythonhosted.org/packages/84/ab/be3ad0d46cffe35fd1e7cc3f9947edd6cb3c552229de3be2742f15f7ea47/grpc_google_iam_v1-0.14.5-py3-none-any.whl /grpc_google_iam_v1-0.14.5-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:f6a838a7c5fb84ae98833ec0ef81ed438c26e11e54b2ddb8e92ad328c861de69 https://files.pythonhosted.org/packages/d6/00/73204406228cf989bea6b0fd9fe4702fab49a8a152a0c6f90856dadb6ac7/grpcio_status-1.83.0-py3-none-any.whl /grpcio_status-1.83.0-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:dc6705cacdf3fb0a2aba7629fa33c90fd93e30035db0c157325826be177e4816 https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl /httplib2-0.32.0-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl /opentelemetry_api-1.44.0-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl /opentelemetry_sdk-1.44.0-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl /opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:dc76880b8ee951cca002098574376cf71e055f9f16d9ba6570fb8a06f726d281 https://files.pythonhosted.org/packages/61/3a/cfee3c50294f55a2f0f9575052dec2c2a48891ad4b1c2a133b05a87026cd/proto_plus-1.28.3-py3-none-any.whl /proto_plus-1.28.3-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9 https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl /protobuf-7.35.1-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl /pyasn1-0.6.4-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl /pyasn1_modules-0.4.2-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl /pyparsing-3.3.2-py3-none-any.whl
ADD --chmod=0444 --checksum=sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686 https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl /uritemplate-4.2.0-py3-none-any.whl

FROM hermes-managed-teams-common-wheels AS hermes-managed-teams-amd64-wheels

ADD --chmod=0444 --checksum=sha256:10e481880b307a6a438c1cc7b0a1fa8754247239ef5a2e8fe82bd8a1e76e7682 https://files.pythonhosted.org/packages/68/20/fc1812f20ec75af2d4b5e391e93f15621af3102da46142e26177eb506b32/dependency_injector-4.49.1-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl /dependency_injector-4.49.1-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl
ADD --chmod=0444 --checksum=sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f https://files.pythonhosted.org/packages/e8/c0/539fe0832f2dd6500a28f5263071623fb34e8d4867aec632ccf81bd21156/grpcio-1.83.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl /grpcio-1.83.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl

FROM hermes-managed-teams-common-wheels AS hermes-managed-teams-arm64-wheels

ADD --chmod=0444 --checksum=sha256:e05da5bc73a3e026f962a223672002934c0f415064b6e2c3db0b255e46c7b521 https://files.pythonhosted.org/packages/32/ab/b1e1826aacc37d07ba0101230de7b5cbbb5ac6364b78ca3957f0a90d6a51/dependency_injector-4.49.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl /dependency_injector-4.49.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
ADD --chmod=0444 --checksum=sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867 https://files.pythonhosted.org/packages/b0/e8/d0389e09cd6b4c4d3089b92967ae4e3ffd64795bd349bf2f85cd6656d3da/grpcio-1.83.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl /grpcio-1.83.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl

FROM scratch AS hermes-managed-teams-0-wheels

Expand Down Expand Up @@ -378,7 +407,7 @@ RUN find /opt/nemoclaw-hermes-config -type d -exec chmod 755 {} + \
/scripts/patch-bundled-npm-tar.mts \
&& chmod -R a+rX /src/lib/messaging

ARG NEMOCLAW_HERMES_IMAGE_BUILD_PROBES_SHA256=89f530da6a8296c8bab449a2a324d8efedca1a90b04a5e0f57cf9203ed2011cd
ARG NEMOCLAW_HERMES_IMAGE_BUILD_PROBES_SHA256=521756ea979d58df44d52b7fc1c2f062a11db397835b925f15bc0265e33e48ea
# hadolint ignore=DL4006
RUN printf '%s %s\n' \
"$NEMOCLAW_HERMES_IMAGE_BUILD_PROBES_SHA256" /opt/nemoclaw-hermes-config/image-build-probes.py \
Expand Down Expand Up @@ -636,6 +665,11 @@ RUN /usr/bin/python3 -I /usr/local/lib/nemoclaw/patch-hermes-discord-recovery-pe
&& /usr/bin/python3 -I /opt/nemoclaw-hermes-config/image-build-probes.py \
discord-recovery-source

# Pin the Hermes definitions the Google Chat override binds, so an upgrade fails
# this build instead of silently falling back to the stock adapter.
RUN /usr/bin/python3 -I /opt/nemoclaw-hermes-config/image-build-probes.py \
googlechat-override-seams

# Hermes v0.19.0's bundled Langfuse plugin rejects OpenShell resolver
# placeholders before the SDK can construct its Basic-auth headers. Accept only
# the exact public/secret placeholder bound to the matching standard Langfuse
Expand Down
1 change: 1 addition & 0 deletions agents/hermes/config/managed-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const MANAGED_IMAGE_HERMES_SUPPORTED_PLATFORMS = [
"slack",
"whatsapp",
"teams",
"google_chat",
] as const;

// Hermes v0.19.0 also packages platform plugins and built-in adapters that are
Expand Down
30 changes: 30 additions & 0 deletions agents/hermes/image-build-probes.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,35 @@ def reopen_probe(conn):
assert store.call(reopen_probe) == ("gateway-reopened",)


def verify_googlechat_override_seams() -> None:
"""Fail the build when a Google Chat definition the channel override binds moves.

The override subclasses the bundled adapter because ``PlatformEntry`` carries
no credential or transport field. Those names are Hermes internals, so pin
them: an upgrade that renames one stops the build instead of letting the
channel fall back to the stock adapter unnoticed.
"""
path = "/opt/hermes/plugins/platforms/google_chat/adapter.py"
source = Path(path).read_text(encoding="utf-8")
expected = {
"def _validate_config(self) -> Tuple[str, Optional[str]]:": 1,
"def _load_sa_credentials(self) -> Any:": 1,
"def _new_authed_http(self) -> Any:": 1,
"async def connect(self, *, is_reconnect: bool = False) -> bool:": 1,
# connect() gates its gRPC subscriber precheck and its own supervisor on
# this test; the override reports no subscription so both are skipped.
"if subscription_path is not None:": 2,
}
for needle, count in expected.items():
actual = source.count(needle)
assert actual == count, (
f"{path}: expected {count} occurrence(s) of {needle!r}, found {actual}. "
"The Google Chat channel override binds this definition; re-review "
"src/lib/messaging/channels/googlechat/runtime/hermes-adapter.py before "
"upgrading Hermes."
)


COMMANDS: dict[str, Callable[[], None]] = {
"cron-backup": verify_cron_backup,
"cron-create": verify_cron_create,
Expand All @@ -399,6 +428,7 @@ def reopen_probe(conn):
"discord-recovery-source": verify_discord_recovery_source,
"discord-reopen": verify_discord_reopen,
"gateway-process-identity": verify_gateway_process_identity,
"googlechat-override-seams": verify_googlechat_override_seams,
"gateway-runtime-metadata": verify_gateway_runtime_metadata,
"langfuse-credentials": verify_langfuse_credentials,
"neutral-platform-inertness": verify_neutral_platform_inertness,
Expand Down
41 changes: 41 additions & 0 deletions agents/hermes/plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
and quiet runtime grounding when Hermes runs inside an OpenShell sandbox
managed by NemoClaw.

Layout: channel-specific runtime overrides live in sibling modules loaded by
register() only when that channel is configured, so a sandbox without the
channel carries none of its behavior. Today that is googlechat_adapter.py.

Skill hot-reload: Hermes caches its skill slash-command registry in a
module-global dict on first scan. New skills dropped on disk are invisible
until the cache is cleared. This plugin provides a nemoclaw_reload_skills
Expand All @@ -22,9 +26,11 @@
"""

import atexit
import importlib.util
import inspect
import ipaddress
import json
import logging
import os
import re
import subprocess
Expand Down Expand Up @@ -96,6 +102,7 @@
"qqbot",
"yuanbao",
"webhook",
"google_chat",
)
_RAW_MESSAGING_TOOL_RE = re.compile(
r"^\s*send_message\s*:\s*(?P<body>.+?)\s*$",
Expand Down Expand Up @@ -1418,10 +1425,44 @@ def _handle_reload_skills(tool_input=None, context=None, **_kwargs):
return "\n".join(lines)


# Google Chat: the channel owns the override. Source lives in
# src/lib/messaging/channels/googlechat/runtime/hermes-adapter.py; the Hermes
# image copies it in beside this file. Loaded only when the channel is
# configured, so other sandboxes never replace the bundled platform entry.
_GOOGLE_CHAT_SUBSCRIPTION_ENV = "GOOGLE_CHAT_SUBSCRIPTION_NAME"
_GOOGLE_CHAT_MODULE = "googlechat_adapter.py"


def _install_googlechat_adapter(ctx):
"""Install the Google Chat override when that channel is configured.

The module is loaded by path: Hermes imports this plugin as a directory
module under a synthetic name, so a relative import has no package context.
Load failure must not abort plugin registration, but it has to be visible —
without the override the bundled gRPC adapter hangs under the REST-only
egress policy and the channel goes quiet with no other clue.
"""
if not _get_env_value(_GOOGLE_CHAT_SUBSCRIPTION_ENV):
return False
path = os.path.join(os.path.dirname(__file__), _GOOGLE_CHAT_MODULE)
try:
spec = importlib.util.spec_from_file_location("nemoclaw_hermes_googlechat", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module.install(ctx)
except Exception:
logging.getLogger("gateway.platforms.google_chat").exception(
"[GoogleChat][NemoClaw] loading %s failed", _GOOGLE_CHAT_MODULE,
)
return False
return True


def register(ctx):
"""Register NemoClaw tools and hooks with Hermes."""
_install_nous_tool_broker_patch()
_install_messaging_response_patch()
_install_googlechat_adapter(ctx)

# Register status tool
ctx.register_tool(
Expand Down
12 changes: 7 additions & 5 deletions src/lib/actions/sandbox/policy-channel-agent-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,10 @@ describe("channel lifecycle agent gate", () => {
["start", ["googlechat"], () => startSandboxChannel("da-test", { channel: "googlechat" })],
["stop", [], () => stopSandboxChannel("da-test", { channel: "googlechat" })],
])("rejects a stale channel during %s before reading channel state or mutating the sandbox", async (_verb, disabledChannels, run) => {
getSandboxMock.mockReturnValue({ name: "da-test", agent: "hermes" });
vi.spyOn(defs, "loadAgent").mockReturnValue(agentFixture("hermes"));
// googlechat now supports openclaw + hermes, so exercise the unsupported-pair
// lifecycle gate with a non-messaging custom agent (supported by no channel).
getSandboxMock.mockReturnValue({ name: "da-test", agent: "custom-agent" });
vi.spyOn(defs, "loadAgent").mockReturnValue(agentFixture("custom-agent"));
const configuredChannelsMock = vi
.spyOn(registry, "getConfiguredMessagingChannelsFromEntry")
.mockReturnValue(["googlechat"]);
Expand All @@ -176,9 +178,9 @@ describe("channel lifecycle agent gate", () => {
const errorText = (errSpy.mock.calls as unknown[][])
.map((call) => call.map(String).join(" "))
.join("\n");
expect(errorText).toMatch(/Channel 'googlechat' does not support agent 'hermes'/);
expect(errorText).toMatch(/Channel-supported agents: openclaw/);
expect(errorText).toMatch(/Channels supported by agent 'hermes':/);
expect(errorText).toMatch(/Channel 'googlechat' does not support agent 'custom-agent'/);
expect(errorText).toMatch(/Channel-supported agents: openclaw, hermes/);
expect(errorText).toMatch(/Channels supported by agent 'custom-agent': \(none\)/);

expect(configuredChannelsMock).not.toHaveBeenCalled();
expect(disabledChannelsMock).not.toHaveBeenCalled();
Expand Down
3 changes: 3 additions & 0 deletions src/lib/actions/sandbox/policy-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,9 @@ async function applyChannelAddToGatewayAndRegistry(
// deferred rebuild cannot configure it.
const bridgeDefs = collectMessagingBridgeTokenDefs({
sandboxName,
// Unnormalized: the bridge profile filter owns the unset default and rejects
// an agent no profile declares.
agent: registry.getSandbox(sandboxName)?.agent,
enabledChannels: [channelName],
disabledChannelNames: new Set<string>(),
getCredential,
Expand Down
2 changes: 2 additions & 0 deletions src/lib/messaging-channel-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ describe("messaging channel config", () => {
"GOOGLECHAT_AUDIENCE",
"GOOGLECHAT_APP_PRINCIPAL",
"GOOGLECHAT_ALLOWED_USERS",
"GOOGLE_CHAT_PROJECT_ID",
"GOOGLE_CHAT_SUBSCRIPTION_NAME",
]);
});

Expand Down
10 changes: 9 additions & 1 deletion src/lib/messaging/applier/build/messaging-build-applier.mts
Original file line number Diff line number Diff line change
Expand Up @@ -1882,7 +1882,15 @@ function installHermesUvPackages(selectedPackages: readonly string[], env: Env):
"--",
...selectedPackages,
],
env,
// uv (rustls) ignores the corporate-only SSL_CERT_FILE, so a PyPI fetch
// behind a MITM proxy fails with `UnknownIssuer`. Point it at the merged
// system bundle instead; harmless off-proxy, and UV_SYSTEM_CERTS is the
// current name for UV_NATIVE_TLS.
{
...env,
UV_SYSTEM_CERTS: "1",
SSL_CERT_FILE: "/etc/ssl/certs/ca-certificates.crt",
},
);
}

Expand Down
9 changes: 9 additions & 0 deletions src/lib/messaging/applier/setup-applier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ const ALL_CHANNEL_ENV = {
MSTEAMS_TENANT_ID: "teams-tenant-id",
TEAMS_ALLOWED_USERS: "00000000-0000-0000-0000-000000000001",
MSTEAMS_PORT: "3978",
// The service-account validator only requires non-empty client_email and
// private_key, so keep the fixture free of PEM markers: a real-looking key
// block trips the detect-private-key pre-commit hook.
GOOGLECHAT_SERVICE_ACCOUNT:
'{"client_email":"bot@demo.iam.gserviceaccount.com","private_key":"test-key-material","project_id":"demo"}',
GOOGLE_CHAT_PROJECT_ID: "demo",
GOOGLE_CHAT_SUBSCRIPTION_NAME: "projects/demo/subscriptions/hermes-chat-events-sub",
GOOGLECHAT_ALLOWED_USERS: "user@example.com",
} as const;

const ALL_CHANNELS = createBuiltInChannelManifestRegistry()
Expand Down Expand Up @@ -621,6 +629,7 @@ describe("MessagingSetupApplier", () => {
"slack",
"whatsapp",
"teams",
"googlechat_hermes",
]);
expect(renderResult.appliedTargets).toEqual([
"/sandbox/.hermes/.env",
Expand Down
Loading
Loading