feat(server): add Hermes HookProvider adapter - #390
Open
ky-os wants to merge 1 commit into
Open
Conversation
Add a second HookProvider so Hermes roster bots render as characters in
the Pixel Agents office, mirroring the reference Claude provider.
- providers/hook/hermes/hermes.ts: HookProvider impl. normalizeHookEvent
maps Hermes outbound-webhook payloads (pre_tool_call -> toolStart,
post_tool_call -> toolEnd, on_turn_complete -> turnEnd awaitingInput,
on_session_start -> sessionStart, on_session_end -> sessionEnd,
subagent_start -> subagentStart, subagent_stop -> subagentEnd);
formatToolStatus for Hermes snake_case tool names; readingTools /
subagentToolNames={delegate_task}; best-effort contextWindowForModel.
No TeamProvider / file-fallback: Hermes sessions are SQLite, not JSONL.
- providers/hook/hermes/hermesHookInstaller.ts: appends a target to
hooks.outbound in Hermes config.yaml ($HERMES_CONFIG or
~/.hermes/config.yaml). Edits YAML directly with js-yaml (atomic tmp +
rename) -- never 'hermes config set' for list keys (corrupts the
loader). Idempotent, preserves unrelated config keys and other
outbound targets.
- providers/hook/hermes/constants.ts: Hermes hook events, provider id,
context-window tables.
- providers/index.ts: export hermesProvider.
- httpServer.ts: hook route auth now accepts either the existing Bearer
token or Hermes' X-Hermes-Signature-256 HMAC (GitHub-webhook style)
verified over the raw body. Raw body preserved via a parseAs:'buffer'
JSON content-type parser.
- Tests mirroring the Claude provider suite: normalize table +
formatToolStatus (hermes.test.ts), installer install/idempotence/
uninstall/preserve-keys (hermesHookInstaller.test.ts), and hook-route
HMAC auth (hermesHookAuth.test.ts). 58 new tests, all green.
- server/package.json: add js-yaml (+ @types/js-yaml) for YAML config
editing.
Known Hermes gaps (noted for v2): no permissionRequest event exists, and
on_turn_complete is a context-engine observation rather than a plugin
hook today, so it is handled defensively but not installed as an event.
Author
QA Review — APPROVED (board verdict: kanban t_2ab0a6a9)Independent review by @qa-reviewer. Artifact-lens round: cold-read the diff, then verified every claim against the actual Hermes source and by running the gates. (Formal approve not possible via API — PR authored by same account.) Verified against Hermes source (C:/.../hermes-agent)
Gates run (reproduced, not taken on faith)
Non-blocking observations (follow-ups, not required for merge)
No implementation files were edited by the reviewer. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hermes HookProvider adapter
Adds a second
HookProviderso Hermes roster bots render as characters in the Pixel Agents office, mirroring the reference Claude provider. Hermes needs no code changes: its config-driven outbound webhooks (agent/outbound_webhooks.py) already POST Claude-style payloads (hook_event_name,tool_name,tool_input,session_id,cwd), so the adapter is purely a normalization boundary on this side.What's included
server/src/providers/hook/hermes/hermes.ts—HookProviderimpl:normalizeHookEventmapping table:pre_tool_calltoolStart(synthetichook-*toolId)post_tool_calltoolEnd(sentinelcurrenttoolId, correlated by the handler'scurrentHookToolId)on_turn_completeturnEndwithawaitingInput: true(Hermes is idle waiting on the user)on_session_startsessionStart(sourcefromextra.platform,cwdtop-level)on_session_endsessionEnd(reasonderived fromextra.completed/extra.interrupted)subagent_startsubagentStart(toolName=extra.child_role)subagent_stopsubagentEndgateway_platform_eventandon_session_finalizeare intentionally dropped (informational / duplicate ofsessionEndcleanup).formatToolStatusfor Hermes snake_case tools:terminal,read_file,write_file,patch(single + V4A multi-file path arrays),search_files,web_search,web_extract,execute_code,browser_exec,computer_use,skill_view,delegate_task.subagentToolNames = {delegate_task},readingTools= read-family set,permissionExemptTools = {delegate_task}.contextWindowForModel— best-effort heuristic (small-window regex → 128k, else 200k,undefinedfor unknown/synthetic). Runtime widens on overrun, so a wrong guess self-corrects.No
TeamProviderand no file-fallback surface: Hermes sessions live in SQLite, not JSONL, and subagents are plaindelegate_taskruns without team semantics.hermesHookInstaller.ts— appends one target tohooks.outboundin Hermesconfig.yaml($HERMES_CONFIGor~/.hermes/config.yaml):js-yaml(atomic tmp + rename), becausehermes config setcorrupts list-valued keys (known Hermes loader issue). Never uses it.name: pixel-agents).secret: <server token>so Hermes signs every delivery withX-Hermes-Signature-256: sha256=<hex>(GitHub-webhook style).secret_envis noted in-code as the recommended alternative for non-local deployments.pre_tool_call,post_tool_call,on_session_start,on_session_end,subagent_start,subagent_stop(all validVALID_HOOKSplugin events). See the caveat below abouton_turn_complete.httpServer.ts— the hook route now accepts either the existingAuthorization: Bearer <token>or Hermes'X-Hermes-Signature-256: sha256=<hex>HMAC verified over the exact raw body bytes (preserved via aparseAs: 'buffer'JSON content-type parser). Constant-time comparisons on both paths.providers/index.ts— exportshermesProvider.Known gaps (v2)
permissionRequestevent exists in Hermes. Approvals are an interactive CLI prompt, never a hook. A future permission surface should map to thepermissionRequestAgentEvent.on_turn_completeis not a plugin-hook event today. It's a context-engine observation method (agent/context_engine.py), so an outbound-webhook entry for it would be rejected at registration.normalizeHookEventstill handles the name defensively in case a future Hermes delivers it, but the installer's events list excludes it.Follow-ups (not in this PR)
Verification
npm run check-types— green (bothtsc --noEmitpasses).npm run lint— 0 errors (one pre-existing warning inwebview-ui/src/App.tsx).npm test:main(verified by stashing this branch and running the same 3 files: identical failures). They are unrelated Windows test-infra issues:configPersistence.test.ts/clientMessageHandler.test.ts(6): tests setprocess.env.HOMEbutconfigPersistence.tsresolves paths viaos.homedir(), which on Windows ignoresHOME— the tests read/write the real~/.pixel-agents/config.jsoninstead of the temp dir.mockClaudeRunner.test.ts(4):fs.watch-based tests time out (10s) on Windows.hermes.test.tsnormalize/format tables,hermesHookInstaller.test.tsinstall/idempotence/uninstall/preserve-keys,hermesHookAuth.test.tsHMAC-vs-Bearer auth).Dependency change
server/package.jsongainsjs-yaml(+@types/js-yamldev) for YAML config editing. Lockfile diff is minimal (js-yaml was already in the tree as a transitive dep; itsdevflag is flipped off since the server now depends on it at runtime).