Commit Graph
36 Commits
Author SHA1 Message Date
william 732246d4fd Add MCP bridge to claude-agent so Hermes can delegate to the real Claude Code CLI
New POST /mcp endpoint (Streamable HTTP transport, stateless — fresh
McpServer+transport per request) exposing one tool, ask_claude_code: runs
the real `claude` binary against a prompt, billed against the Pro/Max
subscription rather than API credits. This works specifically because it's
the actual claude CLI making the request server-side — the same reason
Hermes itself can't authenticate with the subscription directly (proven
earlier: Anthropic rejects the OAuth token from any client that isn't the
real CLI's exact request fingerprint). Read-only: no Edit/Write/git-push/
git-commit tools, since this is a quick-answer bridge, not a repo editor.

Tested end-to-end locally (built + ran the image, curled the full MCP
handshake: initialize -> tools/list -> tools/call) before pushing — got a
real 'pong' back from the actual claude CLI through the MCP protocol.

To register it with Hermes (lives in its own data volume, not git — see
.env.example comment):
  docker exec hermes hermes config set mcp_servers.claude-code.url http://claude-agent:3001/mcp
  docker exec hermes hermes config set 'mcp_servers.claude-code.headers.Authorization' 'Bearer <MCP_BRIDGE_KEY>'
2026-08-23 17:24:46 +00:00
william ec37245b37 Merge pull request 'Add Hermes web dashboard at hermes.apps.williamturner.eu (dual auth)' (#15) from feat/hermes-dashboard into main
Reviewed-on: #15
2026-08-23 17:16:05 +00:00
william 3dc6397862 Merge pull request 'LiteLLM: drop unsupported params (fixes Hermes routing)' (#16) from fix/litellm-drop-params into main
Reviewed-on: #16
2026-08-23 17:15:38 +00:00
william 02a94d1d03 LiteLLM: drop unsupported params instead of erroring (fixes Hermes routing)
Hermes sends provider-agnostic params like reasoning_effort that OpenRouter's
auto model doesn't accept, causing a 400 on every request until this was
set. Also documents (in a follow-up, not this commit) that getting Hermes to
actually use this gateway needed live 'hermes config set' calls on the
running container (providers.litellm.{api,api_key}, model.provider=litellm,
model.default=auto) — that state lives in the hermes-data volume, not git,
so it isn't reproduced automatically by a fresh deploy. See README.
2026-08-23 17:09:39 +00:00
william 143be8200e Drop the Traefik-level basic auth layer — keep only Hermes's own login
By request: one auth layer, not two. Note this isn't really removing a
layer I added on top of nothing — Hermes's own gate is mandatory and
can't be disabled while the dashboard is reachable through a separate
Traefik container (it fails closed at startup on any non-loopback bind
without a configured auth provider). The only thing actually optional
was the Traefik-level middleware, so that's what comes out; Traefik now
just does TLS termination + routing.
2026-08-23 17:02:59 +00:00
william 9739676409 Add Hermes web dashboard at hermes.apps.williamturner.eu
Enabled via HERMES_DASHBOARD=1 (supervised in-container alongside the
gateway, per docs/user-guide/docker.md), bound to 0.0.0.0:9119 so Traefik
can reach it. Two independent auth layers, not one:

1. Traefik basicauth middleware in front of the whole route.
2. Hermes's own basic-auth gate (mandatory once the bind is non-loopback).

Hermes's docs explicitly call basic-auth-alone "not suitable for direct
public-internet exposure" and cite a real June 2026 incident where
internet scanners reached exposed dashboards and drove agents into
planting SSH-key backdoors — hence the extra Traefik-level gate rather
than relying on Hermes's own login page alone.

Also fixes: the htpasswd hash for Traefik's basicauth needs its literal
'$' characters escaped as '2824147' in .env, or docker compose's own variable
interpolation corrupts it (mistook '' for further
 references). Also switched the hash from Python's default SHA-512
crypt ('$...') to apr1 ('$...', via openssl passwd -apr1) —
Traefik's basicauth middleware doesn't accept SHA-512-crypt.

Also re-adds Hermes's OPENAI_BASE_URL/OPENAI_API_KEY routing through the
local litellm gateway (instead of OPENROUTER_API_KEY direct) — this was
part of the now-abandoned PR #12 and never actually landed on main.
2026-08-23 16:21:50 +00:00
william 6d9d081030 Merge pull request 'Remove claude-agent Matrix presence — Hermes only' (#14) from remove/claude-bot-matrix-presence into main
build-agent / build-and-push (push) Successful in 10s
Reviewed-on: #14
2026-08-23 16:16:58 +00:00
william 6bc862e051 Remove claude-agent's Matrix presence entirely — Hermes is the only agent in Matrix
By request: one agent in Matrix, not several. Removes matrixBot.js,
router.js (chat-vs-code-task classifier), litellm.js (claude-agent's own
LiteLLM client), the matrix-bot-sdk dependency, runChatTask() and its
gitea.js branch/PR helpers (createBranch/createPullRequest — only ever
called from the now-removed chat flow), and every Matrix/LiteLLM env var
from claude-agent's compose service.

claude-agent already left the control room manually before this merge.
It keeps its Gitea-webhook-triggered PR review, which never touched Matrix
or LiteLLM to begin with.

Makes PR #12 (the claude-bot/Hermes cross-reply cascade fix) moot — the
bug can't happen once claude-agent has no Matrix client at all. Close #12
without merging once this lands.
2026-08-23 16:16:19 +00:00
william fff021283d Merge pull request 'Re-add claude-subscription LiteLLM route — confirmed working for the real CLI' (#13) from feat/litellm-claude-subscription-route into main
Reviewed-on: #13
2026-08-23 16:09:47 +00:00
william f4f785e0bb Re-add the claude-subscription LiteLLM route — confirmed working for the real CLI
Earlier this session I removed this route after a curl-based test got
rejected by Anthropic and concluded OAuth subscription forwarding doesn't
work through a proxy at all. That conclusion was wrong: the real `claude`
CLI binary, with ANTHROPIC_BASE_URL pointed at litellm, successfully
completed a request billed against the subscription. The earlier curl test
just didn't replicate whatever header/fingerprint Anthropic requires from
genuine Claude Code CLI traffic — LiteLLM relays that fine when the real
CLI is the caller, but a hand-built request from any other client (Hermes
included) still gets rejected the same way curl did.
2026-08-23 16:08:26 +00:00
william c88fdcc2ea Merge pull request 'Fix Hermes: gateway command + disable network-reachable API server' (#11) from fix/hermes-gateway-command-and-api-server into main
Reviewed-on: #11
2026-08-23 15:52:35 +00:00
william 98762e764a Fix Hermes container: add gateway command, disable network-reachable API server
Without an explicit command the image launches the interactive CLI by
default, which immediately exits with 'Input is not a terminal' in a
detached container — it was doing nothing on every restart. Also disables
API_SERVER_ENABLED: Hermes itself warns at startup that a network-reachable
API server combined with the default unsandboxed 'local' terminal backend
gives any caller on the network full terminal/file access. Not needed yet
(Matrix is the actual interface) — can re-enable properly (with a sandboxed
terminal backend) if claude-agent ever needs to call Hermes programmatically.
2026-08-23 15:52:08 +00:00
william 5507192ee6 Merge pull request 'Add Hermes Agent as a second native Matrix presence' (#10) from feat/hermes-matrix into main
build-agent / build-and-push (push) Successful in 11s
Reviewed-on: #10
2026-08-23 15:49:05 +00:00
william 9fa025f7d5 Add Hermes Agent as a second native Matrix presence
Deployed as its own service (pinned nousresearch/hermes-agent:v2026.8.19),
own Matrix bot account (@hermes), own OpenRouter-backed model config, and
its own OpenAI-compatible API server (internal network only, for possible
future use by claude-agent). Joins the same control room but only responds
when explicitly @mentioned, restricted to the human user — no conflict with
claude-bot's default no-prefix chat routing. claude-agent's router now
ignores messages addressed to @hermes so both bots don't answer the same
message.

Bridge networking (the 'web' network), not the image's default host mode —
no reason for an agent container to share the host's network namespace when
everything it needs (the homeserver, OpenRouter) is reachable over the
existing bridge.
2026-08-23 15:46:58 +00:00
william 5461754f23 Merge pull request 'Route chat through local LiteLLM gateway, drop !claude/!ai prefixes' (#8) from feat/litellm-router into main
build-agent / build-and-push (push) Successful in 7s
Reviewed-on: #8
2026-08-23 15:34:29 +00:00
william f93bfb25a2 Route all chat through a local LiteLLM gateway, drop command prefixes
- New litellm service (pinned v1.98.0 — litellm 1.82.7/1.82.8 on PyPI were
  compromised with credential-stealing malware in March 2026; internal-only,
  no Traefik route, no reason to expose an LLM gateway with a master key
  publicly).
- Replaces !claude/!ai command prefixes with automatic routing: every plain
  message in the control room goes through a classifier (router.js) that
  decides chat vs code_task. Chat replies use OpenRouter's own auto-router
  (openrouter/auto) via LiteLLM; code_task requests go through the existing
  runChatTask() flow (Claude Code CLI, unchanged, still using the
  subscription token directly).
- Investigated routing Claude itself through LiteLLM via OAuth token
  forwarding (general_settings.forward_client_headers_to_llm_api) so the
  Pro/Max subscription could be one of the auto-routable options. Confirmed
  non-functional: Anthropic returns a generic rate_limit_error for any
  direct API call using this token type outside the real Claude Code CLI,
  reproduced with plain curl straight to api.anthropic.com. Not included.
- MATRIX_BOT_USER_ID now required and set explicitly (self-message filtering
  can no longer rely on a command-prefix mismatch once there isn't one).
2026-08-23 15:32:55 +00:00
william 4fc4433833 Merge pull request 'Fix GitOps redeploy race: chain to end of build, not a parallel webhook' (#7) from fix/chain-portainer-redeploy-after-build into main
build-agent / build-and-push (push) Successful in 3s
Reviewed-on: #7
2026-08-23 15:12:03 +00:00
william 8db8347df0 Chain Portainer redeploy to the end of the build workflow, not a parallel webhook
The separate Gitea push webhook to Portainer fired in parallel with this
build workflow on the same push event, so it could redeploy before the
new image finished pushing — Portainer would then pull the still-current
:latest tag and silently keep running old code. Removed that webhook;
redeploy now only happens as this workflow's last step, after the image
push actually completes.
2026-08-23 15:11:31 +00:00
william 34c2f7d634 Merge pull request 'Fix Matrix bot autojoin 404 on startup' (#5) from fix/matrix-autojoin-404 into main
build-agent / build-and-push (push) Successful in 10s
Reviewed-on: #5
2026-08-23 15:08:37 +00:00
william fc53251285 Replace AutojoinRoomsMixin with a lightweight invite handler
Same root cause as the earlier whoami fix: the mixin's initial
/joined_rooms scan 404s against Continuwuity even though the endpoint
works fine when called directly. Only auto-join-on-invite was actually
needed, so a plain room.invite listener replaces the whole mixin.
2026-08-23 15:06:57 +00:00
william 6d0cb3bba3 Document branch protection + dedicated bot account so the bot can only open PRs, never merge 2026-08-23 14:51:13 +00:00
william d7a881c768 Cap OpenRouter max_tokens — some models default to their full context window
build-agent / build-and-push (push) Successful in 11s
2026-08-23 14:45:19 +00:00
william 77ce4a7334 Add !ai Matrix command for querying other models via OpenRouter
build-agent / build-and-push (push) Successful in 7s
2026-08-23 14:40:46 +00:00
william 58bce828bf Drop getUserId() call in Matrix bot startup — its whoami request 404s against Continuwuity
build-agent / build-and-push (push) Successful in 10s
Not actually needed: self-message filtering was only used to avoid the bot
reacting to its own replies, but those never match the !claude command
pattern anyway, so parseCommand() already filters them out for free.
2026-08-23 11:42:48 +00:00
william ec5ca6ff85 Use Claude subscription auth (CLAUDE_CODE_OAUTH_TOKEN) instead of API billing 2026-08-23 11:35:16 +00:00
william 8dfdcb80e8 Run claude-agent as non-root user — Claude Code refuses bypassPermissions as root
build-agent / build-and-push (push) Successful in 16s
2026-08-23 11:25:28 +00:00
william 6da5c8cc44 Smoke test: verify full push -> CI build -> Portainer redeploy loop 2026-08-23 11:23:34 +00:00
william 31d0923fdf Remove act_runner config file/mount — unreliable under Portainer git-stack deploy, and unnecessary
The bind-mounted relative path resolved to nothing once Portainer's cloned
checkout no longer persisted for the container's runtime, so Docker silently
created an empty directory there and act_runner crash-looped. act_runner
already auto-detects its own docker.sock without any custom config.
2026-08-23 11:21:27 +00:00
william 11d2ae8cbb Move Traefik out of this stack into its own standalone stack
Fixes a circular dependency: a full stack redeploy tears every service down
before bringing any back up, but claude-agent's image pull goes through
Traefik->Gitea's registry. Self-hosting Traefik in the same stack it fronts
means redeploying this stack could tear down the very route needed to bring
it back. Traefik now lives in ~/traefik/docker-compose.yml as shared infra,
independent of this stack's lifecycle.
2026-08-23 11:19:55 +00:00
william aca1be8fd2 Fix claude-agent env vars for Portainer git-stack deploy (env_file doesn't work there) 2026-08-23 11:15:37 +00:00
william 32be57fe30 Use a real PAT (REGISTRY_TOKEN) for registry login, not the auto GITEA_TOKEN
build-agent / build-and-push (push) Successful in 36s
2026-08-23 11:05:19 +00:00
william 5f804baea9 Grant packages:write to the build job's auto-token
build-agent / build-and-push (push) Failing after 2s
2026-08-23 11:03:42 +00:00
william 0dcf5488d2 Fix Actions runner: use docker-capable job image, mount docker socket, add manual trigger
build-agent / build-and-push (push) Failing after 0s
2026-08-23 10:53:18 +00:00
william 8c57c48e9e Use non-reserved variable name for registry host in build workflow
build-agent / build-and-push (push) Failing after 2s
2026-08-23 10:50:39 +00:00
william 622e35f979 Fix Traefik/Docker API version mismatch, tidy Matrix registration-token env handling 2026-08-23 10:49:11 +00:00
william 357791d6e3 Scaffold Matrix + GitOps + Claude automation stack
build-agent / build-and-push (push) Failing after 18s
2026-08-23 10:25:57 +00:00