Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
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
10 changes: 6 additions & 4 deletions agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER=${NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER} \
NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64=${NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64}

# Apply messaging agent-install hooks as root so Hermes Python packages can update
# /opt/hermes/.venv before the runtime drops to the sandbox user.
WORKDIR /opt/hermes
# hadolint ignore=DL3059
RUN node --experimental-strip-types /src/lib/messaging/applier/build/messaging-build-applier.mts --agent hermes --phase agent-install

WORKDIR /sandbox
USER sandbox

Expand All @@ -154,10 +160,6 @@ RUN mkdir -p /sandbox/.nemoclaw/blueprints/0.1.0 \
# code injection via build-arg interpolation (same concern as OpenClaw C-2).
RUN node --experimental-strip-types /opt/nemoclaw-hermes-config/generate-config.ts

# Apply messaging agent-install hooks before Hermes plugin installation.
# hadolint ignore=DL3059
RUN node --experimental-strip-types /src/lib/messaging/applier/build/messaging-build-applier.mts --agent hermes --phase agent-install

# Install NemoClaw plugin into Hermes
# hadolint ignore=DL3059
RUN mkdir -p /sandbox/.hermes/plugins/nemoclaw \
Expand Down
2 changes: 2 additions & 0 deletions agents/hermes/Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ RUN printf '%s\n' \
# and pty (optional browser TUI bridge). These extras are resolved from the
# selected Hermes release's uv.lock via `uv sync --frozen`, so dependency
# changes remain tied to HERMES_VERSION/HERMES_TARBALL_SHA256 review.
# Microsoft Teams adapter dependencies are installed by the manifest-driven
# final image when selected.
# New Hermes integrations should be installed by the agent workflow when they
# are enabled rather than shipped in the base image by default.
# Root Node dependencies provide Hermes browser tooling such as agent-browser.
Expand Down
2 changes: 2 additions & 0 deletions agents/hermes/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,15 @@ web_auth_env: API_SERVER_KEY
# https://hermes-agent.nousresearch.com/docs/user-guide/messaging/weixin.
# WhatsApp pairs in the sandbox via `hermes whatsapp`; the selected channel
# bakes WHATSAPP_ENABLED/WHATSAPP_MODE into .env and preserves session state.
# Microsoft Teams uses the Bot Framework webhook adapter at /api/messages.
messaging_platforms:
supported:
- telegram
- discord
- slack
- wechat
- whatsapp
- teams
# Future: signal, matrix, mattermost, email, etc.
# Each needs a network policy entry before enabling.

Expand Down
80 changes: 80 additions & 0 deletions agents/hermes/policy-additions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,86 @@ network_policies:
- { path: /usr/bin/python3* }
- { path: /opt/hermes/.venv/bin/python }

teams:
name: teams
endpoints:
- host: login.microsoftonline.com
port: 443
protocol: rest
enforcement: enforce
request_body_credential_rewrite: true
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
- host: login.botframework.com
port: 443
protocol: rest
enforcement: enforce
request_body_credential_rewrite: true
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
- host: api.botframework.com
port: 443
protocol: rest
enforcement: enforce
request_body_credential_rewrite: true
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
- host: smba.trafficmanager.net
port: 443
protocol: rest
enforcement: enforce
request_body_credential_rewrite: true
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
- allow: { method: PUT, path: "/**" }
- allow: { method: DELETE, path: "/**" }
- host: graph.microsoft.com
port: 443
protocol: rest
enforcement: enforce
request_body_credential_rewrite: true
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
- host: teams.microsoft.com
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- host: teams.cdn.office.net
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- host: statics.teams.cdn.office.net
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- host: "*.sharepoint.com"
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- host: 1drv.ms
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
binaries:
- { path: /usr/local/bin/hermes }
- { path: /usr/bin/python3* }
- { path: /opt/hermes/.venv/bin/python }

# WeChat (personal) via Tencent's iLink Bot API. The Hermes adapter uses
# HTTP long-polling (no WebSocket). WEIXIN_TOKEN is L7-resolved at egress
# from WECHAT_BOT_TOKEN (same credential slot OpenClaw's bridge uses) via
Expand Down
1 change: 1 addition & 0 deletions agents/openclaw/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ messaging_platforms:
- slack
- wechat
- whatsapp
- teams

# ── Inference ───────────────────────────────────────────────────
inference:
Expand Down
91 changes: 91 additions & 0 deletions nemoclaw-blueprint/policies/presets/teams.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

preset:
name: teams
description: "Microsoft Teams Bot Framework and Graph API access"

network_policies:
teams:
name: teams
endpoints:
# Azure AD app credential exchange for the Bot Framework and Graph scopes.
- host: login.microsoftonline.com
port: 443
protocol: rest
enforcement: enforce
request_body_credential_rewrite: true
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
# Bot Framework token scope, OpenID metadata, and connector calls.
- host: login.botframework.com
port: 443
protocol: rest
enforcement: enforce
request_body_credential_rewrite: true
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
- host: api.botframework.com
port: 443
protocol: rest
enforcement: enforce
request_body_credential_rewrite: true
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
# Public Teams Bot Connector service URL used by incoming activities.
- host: smba.trafficmanager.net
port: 443
protocol: rest
enforcement: enforce
request_body_credential_rewrite: true
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
- allow: { method: PUT, path: "/**" }
- allow: { method: DELETE, path: "/**" }
# Teams plugin media and delegated context helpers use Microsoft Graph.
- host: graph.microsoft.com
port: 443
protocol: rest
enforcement: enforce
request_body_credential_rewrite: true
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
# Read-only Teams/Office media surfaces referenced by Teams messages.
- host: teams.microsoft.com
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- host: teams.cdn.office.net
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- host: statics.teams.cdn.office.net
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- host: "*.sharepoint.com"
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- host: 1drv.ms
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
binaries:
- { path: /usr/local/bin/node }
- { path: /usr/bin/node }
1 change: 1 addition & 0 deletions nemoclaw-blueprint/policies/tiers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@ tiers:
- { name: telegram, access: read-write }
- { name: wechat, access: read-write }
- { name: whatsapp, access: read-write }
- { name: teams, access: read-write }
- { name: jira, access: read-write }
- { name: outlook, access: read-write }
2 changes: 1 addition & 1 deletion src/lib/actions/sandbox/channel-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const PROBED_AT = new Date("2026-05-28T04:00:00.000Z");
function fakeAgent(name: "openclaw" | "hermes" = "openclaw"): AgentDefinition {
const configDir = name === "openclaw" ? "/sandbox/.openclaw" : "/sandbox/.hermes";
const stateDirs = name === "openclaw" ? ["whatsapp"] : ["platforms"];
const messagingPlatforms = ["telegram", "discord", "slack", "wechat", "whatsapp"];
const messagingPlatforms = ["telegram", "discord", "slack", "wechat", "whatsapp", "teams"];
return {
name,
agentDir: `/fake/${name}`,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/actions/sandbox/policy-channel-conflict.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ beforeEach(() => {
// Agent gate: support every channel.
vi.spyOn(defs, "loadAgent").mockReturnValue({
name: "openclaw",
messagingPlatforms: ["telegram", "discord", "slack", "wechat", "whatsapp"],
messagingPlatforms: ["telegram", "discord", "slack", "wechat", "whatsapp", "teams"],
});

// Policy seam. addSandboxChannel gates on loadPreset()/parsePresetPolicyKeys()
Expand Down
12 changes: 10 additions & 2 deletions src/lib/actions/sandbox/policy-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,11 @@ async function persistManifestChannelDisabledPlan(
const entry = registry.getSandbox(sandboxName);
if (!entry?.messaging?.plan) return false;
const agent = resolveAgentForSandbox(sandboxName);
const planner = new MessagingWorkflowPlanner(messagingManifestRegistry);
const planner = new MessagingWorkflowPlanner(
messagingManifestRegistry,
undefined,
createBuiltInRenderTemplateResolver(),
);
const context = {
sandboxName,
agent: toMessagingAgentId(agent),
Expand All @@ -784,7 +788,11 @@ async function persistManifestChannelRemovePlan(
const entry = registry.getSandbox(sandboxName);
if (!entry) return false;
const agent = resolveAgentForSandbox(sandboxName);
const planner = new MessagingWorkflowPlanner(messagingManifestRegistry);
const planner = new MessagingWorkflowPlanner(
messagingManifestRegistry,
undefined,
createBuiltInRenderTemplateResolver(),
);
const plan = await planner.buildChannelRemovePlanFromSandboxEntry({
sandboxName,
agent: toMessagingAgentId(agent),
Expand Down
Loading