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
5 changes: 3 additions & 2 deletions agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ COPY agents/hermes/patch-cron-restore-drain.py /opt/nemoclaw-hermes-config/patch
COPY agents/hermes/patch-neutral-platform-env-activation.py /opt/nemoclaw-hermes-config/patch-neutral-platform-env-activation.py
COPY agents/hermes/host/managed-tool-gateway-matrix.json /opt/nemoclaw-hermes-config/managed-tool-gateway-matrix.json
COPY src/lib/hermes-managed-route.ts /src/lib/hermes-managed-route.ts
COPY src/lib/hermes-switchyard-routing.ts /src/lib/hermes-switchyard-routing.ts
COPY src/lib/tool-disclosure.ts /src/lib/tool-disclosure.ts
COPY src/lib/messaging/ /src/lib/messaging/
COPY scripts/lib/openclaw-npm-remediation.mts /scripts/lib/openclaw-npm-remediation.mts
Expand Down Expand Up @@ -405,7 +406,7 @@ RUN chmod -R a+rX /opt/nemoclaw-hermes-plugin/
# read-only.
RUN find /opt/nemoclaw-hermes-config -type d -exec chmod 755 {} + \
&& find /opt/nemoclaw-hermes-config -type f -exec chmod 444 {} + \
&& chmod 444 /src/lib/hermes-managed-route.ts /src/lib/tool-disclosure.ts \
&& chmod 444 /src/lib/hermes-managed-route.ts /src/lib/hermes-switchyard-routing.ts /src/lib/tool-disclosure.ts \
&& chmod 444 /scripts/lib/reviewed-npm-archive.mts /scripts/lib/bundled-npm-package.mts \
/scripts/lib/openclaw-npm-remediation.mts \
/scripts/patch-bundled-npm-brace-expansion.mts /scripts/lib/patch-bundled-npm-ip-address.mts \
Expand Down Expand Up @@ -704,7 +705,7 @@ RUN node --experimental-strip-types \
ARG NEMOCLAW_HERMES_WRAPPER_SHA256=f4276e9833638b7a620176c88bd329d6b6d4948538a3227b727a1397146a0e0e
ARG NEMOCLAW_HERMES_CLI_ADAPTER_SHA256=989edf54a8c09c6efb348600a8aa2f264c0b71408eb9d7bcd579b92cbeccf9b1
ARG NEMOCLAW_HERMES_CLI_ADAPTER_VALIDATOR_SHA256=db4046e79e513eab67b069a8eda20167b8b65529cf26842531d2ad673c670330
ARG NEMOCLAW_HERMES_VALIDATOR_SHA256=b355d1365fb1d15475e327f312ceb854ae96f9ebed28cf96bc8817f550df2688
ARG NEMOCLAW_HERMES_VALIDATOR_SHA256=104443a6b509844e8c2cfda06634664dd608b786b8cc036343d10c50937a2adc
ARG NEMOCLAW_HERMES_TIRITH_FINALIZER_SHA256=a1e6b1c53ab297569abb87c29d15c294d729e46005bfd022136b4c447a791819
ARG NEMOCLAW_HERMES_CRON_RESTORE_CONTROLLER_SHA256=e8593cf1580bffa4663e91c079ba0ce31c3d26391f5b1718872701138ce250b0
# hadolint ignore=DL4006
Expand Down
30 changes: 30 additions & 0 deletions agents/hermes/config/build-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
// SPDX-License-Identifier: Apache-2.0

import { Buffer } from "node:buffer";
import { TextDecoder } from "node:util";

import {
type HermesSwitchyardRouting,
validateHermesSwitchyardRouting,
} from "../../../src/lib/hermes-switchyard-routing.ts";
import { normalizeProviderPlaceholderForEnvKey } from "../../../src/lib/messaging/provider-placeholders.ts";
import { readToolDisclosureEnv } from "../../../src/lib/tool-disclosure.ts";
import { isObjectRecord } from "./object-record.ts";
Expand Down Expand Up @@ -31,6 +36,7 @@ export type HermesBuildSettings = {
brokerEnabled: boolean;
presets: string[];
};
switchyardRouting?: HermesSwitchyardRouting | null;
};

/** Read and validate the environment consumed by the Hermes config generator. */
Expand Down Expand Up @@ -60,9 +66,33 @@ export function readHermesBuildSettings(env: NodeJS.ProcessEnv): HermesBuildSett
brokerEnabled: env.NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER === "1",
presets: readBase64Json<string[]>(env, "NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64", "W10="),
},
switchyardRouting: readSwitchyardRouting(env),
};
}

function readSwitchyardRouting(env: NodeJS.ProcessEnv): HermesSwitchyardRouting | null {
const encoded = env.NEMOCLAW_HERMES_SWITCHYARD_ROUTING_B64;
if (encoded === undefined || encoded === "") return null;
try {
if (
encoded.length > 128 * 1024 ||
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(encoded)
) {
throw new Error("transport is not canonical base64");
}
const bytes = Buffer.from(encoded, "base64");
if (bytes.toString("base64") !== encoded) {
throw new Error("transport is not canonical base64");
}
return validateHermesSwitchyardRouting(
JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown,
);
} catch (error) {
const message = error instanceof Error ? error.message : "unknown validation failure";
throw new Error(`NEMOCLAW_HERMES_SWITCHYARD_ROUTING_B64 is invalid: ${message}`);
}
}

function readBooleanBuildFlag(env: NodeJS.ProcessEnv, name: string): boolean {
const value = env[name] ?? "0";
if (value !== "0" && value !== "1") {
Expand Down
11 changes: 10 additions & 1 deletion agents/hermes/config/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,20 @@ export function generateHermesConfig({
const config = policy.config;
const envLines = policy.env_lines;
finalizeHermesPlatformToolsets(config, settings);
const written = writeHermesConfigFiles(config, envLines, policy, homeDir);
const written = writeHermesConfigFiles(
config,
envLines,
policy,
settings.switchyardRouting ?? null,
homeDir,
);

log(`[config] Wrote ${written.configPath} (model=${settings.model}, provider=custom)`);
log(`[config] Wrote ${written.envPath} (${written.envEntryCount} entries)`);
log(`[config] Wrote ${written.policyPath} (schema=${policy.schema_version})`);
if (written.relayPluginsPath !== null) {
log(`[config] Wrote ${written.relayPluginsPath} (native Relay/Switchyard configuration)`);
}

return { settings, config, envLines, policy, written };
}
5 changes: 5 additions & 0 deletions agents/hermes/config/hermes-env.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 { HermesBuildSettings } from "./build-env.ts";
import { HERMES_SWITCHYARD_RELAY_TOML } from "../../../src/lib/hermes-switchyard-routing.ts";
import {
effectiveManagedToolGatewayPresets,
loadManagedToolGatewayMatrix,
Expand All @@ -15,6 +16,10 @@ export function buildHermesEnvLines(
): string[] {
const envLines = ["API_SERVER_PORT=18642", "API_SERVER_HOST=127.0.0.1"];

if (settings.switchyardRouting != null) {
envLines.push(`HERMES_NEMO_RELAY_PLUGINS_TOML=${HERMES_SWITCHYARD_RELAY_TOML}`);
}

for (const { envKey, placeholder } of settings.messagingCredentialPlaceholders) {
envLines.push(`${envKey}=${placeholder}`);
}
Expand Down
19 changes: 18 additions & 1 deletion agents/hermes/config/write-config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { chmodSync, writeFileSync } from "node:fs";
import { chmodSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { HermesManagedPolicyV1 } from "./managed-policy.ts";
import {
serializeHermesSwitchyardRelayToml,
type HermesSwitchyardRouting,
} from "../../../src/lib/hermes-switchyard-routing.ts";
import { buildHermesUpstreamHeader } from "./upstream-header.ts";
import { toYaml } from "./yaml.ts";

Expand All @@ -13,12 +17,14 @@ export type WrittenHermesConfig = {
envPath: string;
envEntryCount: number;
policyPath: string;
relayPluginsPath: string | null;
};

export function writeHermesConfigFiles(
config: Record<string, unknown>,
envLines: string[],
policy: HermesManagedPolicyV1,
switchyardRouting: HermesSwitchyardRouting | null,
homeDir: string = homedir(),
): WrittenHermesConfig {
const configPath = join(homeDir, ".hermes", "config.yaml");
Expand All @@ -33,10 +39,21 @@ export function writeHermesConfigFiles(
writeFileSync(policyPath, `${JSON.stringify(policy, null, 2)}\n`);
chmodSync(policyPath, 0o600);

const generatedRelayPluginsPath = join(homeDir, ".hermes", "relay-plugins.toml");
let relayPluginsPath: string | null = null;
if (switchyardRouting === null) {
rmSync(generatedRelayPluginsPath, { force: true });
} else {
writeFileSync(generatedRelayPluginsPath, serializeHermesSwitchyardRelayToml(switchyardRouting));
chmodSync(generatedRelayPluginsPath, 0o600);
relayPluginsPath = generatedRelayPluginsPath;
}

return {
configPath,
envPath,
envEntryCount: envLines.length,
policyPath,
relayPluginsPath,
};
}
Loading
Loading