From 1bf242437780517f89e6d3f5981f945cf60a893d Mon Sep 17 00:00:00 2001 From: Hiuri Noronha <115645534+NoronhaH@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:55:08 -0300 Subject: [PATCH] fix: honor Hermes local command override (#3503) ## Summary This fixes the Hermes local adapter so that a configured command override is respected during both environment tests and execution. ## Problem The Hermes adapter expects `adapterConfig.hermesCommand`, but the generic local command path in the UI was storing `adapterConfig.command`. As a result, changing the command in the UI did not reliably affect runtime behavior. In real use, the adapter could still fall back to the default `hermes` binary. This showed up clearly in setups where Hermes is launched through a wrapper command rather than installed directly on the host. ## What changed - switched the Hermes local UI adapter to the Hermes-specific config builder - updated the configuration form to read and write `hermesCommand` for `hermes_local` - preserved the override correctly in the test-environment path - added server-side normalization from legacy `command` to `hermesCommand` ## Compatibility The server-side normalization keeps older saved agent configs working, including configs that still store the value under `command`. ## Validation Validated against a Docker-based Hermes workflow using a local wrapper exposed through a symlinked command: - `Command = hermes-docker` - environment test respects the override - runs no longer fall back to `hermes` Typecheck also passed for both UI and server. Co-authored-by: NoronhaH --- server/src/adapters/registry.ts | 35 +++++++++++++++++++++++++-- ui/src/adapters/hermes-local/index.ts | 4 +-- ui/src/components/AgentConfigForm.tsx | 28 ++++++++++++++++++--- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index d3fa0fbe..59f01289 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -86,6 +86,37 @@ import { getDisabledAdapterTypes } from "../services/adapter-plugin-store.js"; import { processAdapter } from "./process/index.js"; import { httpAdapter } from "./http/index.js"; +function normalizeHermesConfig(ctx: T): T { + const config = + ctx && typeof ctx === "object" && "config" in ctx && ctx.config && typeof ctx.config === "object" + ? (ctx.config as Record) + : null; + const agent = + ctx && typeof ctx === "object" && "agent" in ctx && ctx.agent && typeof ctx.agent === "object" + ? (ctx.agent as Record) + : null; + const agentAdapterConfig = + agent?.adapterConfig && typeof agent.adapterConfig === "object" + ? (agent.adapterConfig as Record) + : null; + + const configCommand = + typeof config?.command === "string" && config.command.length > 0 ? config.command : undefined; + const agentCommand = + typeof agentAdapterConfig?.command === "string" && agentAdapterConfig.command.length > 0 + ? agentAdapterConfig.command + : undefined; + + if (config && !config.hermesCommand && configCommand) { + config.hermesCommand = configCommand; + } + if (agentAdapterConfig && !agentAdapterConfig.hermesCommand && agentCommand) { + agentAdapterConfig.hermesCommand = agentCommand; + } + + return ctx; +} + const claudeLocalAdapter: ServerAdapterModule = { type: "claude_local", execute: claudeExecute, @@ -202,8 +233,8 @@ const piLocalAdapter: ServerAdapterModule = { const hermesLocalAdapter: ServerAdapterModule = { type: "hermes_local", - execute: hermesExecute, - testEnvironment: hermesTestEnvironment, + execute: (ctx) => hermesExecute(normalizeHermesConfig(ctx) as never), + testEnvironment: (ctx) => hermesTestEnvironment(normalizeHermesConfig(ctx) as never), sessionCodec: hermesSessionCodec, listSkills: hermesListSkills, syncSkills: hermesSyncSkills, diff --git a/ui/src/adapters/hermes-local/index.ts b/ui/src/adapters/hermes-local/index.ts index c037a747..a7449174 100644 --- a/ui/src/adapters/hermes-local/index.ts +++ b/ui/src/adapters/hermes-local/index.ts @@ -1,12 +1,12 @@ import type { UIAdapterModule } from "../types"; import { parseHermesStdoutLine } from "hermes-paperclip-adapter/ui"; -import { SchemaConfigFields, buildSchemaAdapterConfig } from "../schema-config-fields"; import { buildHermesConfig } from "hermes-paperclip-adapter/ui"; +import { SchemaConfigFields } from "../schema-config-fields"; export const hermesLocalUIAdapter: UIAdapterModule = { type: "hermes_local", label: "Hermes Agent", parseStdoutLine: parseHermesStdoutLine, ConfigFields: SchemaConfigFields, - buildAdapterConfig: buildSchemaAdapterConfig, + buildAdapterConfig: buildHermesConfig, }; diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 17437bc8..a053b260 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -291,6 +291,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) { enabled: Boolean(selectedCompanyId), }); const models = fetchedModels ?? externalModels ?? []; + const adapterCommandField = + adapterType === "hermes_local" ? "hermesCommand" : "command"; const { data: detectedModelData, refetch: refetchDetectedModel, @@ -346,7 +348,19 @@ export function AgentConfigForm(props: AgentConfigFormProps) { return uiAdapter.buildAdapterConfig(val!); } const base = config as Record; - return { ...base, ...overlay.adapterConfig }; + const next = { ...base, ...overlay.adapterConfig }; + if (adapterType === "hermes_local") { + const hermesCommand = + typeof next.hermesCommand === "string" && next.hermesCommand.length > 0 + ? next.hermesCommand + : typeof next.command === "string" && next.command.length > 0 + ? next.command + : undefined; + if (hermesCommand) { + next.hermesCommand = hermesCommand; + } + } + return next; } const testEnvironment = useMutation({ @@ -667,12 +681,20 @@ export function AgentConfigForm(props: AgentConfigFormProps) { value={ isCreate ? val!.command - : eff("adapterConfig", "command", String(config.command ?? "")) + : eff( + "adapterConfig", + adapterCommandField, + String( + (adapterType === "hermes_local" + ? config.hermesCommand ?? config.command + : config.command) ?? "", + ), + ) } onCommit={(v) => isCreate ? set!({ command: v }) - : mark("adapterConfig", "command", v || null) + : mark("adapterConfig", adapterCommandField, v || null) } immediate className={inputClass}