Skip to content
Draft
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
11 changes: 10 additions & 1 deletion agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ RUN chmod -R a+rX /opt/nemoclaw-hermes-plugin/
# Copy config generator 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 scripts/hermes-managed-tool-gateway-matrix.json /opt/nemoclaw-hermes-config/hermes-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 All @@ -59,7 +60,9 @@ RUN chmod 755 /usr/local/bin/nemoclaw-start /usr/local/lib/nemoclaw/sandbox-init
# Build args for config that varies per deployment.
ARG NEMOCLAW_MODEL=nvidia/nemotron-3-super-120b-a12b
ARG NEMOCLAW_PROVIDER_KEY=custom
ARG NEMOCLAW_PRIMARY_MODEL_REF=inference/claude-opus-4-7
ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1
ARG NEMOCLAW_INFERENCE_API=openai-completions
# CHAT_UI_URL is a legacy name shared with the OpenClaw build arg. For
# Hermes this URL points at the OpenAI-compatible API server (port 8642,
# exposing /v1 and /health), NOT a browser chat UI. Callers authenticate
Expand All @@ -70,17 +73,23 @@ 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_PRESETS_B64=W10=
ARG NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER=0
ARG NEMOCLAW_BUILD_ID=default

# Promote build-args to env vars for the config generation script.
ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
NEMOCLAW_PROVIDER_KEY=${NEMOCLAW_PROVIDER_KEY} \
NEMOCLAW_PRIMARY_MODEL_REF=${NEMOCLAW_PRIMARY_MODEL_REF} \
NEMOCLAW_INFERENCE_BASE_URL=${NEMOCLAW_INFERENCE_BASE_URL} \
NEMOCLAW_INFERENCE_API=${NEMOCLAW_INFERENCE_API} \
CHAT_UI_URL=${CHAT_UI_URL} \
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_PRESETS_B64=${NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64} \
NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER=${NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER}

WORKDIR /sandbox
USER sandbox
Expand Down
8 changes: 8 additions & 0 deletions agents/hermes/config/build-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export type HermesBuildSettings = {
baseUrl: string;
providerKey: string;
inferenceApi: string;
toolGatewayPresets: string[];
toolGatewayBrokerEnabled: boolean;
messaging: {
enabledChannels: Set<string>;
allowedIds: MessagingAllowedIds;
Expand All @@ -39,6 +41,12 @@ export function readHermesBuildSettings(env: NodeJS.ProcessEnv): HermesBuildSett
baseUrl,
providerKey: env.NEMOCLAW_PROVIDER_KEY || "custom",
inferenceApi: env.NEMOCLAW_INFERENCE_API || "",
toolGatewayPresets: readBase64Json<string[]>(
env,
"NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64",
"W10=",
),
toolGatewayBrokerEnabled: env.NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER === "1",
messaging: {
enabledChannels: new Set(
readBase64Json<string[]>(env, "NEMOCLAW_MESSAGING_CHANNELS_B64", "W10="),
Expand Down
27 changes: 26 additions & 1 deletion agents/hermes/config/hermes-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,29 @@
// 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 mapHermesProvider(providerKey: string): string {
switch (providerKey) {
case "anthropic":
return "anthropic";
case "openai":
return "openai";
default:
return "custom";
}
}

export function buildHermesConfig(settings: HermesBuildSettings): Record<string, unknown> {
const config: Record<string, unknown> = {
_config_version: 12,
model: {
default: settings.model,
provider: "custom",
provider: mapHermesProvider(settings.providerKey),
base_url: settings.baseUrl,
},
terminal: {
Expand Down Expand Up @@ -50,6 +65,16 @@ export function buildHermesConfig(settings: HermesBuildSettings): Record<string,
};
}

if (settings.toolGatewayPresets.length > 0) {
const matrix = loadManagedToolGatewayMatrix();
for (const preset of settings.toolGatewayPresets) {
const entry = matrix[preset];
if (entry) {
applyManagedToolConfig(config, entry.config);
}
}
}

// API server — internal port only.
// Hermes binds to 127.0.0.1 regardless of config (upstream bug).
// socat in start.sh forwards 0.0.0.0:8642 -> 127.0.0.1:18642.
Expand Down
55 changes: 55 additions & 0 deletions agents/hermes/config/managed-tool-gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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, "../../../scripts/hermes-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;
}
}
}
37 changes: 37 additions & 0 deletions agents/hermes/config/messaging-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,54 @@
// SPDX-License-Identifier: Apache-2.0

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

const CHANNEL_TOKEN_ENVS: Record<string, string[]> = {
telegram: ["TELEGRAM_BOT_TOKEN"],
discord: ["DISCORD_BOT_TOKEN"],
slack: ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"],
};

const PROVIDER_API_KEY_ENV: Record<string, string> = {
anthropic: "ANTHROPIC_API_KEY",
openai: "OPENAI_API_KEY",
};

export function buildMessagingEnvLines(
enabledChannels: Set<string>,
allowedIds: MessagingAllowedIds,
discordGuilds: DiscordGuilds,
providerKey = "custom",
toolGatewayPresets: string[] = [],
toolGatewayBrokerEnabled = false,
): string[] {
const envLines = ["API_SERVER_PORT=18642", "API_SERVER_HOST=127.0.0.1"];

if (enabledChannels.has("discord") || toolGatewayBrokerEnabled || toolGatewayPresets.length > 0) {
envLines.push(
`API_SERVER_KEY=${process.env.NEMOCLAW_HERMES_API_SERVER_KEY?.trim() || randomBytes(32).toString("hex")}`,
);
}

const providerCredEnv = PROVIDER_API_KEY_ENV[providerKey];
if (providerCredEnv) {
envLines.push(`${providerCredEnv}=openshell:resolve:env:${providerCredEnv}`);
}

if (toolGatewayBrokerEnabled || toolGatewayPresets.length > 0) {
envLines.push("NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER=1");
envLines.push(
"TOOL_GATEWAY_USER_TOKEN=openshell:resolve:env:NEMOCLAW_HERMES_TOOL_BROKER_TOKEN",
);
const matrix = loadManagedToolGatewayMatrix();
const selected = new Set(toolGatewayPresets);
for (const [preset, entry] of Object.entries(matrix)) {
if (selected.size > 0 && !selected.has(preset)) continue;
envLines.push(`${entry.envKey}=${entry.envValue}`);
}
}

for (const channel of enabledChannels) {
const envKeys = CHANNEL_TOKEN_ENVS[channel] ?? [];
for (const envKey of envKeys) {
Expand All @@ -30,6 +64,9 @@ export function buildMessagingEnvLines(
if (allowedIds.telegram?.length) {
envLines.push(`TELEGRAM_ALLOWED_USERS=${allowedIds.telegram.map(String).join(",")}`);
}
if (allowedIds.slack?.length) {
envLines.push(`SLACK_ALLOWED_USERS=${allowedIds.slack.map(String).join(",")}`);
}

return envLines;
}
Expand Down
7 changes: 6 additions & 1 deletion agents/hermes/generate-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,15 @@ function main(): void {
settings.messaging.enabledChannels,
settings.messaging.allowedIds,
settings.messaging.discordGuilds,
settings.providerKey,
settings.toolGatewayPresets,
settings.toolGatewayBrokerEnabled,
);
const written = writeHermesConfigFiles(config, envLines);

console.log(`[config] Wrote ${written.configPath} (model=${settings.model}, provider=custom)`);
console.log(
`[config] Wrote ${written.configPath} (model=${settings.model}, provider=${String((config.model as Record<string, unknown>).provider)})`,
);
console.log(`[config] Wrote ${written.envPath} (${written.envEntryCount} entries)`);
}

Expand Down
Loading
Loading