Skip to content
Closed
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 @@ -43,6 +43,7 @@ RUN chmod -R a+rX /opt/nemoclaw-hermes-plugin/
# Copy config generator, Discord facade, and URL-decode proxy
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 {} +
COPY agents/hermes/decode-proxy.py /usr/local/bin/nemoclaw-decode-proxy
Expand Down Expand Up @@ -76,6 +77,8 @@ ARG NEMOCLAW_MESSAGING_CHANNELS_B64=W10=
ARG NEMOCLAW_MESSAGING_ALLOWED_IDS_B64=e30=
ARG NEMOCLAW_DISCORD_GUILDS_B64=e30=
ARG NEMOCLAW_TELEGRAM_CONFIG_B64=e30=
ARG NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER=0
ARG NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64=W10=
ARG NEMOCLAW_BUILD_ID=default

# Promote build-args to env vars for the config generation script.
Expand All @@ -86,7 +89,9 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
NEMOCLAW_MESSAGING_CHANNELS_B64=${NEMOCLAW_MESSAGING_CHANNELS_B64} \
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_TELEGRAM_CONFIG_B64=${NEMOCLAW_TELEGRAM_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 @@ -22,6 +22,10 @@ export type HermesBuildSettings = {
baseUrl: string;
providerKey: string;
inferenceApi: string;
managedToolGateways: {
brokerEnabled: boolean;
presets: string[];
};
messaging: {
enabledChannels: Set<string>;
allowedIds: MessagingAllowedIds;
Expand All @@ -39,6 +43,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
13 changes: 13 additions & 0 deletions agents/hermes/config/hermes-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
// 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";

export function buildHermesConfig(settings: HermesBuildSettings): Record<string, unknown> {
Expand Down Expand Up @@ -40,6 +44,15 @@ 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);
}
}

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;
}
}
}
15 changes: 15 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 } from "./build-env.ts";
import { loadManagedToolGatewayMatrix } from "./managed-tool-gateway.ts";

const CHANNEL_TOKEN_ENVS: Record<string, string[]> = {
telegram: ["TELEGRAM_BOT_TOKEN"],
Expand All @@ -16,9 +17,23 @@ export function buildMessagingEnvLines(
enabledChannels: Set<string>,
allowedIds: MessagingAllowedIds,
discordGuilds: DiscordGuilds,
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");
envLines.push(
"TOOL_GATEWAY_USER_TOKEN=openshell:resolve:env:NEMOCLAW_HERMES_TOOL_GATEWAY_REFRESH_TOKEN",
);
for (const preset of managedToolGatewayPresets) {
const entry = matrix[preset];
if (!entry) continue;
envLines.push(`${entry.envKey}=${entry.envValue}`);
}
}

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 @@ -41,6 +41,9 @@ function main(): void {
settings.messaging.enabledChannels,
settings.messaging.allowedIds,
settings.messaging.discordGuilds,
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