diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a902f538e..c0a22be9a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Dockerfile b/Dockerfile index 5c4f122f8c..8e8f782158 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index 223fc749f4..290735e122 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 98cf0d8221..b553090582 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,10 +21,13 @@ # 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 @@ -32,10 +35,12 @@ # - 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. @@ -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, @@ -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 @@ -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: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000000..bbc93b4387 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +allowBuilds: + electron: true + electron-winstaller: true + esbuild: true + unrs-resolver: true diff --git a/src/routes/api/claude-config.ts b/src/routes/api/claude-config.ts index c3333b4cd7..dc58e76f4e 100644 --- a/src/routes/api/claude-config.ts +++ b/src/routes/api/claude-config.ts @@ -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.', }) }, }, diff --git a/src/screens/chat/chat-screen.tsx b/src/screens/chat/chat-screen.tsx index 90a83d8578..3302be0a7f 100644 --- a/src/screens/chat/chat-screen.tsx +++ b/src/screens/chat/chat-screen.tsx @@ -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, @@ -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 @@ -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) @@ -2761,7 +2764,7 @@ export function ChatScreen({ disabled={sending || hideUi} sessionKey={ isNewChat - ? undefined + ? 'main' : forcedSessionKey || resolvedSessionKey || activeSessionKey } wrapperRef={composerRef} diff --git a/src/screens/settings/providers-screen.tsx b/src/screens/settings/providers-screen.tsx index 3004cf2047..1cbc2c475e 100644 --- a/src/screens/settings/providers-screen.tsx +++ b/src/screens/settings/providers-screen.tsx @@ -260,10 +260,10 @@ const SETTINGS: Array = [ { 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', },