Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 6 additions & 1 deletion agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ RUN chmod -R a+rX /opt/nemoclaw-hermes-plugin/
# Copy config generator
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/host/managed-tool-gateway-matrix.json /opt/nemoclaw-hermes-config/managed-tool-gateway-matrix.json
RUN find /opt/nemoclaw-hermes-config -type d -exec chmod 755 {} + \
&& find /opt/nemoclaw-hermes-config -type f -exec chmod 444 {} +

Expand Down Expand Up @@ -88,6 +89,8 @@ ARG NEMOCLAW_TELEGRAM_CONFIG_B64=e30=
# is never baked here — it flows through the OpenShell L7 proxy via the
# WECHAT_BOT_TOKEN credential slot.
ARG NEMOCLAW_WECHAT_CONFIG_B64=e30=
ARG NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER=0
ARG NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64=W10=
ARG NEMOCLAW_BUILD_ID=default
ARG NEMOCLAW_DARWIN_VM_COMPAT=0

Expand All @@ -100,7 +103,9 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
NEMOCLAW_MESSAGING_ALLOWED_IDS_B64=${NEMOCLAW_MESSAGING_ALLOWED_IDS_B64} \
NEMOCLAW_DISCORD_GUILDS_B64=${NEMOCLAW_DISCORD_GUILDS_B64} \
NEMOCLAW_TELEGRAM_CONFIG_B64=${NEMOCLAW_TELEGRAM_CONFIG_B64} \
NEMOCLAW_WECHAT_CONFIG_B64=${NEMOCLAW_WECHAT_CONFIG_B64}
NEMOCLAW_WECHAT_CONFIG_B64=${NEMOCLAW_WECHAT_CONFIG_B64} \
NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER=${NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER} \
NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64=${NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64}

WORKDIR /sandbox
USER sandbox
Expand Down
12 changes: 12 additions & 0 deletions agents/hermes/config/build-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export type HermesBuildSettings = {
baseUrl: string;
providerKey: string;
inferenceApi: string;
managedToolGateways: {
brokerEnabled: boolean;
presets: string[];
};
messaging: {
enabledChannels: Set<string>;
allowedIds: MessagingAllowedIds;
Expand All @@ -51,6 +55,14 @@ export function readHermesBuildSettings(env: NodeJS.ProcessEnv): HermesBuildSett
baseUrl,
providerKey: env.NEMOCLAW_PROVIDER_KEY || "custom",
inferenceApi: env.NEMOCLAW_INFERENCE_API || "",
managedToolGateways: {
brokerEnabled: env.NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER === "1",
presets: readBase64Json<string[]>(
env,
"NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64",
"W10=",
),
},
messaging: {
enabledChannels: new Set(
readBase64Json<string[]>(env, "NEMOCLAW_MESSAGING_CHANNELS_B64", "W10="),
Expand Down
46 changes: 46 additions & 0 deletions agents/hermes/config/hermes-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,32 @@
// SPDX-License-Identifier: Apache-2.0

import type { HermesBuildSettings } from "./build-env.ts";
import {
applyManagedToolConfig,
loadManagedToolGatewayMatrix,
} from "./managed-tool-gateway.ts";
import { buildDiscordConfig } from "./messaging-config.ts";

const API_SERVER_TOOLSETS = [
"web",
"browser",
"terminal",
"file",
"code_execution",
"vision",
"image_gen",
"skills",
"todo",
"memory",
"session_search",
"delegation",
"cronjob",
"nemoclaw",
"audio",
];

export function buildHermesConfig(settings: HermesBuildSettings): Record<string, unknown> {
const apiServerToolsets = [...API_SERVER_TOOLSETS];
const config: Record<string, unknown> = {
_config_version: 12,
model: {
Expand All @@ -31,6 +54,12 @@ export function buildHermesConfig(settings: HermesBuildSettings): Record<string,
compact: false,
tool_progress: "all",
},
plugins: {
enabled: ["nemoclaw"],
},
platform_toolsets: {
api_server: apiServerToolsets,
},
};

// Hermes v2026.4.23 reads Discord behavior from top-level `discord:`.
Expand All @@ -40,6 +69,23 @@ export function buildHermesConfig(settings: HermesBuildSettings): Record<string,
config.discord = buildDiscordConfig(settings.messaging.discordGuilds);
}

if (settings.managedToolGateways.brokerEnabled) {
const matrix = loadManagedToolGatewayMatrix();
for (const preset of settings.managedToolGateways.presets) {
const entry = matrix[preset];
if (!entry) {
throw new Error(`Unknown Hermes managed-tool gateway preset: ${preset}`);
}
applyManagedToolConfig(config, entry.config);
}
if (
settings.managedToolGateways.presets.includes("nous-audio") &&
!apiServerToolsets.includes("tts")
) {
apiServerToolsets.push("tts");
}
}

const telegramConfig = settings.messaging.telegramConfig;
if (
settings.messaging.enabledChannels.has("telegram") &&
Expand Down
56 changes: 56 additions & 0 deletions agents/hermes/config/managed-tool-gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

export type ManagedToolGatewayEntry = {
service: string;
config: Record<string, unknown>;
envKey: string;
envValue: string;
};

export type ManagedToolGatewayMatrix = Record<string, ManagedToolGatewayEntry>;

export function loadManagedToolGatewayMatrix(): ManagedToolGatewayMatrix {
const scriptDir = dirname(fileURLToPath(import.meta.url));
const candidates = [
process.env.NEMOCLAW_HERMES_TOOL_GATEWAY_MATRIX_PATH,
join(scriptDir, "hermes-managed-tool-gateway-matrix.json"),
join(scriptDir, "../hermes-managed-tool-gateway-matrix.json"),
join(scriptDir, "../host/managed-tool-gateway-matrix.json"),
"/opt/nemoclaw-hermes-config/managed-tool-gateway-matrix.json",
].filter((candidate): candidate is string => Boolean(candidate));

for (const candidate of candidates) {
if (!existsSync(candidate)) continue;
return JSON.parse(readFileSync(candidate, "utf8")) as ManagedToolGatewayMatrix;
}

throw new Error("Hermes managed tool gateway matrix not found");
}

export function applyManagedToolConfig(
config: Record<string, unknown>,
entryConfig: Record<string, unknown>,
): void {
for (const [section, sectionValue] of Object.entries(entryConfig)) {
if (
sectionValue &&
typeof sectionValue === "object" &&
!Array.isArray(sectionValue) &&
config[section] &&
typeof config[section] === "object" &&
!Array.isArray(config[section])
) {
config[section] = {
...(config[section] as Record<string, unknown>),
...(sectionValue as Record<string, unknown>),
};
} else {
config[section] = sectionValue;
}
}
}
14 changes: 14 additions & 0 deletions agents/hermes/config/messaging-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import type { DiscordGuilds, MessagingAllowedIds, WechatConfig } from "./build-env.ts";
import { loadManagedToolGatewayMatrix } from "./managed-tool-gateway.ts";

// Maps each Hermes-supported channel to the in-sandbox env-var name(s) the
// adapter reads. The values are the names Hermes expects — not the names
Expand All @@ -23,9 +24,22 @@ export function buildMessagingEnvLines(
allowedIds: MessagingAllowedIds,
discordGuilds: DiscordGuilds,
wechatConfig: WechatConfig,
managedToolGatewayPresets: string[] = [],
): string[] {
const envLines = ["API_SERVER_PORT=18642", "API_SERVER_HOST=127.0.0.1"];

if (managedToolGatewayPresets.length > 0) {
const matrix = loadManagedToolGatewayMatrix();
envLines.push("NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER=1");
for (const preset of managedToolGatewayPresets) {
const entry = matrix[preset];
if (!entry) {
throw new Error(`Unknown Hermes managed-tool gateway preset: ${preset}`);
}
envLines.push(`${entry.envKey}=${entry.envValue}`);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

for (const channel of enabledChannels) {
const envKeys = CHANNEL_TOKEN_ENVS[channel] ?? [];
for (const envKey of envKeys) {
Expand Down
3 changes: 3 additions & 0 deletions agents/hermes/generate-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ function main(): void {
settings.messaging.allowedIds,
settings.messaging.discordGuilds,
settings.messaging.wechatConfig,
settings.managedToolGateways.brokerEnabled
? settings.managedToolGateways.presets
: [],
);
const written = writeHermesConfigFiles(config, envLines);

Expand Down
116 changes: 116 additions & 0 deletions agents/hermes/host/managed-tool-gateway-matrix.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{
"nous-web": {
"service": "firecrawl",
"description": "Nous Portal managed web search and crawl gateway",
"config": {
"web": {
"backend": "firecrawl",
"use_gateway": true
}
},
"envKey": "FIRECRAWL_GATEWAY_URL",
"envValue": "http://host.openshell.internal:11436/firecrawl",
"brokerPath": "/firecrawl",
"upstream": "https://firecrawl-gateway.nousresearch.com",
"sandboxAuthHeaders": ["Authorization: Bearer", "x-firecrawl-api-key", "x-api-key"],
"upstreamAuthHeader": "Authorization: Bearer",
"policyPreset": "nous-web",
"tools": ["web_search", "web_extract"]
},
"nous-audio": {
"service": "openai-audio",
"description": "Nous Portal managed audio generation and transcription gateway",
"config": {
"tts": {
"provider": "openai",
"use_gateway": true
},
"stt": {
"provider": "openai",
"use_gateway": true
}
},
"envKey": "OPENAI_AUDIO_GATEWAY_URL",
"envValue": "http://host.openshell.internal:11436/openai-audio",
"brokerPath": "/openai-audio",
"upstream": "https://openai-audio-gateway.nousresearch.com",
"sandboxAuthHeaders": ["Authorization: Bearer", "openai-api-key", "x-api-key"],
"upstreamAuthHeader": "Authorization: Bearer",
"policyPreset": "nous-audio",
"tools": ["text_to_speech", "transcribe_audio"]
},
"nous-browser": {
"service": "browser-use",
"description": "Nous Portal managed browser automation gateway",
"config": {
"browser": {
"cloud_provider": "browser-use",
"use_gateway": true
}
},
"envKey": "BROWSER_USE_GATEWAY_URL",
"envValue": "http://host.openshell.internal:11436/browser-use",
"brokerPath": "/browser-use",
"upstream": "https://browser-use-gateway.nousresearch.com",
"sandboxAuthHeaders": ["X-Browser-Use-API-Key", "x-api-key"],
"upstreamAuthHeader": "X-Browser-Use-API-Key",
"policyPreset": "nous-browser",
"tools": [
"browser_navigate",
"browser_snapshot",
"browser_click",
"browser_type",
"browser_scroll",
"browser_back",
"browser_press"
],
"transportExceptions": [
"*.cdp1.browser-use.com",
"*.cdp2.browser-use.com",
"*.cdp3.browser-use.com",
"*.cdp4.browser-use.com",
"*.cdp5.browser-use.com",
"*.cdp6.browser-use.com",
"*.cdp7.browser-use.com",
"*.cdp8.browser-use.com",
"*.cdp9.browser-use.com",
"*.cdp10.browser-use.com"
]
},
"nous-image": {
"service": "fal-queue",
"description": "Nous Portal managed image generation gateway",
"config": {
"image_gen": {
"use_gateway": true
}
},
"envKey": "FAL_QUEUE_GATEWAY_URL",
"envValue": "http://host.openshell.internal:11436/fal-queue",
"brokerPath": "/fal-queue",
"upstream": "https://fal-queue-gateway.nousresearch.com",
"sandboxAuthHeaders": ["Authorization: Key", "x-fal-key", "x-api-key"],
"upstreamAuthHeader": "Authorization: Key",
"policyPreset": "nous-image",
"tools": ["image_generate"]
},
"nous-code": {
"service": "modal",
"description": "Nous Portal managed sandboxed code execution gateway",
"config": {
"terminal": {
"backend": "modal",
"modal_mode": "managed",
"timeout": 180
}
},
"envKey": "MODAL_GATEWAY_URL",
"envValue": "http://host.openshell.internal:11436/modal",
"brokerPath": "/modal",
"upstream": "https://modal-gateway.nousresearch.com",
"sandboxAuthHeaders": ["Authorization: Bearer", "x-api-key"],
"upstreamAuthHeader": "Authorization: Bearer",
"policyPreset": "nous-code",
"tools": ["terminal"]
}
}
Loading
Loading