Hermes Agent Runtime Integration README
This README started life as the reviewer guide for PR #194 (merged to dev 2026-05-05) and now serves as the operational guide for the Hermes-backed agent runtime. It documents the functional integration between the Humanwork agent-service contract and the Hermes runtime, with the Slack dogfood path as the primary slice.
Goal
Paul's requested EOD goal was not full Humanwork MVP completion. The target was a functional integration PR that makes the Hermes-flavor runtime usable by Human.work, with Slack dogfood as the northstar.
The integration problem was:
feat/agent-servicedefines the/v1/*contract surface the Platform calls.feat/hermes-agentcontains the real Hermes-backed runtime.- The contract branch had stub/fake endpoint behavior.
- The Hermes runtime existed, but was not fully wired behind the Platform contract surface or the Slack ingestion path.
This PR wires the contract surface to the real runtime and keeps POST-MVP surfaces explicit instead of pretending they are implemented.
Primary Runtime Path
The main customer-facing Slack path is:
- Slack sends an Events API payload to
POST /channels/slack/events. ChannelsControllerverifies or accepts the event according to configured Slack signing settings.- The Slack event is normalized into a Humanwork channel message.
- Slack integration credentials are inspected to resolve the owning org from
Slack metadata such as
team_id,app_id, orchannel_id. ConversationsService.findOrCreateByChannel()creates or reuses the Slack conversation.ConversationsService.sendMessage()persists the user message and callsAgentClient.chat().AgentClientcalls the Python agent service/v1/chatcontract endpoint./v1/chattranslates the v1 request into the Hermes runtime request shape.- The Python runtime shells out to the real
hermes chatCLI, without Hermes gateway. - Hermes returns an assistant reply and, when available, a Hermes session id.
- The agent service maps the runtime response back into the v1 contract.
- The API persists the agent message and posts a Slack reply when a bot token is configured.
Important Implementation Details
No Fake Hermes On The Critical Path
The production path uses agent/hermes_client.py, which shells out to
HERMES_CLI or hermes:
hermes chat -q <prompt> -Q -m <model> --provider <provider> -t <toolsets> --source tool --yolo
The fake Hermes fixture remains test-only. It is used by the fast opt-in API
smoke at api/test/agent-runtime-e2e.spec.ts, but the real Slack dogfood smoke
uses the actual local Hermes binary.
Session Mapping Fix
Humanwork conversation ids are not Hermes session ids. Passing a fresh
Humanwork conversation id directly as hermes --resume <id> fails with real
Hermes because the session does not exist yet.
The runtime now resumes Hermes only when a cached Humanwork-to-Hermes mapping exists:
- First turn: no
--resume; Hermes creates a real session. - Runtime parses Hermes
session_id. - Runtime stores
Humanwork session key -> Hermes session id. - Later turns: runtime passes the cached Hermes session id to
--resume.
This is the core difference that fake-Hermes tests did not catch.
Slack Org Resolution
Slack inbound messages now try to resolve orgId from stored Slack integration
credentials before calling the conversation service. The resolver checks common
metadata/config spellings:
team_id/teamIdenterprise_id/enterpriseIdapp_id/appIdchannel_id/channelId
If no mapping is found, the route remains tolerant and creates the conversation without an org, matching the previous loose webhook behavior.
Conversation Id Propagation
ConversationsService.sendMessage() now passes the actual Platform
conversation id into AgentClient.chat(). That means /v1/chat receives the
contract-level conversation id instead of falling back to a Slack customer id.
Deferred POST-MVP Surfaces
The task/config/knowledge v1 endpoints are intentionally explicit 501 deferred responses for POST-MVP behavior. This avoids fake success responses on surfaces that are not part of the Slack/Hermes EOD slice.
Files To Review
Agent runtime and contract adapter:
agent/routers/chat.pyagent/chat_helpers.pyagent/hermes_client.pyagent/main.pyagent/prompts_loader.pyagent/routers/deferred.pyagent/routers/configure.pyagent/routers/knowledge.pyagent/routers/tasks.pyagent/routers/corrections.pyagent/corrections_client.py
API/platform integration:
api/src/common/agent.client.tsapi/src/conversations/conversations.service.tsapi/src/channels/channels.controller.tsapi/src/learning/learning.controller.tsapi/src/learning/learning.service.ts
Verification:
agent/evals/test_contract.pyagent/tests/test_chat.pyapi/test/agent-client.spec.tsapi/test/conversations.spec.tsapi/test/channels.spec.tsapi/test/agent-runtime-e2e.spec.tsapi/test/slack-real-hermes-e2e.spec.ts
Contract docs:
agent/AGENT_API_SPEC.mddocs/api/agent-service-openapi.yaml
Environment Variables
Python agent runtime:
| Variable | Purpose |
|---|---|
AGENT_SERVICE_SECRET | Shared secret expected as X-Agent-Secret. |
HERMES_CLI | Path to the real Hermes binary. Defaults to hermes. |
HERMES_PROVIDER | Hermes provider, for example openrouter. |
HERMES_MODEL | Provider model id, for example openai/gpt-4o-mini. |
HERMES_TOOLSETS | Hermes toolsets, for example delegation. |
HERMES_TIMEOUT_SEC | Runtime timeout for Hermes subprocess execution. |
HUMANWORK_WORKSPACE_ROOT | Workspace root for runtime sessions/artifacts. |
API/platform:
| Variable | Purpose |
|---|---|
AGENT_SERVICE_URL | Python agent base URL. |
AGENT_SERVICE_SECRET | Shared secret forwarded to the Python agent. |
SLACK_SIGNING_SECRET | Enables Slack HMAC verification when configured. |
SLACK_BOT_TOKEN | Enables outbound Slack chat.postMessage replies. |
Verification Commands
Run agent tests:
cd agent
.venv/bin/python -m pytest tests evals -q
Run API build and default tests under the repo-compatible Node version:
cd api
source ~/.nvm/nvm.sh
nvm use 22.22.0
npm run build
npm test -- --runInBand
Run the fake-Hermes contract E2E:
cd api
source ~/.nvm/nvm.sh
nvm use 22.22.0
RUN_AGENT_RUNTIME_E2E=true npm test -- agent-runtime-e2e.spec.ts --runInBand
Run the real Slack/Hermes E2E:
cd api
source ~/.nvm/nvm.sh
nvm use 22.22.0
RUN_SLACK_REAL_HERMES_E2E=true npm test -- slack-real-hermes-e2e.spec.ts --runInBand
The real E2E starts a local Python agent service and points it at the real Hermes CLI. It sends a simulated Slack Events API payload through the Nest Slack endpoint and waits for a real Hermes-generated agent reply to be persisted.
Verified In This PR
The latest pushed verification evidence for PR #194 (merged to dev 2026-05-05):
- Real Hermes CLI smoke passed with
openrouterandopenai/gpt-4o-mini. - Real Python
/v1/chatsmoke passed withHERMES_CLI=/Users/koristuvac/.local/bin/hermes. - Real Slack/Hermes E2E passed:
RUN_SLACK_REAL_HERMES_E2E=true npm test -- slack-real-hermes-e2e.spec.ts --runInBand. - Agent full suite passed:
491 passed, 14 skipped. - API build passed under Node
22.22.0. - API Jest passed under Node
22.22.0:32 suites passed, 2 skipped; 374 passed, 376 total. - PR checks are green.
Known Boundaries
- Live outbound Slack posting requires a real
SLACK_BOT_TOKENor per-org Slack integration credential withbot_token. The code path is wired and tested with the Slack WebClient mocked; the real-Hermes E2E disables outbound Slack posting so it does not need a live workspace token. - Full v1 contract coverage is a stretch goal beyond the Slack EOD path. The non-Slack POST-MVP endpoints return explicit deferred 501 envelopes instead of fake success responses.
Post-Merge Notes
PR #194 merged with three known issues flagged by the Greptile security review. They were accepted as non-blocking for the merge but are tracked as follow-ups (GitHub issues to be filed):
- HMAC
verifyHmaccrashes on wrong-length signatures. Inapi/src/agent-api/agent-api.service.ts,crypto.timingSafeEqualthrows aRangeErrorwhen the attacker-supplied signature is a different byte length than the expected HMAC. The route should normalize and return401, not surface a 500. → GitHub issue to be filed. has_morepagination bug ingetConversationMessages. The flag is computed against the unfiltered total instead of the returned page, sohas_moreis incorrect after filtering. Clients that paginate by followinghas_morecan loop or miss the end. The check should comparemsgs.length === limit. → GitHub issue to be filed.- Non-timing-safe
PLATFORM_API_TOKENcomparison.assertPlatformTokeninapi/src/learning/learning.controller.tsuses!==instead ofcrypto.timingSafeEqual. Low impact (single fixed token, narrow surface) but easy to harden. → GitHub issue to be filed.