Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]

### Changed
- **`docker compose up` now pulls pre-built images by default** (#82) — `nousresearch/hermes-agent:latest` for the gateway and `ghcr.io/outsourc-e/hermes-workspace:latest` for the UI. Agent state persists in the `claude-data` named volume. Adds `docker-compose.dev.yml` overlay for building from source.
- **`docker compose up` now pulls pre-built images by default** (#82) — `nousresearch/hermes-agent:latest` for the gateway and `ghcr.io/outsourc-e/hermes-workspace:latest` for the UI. Adds `docker-compose.dev.yml` overlay for building from source.

### Fixed
- **Docker: dashboard API unreachable** — agent now starts the dashboard process (`HERMES_DASHBOARD=1`) on `:9119` alongside the gateway; workspace reads sessions, skills, config, jobs, and MCP over the Docker network instead of falling back to limited zero-fork mode.
- **Docker: gateway bound to loopback inside container** — `API_SERVER_HOST` now defaults to `0.0.0.0` inside the container so workspace can reach it over Docker DNS; host publish remains loopback-only (`127.0.0.1:8642`).
- **Docker: volume naming** — renamed `claude-data` to `hermes-agent-data`; added `hermes-workspace-files` volume for workspace file browser persistence.
- **Docker: healthcheck** now verifies both gateway `:8642` and dashboard `:9119` are responding before marking the agent healthy.
- **pnpm 11 build** — added `pnpm-workspace.yaml` with `allowBuilds` map (replaces deprecated `onlyBuiltDependencies` / `dangerouslyAllowAllBuilds`).
- **Chat: per-session model selection** — model picker in the composer now persists the selected model per session and is sent with each message, instead of silently falling back to the backend default.
- **Settings: default model path** — changed from `agents.defaults.model.primary` to `model.default` to match the actual backend config key.
- **Config API: rename message** — "Restart Claude" → "Restart Hermes Agent".

## [2.0.0] — 2026-04-20

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ RUN corepack enable && apt-get update && apt-get install -y --no-install-recomme
WORKDIR /app

# Install deps (cache-friendly: copy only manifests first)
COPY package.json pnpm-lock.yaml* ./
COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* ./
RUN pnpm install --frozen-lockfile

# Copy sources and build
Expand Down
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,13 +382,13 @@ If you've already started the workspace, change either URL from **Settings → C

[![Open in GitHub Codespaces](https://img.shields.io/badge/GitHub%20Codespaces-Open-181717?logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=outsourc-e/hermes-workspace)

The Docker setup runs both the **Hermes Agent gateway** and **Hermes Workspace** together.
The Docker setup runs **Hermes Agent** and **Hermes Workspace** together. Hermes Agent starts both required APIs inside its container: the gateway on `:8642` and the dashboard API on `:9119` for sessions, skills, config, jobs, and MCP.

### Prerequisites

- **Docker**
- **Docker Compose**
- **Anthropic API Key** — [Get one here](https://console.anthropic.com/settings/keys) (required for the agent gateway)
- At least one model provider key, or a reachable local model server

### Step 1: Configure Environment

Expand Down Expand Up @@ -418,20 +418,30 @@ Using **Ollama, LM Studio, or another local server**? No key needed — just poi
docker compose up
```

This pulls two pre-built images and starts them:
This pulls two pre-built images and starts two containers:

- **hermes-agent** → `nousresearch/hermes-agent:latest` on port **8642**
- **hermes-workspace** → `ghcr.io/outsourc-e/hermes-workspace:latest` on port **3000**
- **hermes-agent** → `nousresearch/hermes-agent:latest`; gateway on Docker/host-loopback `:8642`, dashboard API on Docker-only `:9119`
- **hermes-workspace** → `ghcr.io/outsourc-e/hermes-workspace:latest`; web UI on host-loopback `:3000`

No local build. First run takes a minute to pull; subsequent starts are instant.
Agent state (config, sessions, skills, memory, credentials) persists in the
legacy-named `claude-data` Docker volume, so containers can be recreated without data loss.
Agent state (config, sessions, skills, memory, credentials) persists in the named `hermes-agent-data` Docker volume, and files created from the Workspace file browser persist in `hermes-workspace-files`. These volumes survive container recreation, Docker restarts, and `docker compose down`. Only `docker compose down -v` removes them.

#### What Docker configures for Hermes Agent

The Compose file is non-interactive, so it chooses safe defaults that the manual `hermes setup` wizard normally asks about:

- `terminal.backend: local` in `/opt/data/config.yaml`, meaning agent terminal commands run inside the `hermes-agent` container, not on your host machine.
- `/opt/data` is persisted by the `hermes-agent-data` Docker volume.
- `/workspace` in the Workspace UI is persisted by the `hermes-workspace-files` Docker volume.
- The dashboard API is not published to the host; Workspace reaches it over Docker DNS at `http://hermes-agent:9119`.

To change the agent terminal backend later, open **Settings → Providers / Models** in Workspace or edit `config.yaml` in the persisted volume. For host filesystem access, prefer an explicit host bind mount that you control instead of broad-mounting your home directory.

### Step 3: Access the Workspace

Open `http://localhost:3000` and complete the onboarding.

> **Verify:** Check the Docker logs for `[gateway] Connected to Hermes Agent` — this confirms the workspace successfully connected to the agent.
> **Verify:** Check the Workspace logs for `mode=zero-fork` and `enhanced=[sessions, skills, memory, config, jobs]` — this confirms Workspace reached both the gateway and dashboard APIs.

### Building from source

Expand Down
51 changes: 32 additions & 19 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,26 @@
# docker compose -f docker-compose.yml -f docker-compose.dev.yml up
#
# Persistent data:
# The `claude-data` named volume mounts at /opt/data inside the agent
# container. Config, sessions, skills, memory, and credentials live there
# and survive container recreation. For host-path mounts see the commented
# `volumes:` block on the hermes-agent service.
# `hermes-agent-data` — agent config, sessions, skills, memory, credentials.
# Mounted at /opt/data in the agent container and /home/workspace/.hermes
# in the workspace container (read-write for config reads; the agent is
# the primary writer).
# `hermes-workspace-files` — files created from the Workspace file browser.
# Both volumes survive container recreation and `docker compose down`.
# Only `docker compose down -v` removes them.
#
# Troubleshooting:
# - See README.md "Docker" troubleshooting section
# - Check logs: docker compose logs hermes-agent
# - Agent must expose port 8642

services:
# The Claude AI Agent Gateway
# Provides the backend API that the workspace connects to
# The Hermes Agent gateway + dashboard APIs.
# Gateway runs in the foreground on :8642. Dashboard runs as a background
# process on :9119 and is reachable only on the private Docker network.
hermes-agent:
image: nousresearch/hermes-agent:latest
restart: unless-stopped
# The Hermes Agent image entrypoint defaults to the interactive CLI which exits
# immediately under `docker compose up -d`. We override here to start the
# gateway, which is the long-running API/health server the Workspace needs.
Expand All @@ -46,13 +51,17 @@ services:
environment:
# Pass through whichever provider keys are set in .env. hermes-agent
# uses the one that matches the provider configured in
# ~/.hermes/config.yaml (or whatever `claude setup` picked).
# ~/.hermes/config.yaml (or whatever `hermes setup` picked).
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-}
GOOGLE_API_KEY: ${GOOGLE_API_KEY:-}
GROQ_API_KEY: ${GROQ_API_KEY:-}
MISTRAL_API_KEY: ${MISTRAL_API_KEY:-}
HERMES_UID: '10010'
HERMES_DASHBOARD: '1'
HERMES_DASHBOARD_HOST: 0.0.0.0
HERMES_DASHBOARD_PORT: '9119'
# Authentication for the gateway when exposing off-loopback.
# In the default compose setup the gateway is reachable from the
# workspace container over the docker network on hermes-agent:8642,
Expand All @@ -61,37 +70,37 @@ services:
# strong API_SERVER_KEY in .env — the workspace passes it through
# as HERMES_API_TOKEN below. See #122.
API_SERVER_KEY: ${API_SERVER_KEY:-}
# Bind only on the docker-internal interface by default. Set
# API_SERVER_HOST=0.0.0.0 in .env *and* set API_SERVER_KEY if you
# want to expose the gateway to the LAN / Tailscale. See #122.
API_SERVER_HOST: ${API_SERVER_HOST:-127.0.0.1}
# Bind inside the container so the workspace can reach the gateway over
# Docker DNS. The host publish below remains loopback-only.
API_SERVER_HOST: 0.0.0.0
API_SERVER_ENABLED: 'true'
volumes:
# Persist agent state across container recreation. Swap for a
# host-path mount (e.g. `./data:/opt/data`) if you want to edit
# config/skills directly from the host.
- claude-data:/opt/data
- hermes-agent-data:/opt/data
healthcheck:
test: ['CMD-SHELL', 'curl -fsS http://localhost:8642/health || exit 1']
test: ['CMD-SHELL', 'curl -fsS http://localhost:8642/health && curl -fsS http://localhost:9119/api/status || exit 1']
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
start_period: 30s
ports:
- '8642:8642'
- '127.0.0.1:8642:8642'

# The Hermes Workspace Web UI
# Connects to hermes-agent at http://hermes-agent:8642
hermes-workspace:
image: ghcr.io/outsourc-e/hermes-workspace:latest
restart: unless-stopped
depends_on:
hermes-agent:
condition: service_healthy
env_file:
- .env
environment:
HERMES_HOME: /home/workspace/.hermes
HERMES_WORKSPACE_DIR: /workspace
# Internal Docker network URL (not localhost!)
HERMES_API_URL: http://hermes-agent:8642
HERMES_DASHBOARD_URL: http://hermes-agent:9119
# Must match API_SERVER_KEY on the hermes-agent side when that is set
HERMES_API_TOKEN: ${API_SERVER_KEY:-}
# Workspace session password. REQUIRED when HOST is non-loopback (the
Expand All @@ -108,8 +117,12 @@ services:
# that sanitizes these headers — otherwise a client can spoof its IP
# and bypass local-classification / rate limiting. See #125.
TRUST_PROXY: ${TRUST_PROXY:-}
volumes:
- hermes-agent-data:/home/workspace/.hermes
- hermes-workspace-files:/workspace
ports:
- '127.0.0.1:3000:3000'

volumes:
claude-data:
hermes-agent-data:
hermes-workspace-files:
5 changes: 5 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
allowBuilds:
electron: true
electron-winstaller: true
esbuild: true
unrs-resolver: true
2 changes: 1 addition & 1 deletion src/routes/api/claude-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ export const Route = createFileRoute('/api/claude-config')({

return Response.json({
ok: true,
message: 'Config updated. Restart Claude to apply changes.',
message: 'Config updated. Restart Hermes Agent to apply changes.',
})
},
},
Expand Down
17 changes: 10 additions & 7 deletions src/screens/chat/chat-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
// Module-level local model override — set by composer when user picks a local model
// Avoids prop threading. Reset when switching back to cloud models.
export let _localModelOverride = ''
export function setLocalModelOverride(model: string) { _localModelOverride = model }

import {
useCallback,
useEffect,
Expand Down Expand Up @@ -102,6 +97,8 @@ import { useResearchCard } from '@/hooks/use-research-card'
import { useTapDebug } from '@/hooks/use-tap-debug'
import { useChatMode } from '@/hooks/use-chat-mode'
import { useChatActivityStore, type AgentActivity } from '@/stores/chat-activity-store'
import { useSessionModelStore } from '@/stores/session-model-store'
import { _localModelOverride } from './local-model-override'

type ChatScreenProps = {
activeFriendlyId: string
Expand Down Expand Up @@ -976,7 +973,13 @@ export function ChatScreen({
}, [modelsQuery.data])

const gatewayModel = currentModelQuery.data || ''
const currentModel = _localModelOverride || gatewayModel
const modelSessionKey = isNewChat
? 'main'
: forcedSessionKey || resolvedSessionKey || activeSessionKey || activeFriendlyId
const sessionModel = useSessionModelStore((state) =>
state.getModel(modelSessionKey),
)
const currentModel = sessionModel || _localModelOverride || gatewayModel

// Ref so sendMessage can always read latest thinkingLevel without being in deps
const thinkingLevelRef = useRef<ThinkingLevel>(thinkingLevel)
Expand Down Expand Up @@ -2761,7 +2764,7 @@ export function ChatScreen({
disabled={sending || hideUi}
sessionKey={
isNewChat
? undefined
? 'main'
: forcedSessionKey || resolvedSessionKey || activeSessionKey
}
wrapperRef={composerRef}
Expand Down
4 changes: 2 additions & 2 deletions src/screens/settings/providers-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,10 @@ const SETTINGS: Array<SettingDefinition> = [
{
id: 'primary-model',
tab: 'models',
path: 'agents.defaults.model.primary',
path: 'model.default',
label: 'Default model',
description:
'Primary model used for new agents unless a specific agent overrides it.',
'Backend default model used when a chat does not select a per-session override.',
kind: 'text',
placeholder: 'provider/model',
},
Expand Down