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 <NoronhaH@users.noreply.github.com>
This commit is contained in:
Hiuri Noronha
2026-04-20 17:55:08 -03:00
committed by GitHub
parent 51f127f47b
commit 1bf2424377
3 changed files with 60 additions and 7 deletions

View File

@@ -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<T extends { config?: unknown; agent?: unknown }>(ctx: T): T {
const config =
ctx && typeof ctx === "object" && "config" in ctx && ctx.config && typeof ctx.config === "object"
? (ctx.config as Record<string, unknown>)
: null;
const agent =
ctx && typeof ctx === "object" && "agent" in ctx && ctx.agent && typeof ctx.agent === "object"
? (ctx.agent as Record<string, unknown>)
: null;
const agentAdapterConfig =
agent?.adapterConfig && typeof agent.adapterConfig === "object"
? (agent.adapterConfig as Record<string, unknown>)
: 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,

View File

@@ -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,
};

View File

@@ -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<string, unknown>;
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}