Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
9 changes: 8 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,9 @@ 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_HERMES_TOOL_BROKER_TOKEN=
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
ARG NEMOCLAW_BUILD_ID=default
ARG NEMOCLAW_DARWIN_VM_COMPAT=0

Expand All @@ -100,7 +104,10 @@ 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} \
NEMOCLAW_HERMES_TOOL_BROKER_TOKEN=${NEMOCLAW_HERMES_TOOL_BROKER_TOKEN}

WORKDIR /sandbox
USER sandbox
Expand Down
14 changes: 14 additions & 0 deletions agents/hermes/config/build-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export type HermesBuildSettings = {
baseUrl: string;
providerKey: string;
inferenceApi: string;
managedToolGateways: {
brokerEnabled: boolean;
brokerToken: string;
presets: string[];
};
messaging: {
enabledChannels: Set<string>;
allowedIds: MessagingAllowedIds;
Expand All @@ -51,6 +56,15 @@ 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",
brokerToken: env.NEMOCLAW_HERMES_TOOL_BROKER_TOKEN || "",
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
44 changes: 44 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,21 @@ 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) continue;
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[] = [],
managedToolBrokerToken = "",
): 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");
envLines.push(`TOOL_GATEWAY_USER_TOKEN=${managedToolBrokerToken}`);
for (const preset of managedToolGatewayPresets) {
const entry = matrix[preset];
if (!entry) continue;
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
4 changes: 4 additions & 0 deletions agents/hermes/generate-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ function main(): void {
settings.messaging.allowedIds,
settings.messaging.discordGuilds,
settings.messaging.wechatConfig,
settings.managedToolGateways.brokerEnabled
? settings.managedToolGateways.presets
: [],
settings.managedToolGateways.brokerToken,
);
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