With Hermes Agent running and accessible via Telegram, the next step was a proper web UI. Hermes Workspace is the open-source companion interface — 3,400+ GitHub stars, built on React 19, and designed specifically for Hermes Agent rather than being a generic chat frontend.

This post covers the full deployment: building a custom Docker image, setting up a Forgejo Actions pipeline, getting the Kubernetes manifests right, and what the Conductor, Operations, and Swarm features actually do.


What Hermes Workspace Is

The workspace is a web UI that attaches to a running Hermes Agent instance. It expects two services:

  • Gateway API on port 8642 — the core Hermes Agent API
  • Dashboard API on port 9119 — config, sessions, skills, and jobs

It provides six main surfaces: Chat, Conductor, Dashboard, Memory browser, Terminal, and Skills. More on the interesting ones later.

The key distinction from OpenWebUI or similar: this isn’t a generic LLM frontend. It’s purpose-built for Hermes Agent’s session model, memory system, and delegation/swarm features. OpenWebUI works fine as a basic chat interface via the OpenAI-compatible endpoint, but Workspace unlocks the full agent feature set.


Building the Docker Image

Hermes Workspace has no official Docker image, so the first step is building one. The approach: maintain a repo on Forgejo with a custom Dockerfile and a Forgejo Actions workflow that clones the upstream source, applies the Dockerfile, and pushes to the private registry.

The pnpm 10 Problem

The upstream repo uses pnpm 10, which introduced a security change: build scripts from dependencies are blocked by default and require explicit approval via pnpm approve-builds. This breaks the standard pnpm install --frozen-lockfile in any CI or Docker build environment.

The dependencies causing the issue: electron@40.9.3, electron-winstaller@5.4.0, esbuild@0.27.7, and unrs-resolver@1.11.1.

Electron is only needed for the desktop app — not for a server/Kubernetes deployment. The fix is a custom Dockerfile that installs with --ignore-scripts and then rebuilds only what’s actually needed:

FROM node:22-slim AS build
RUN corepack enable && apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates git \
    && rm -rf /var/lib/apt/lists/*
WORKDIR /app

COPY package.json pnpm-lock.yaml* ./

# Install without running any postinstall scripts (skips electron binary download)
RUN pnpm install --frozen-lockfile --ignore-scripts && \
    # Rebuild only what the web server actually needs
    pnpm rebuild esbuild unrs-resolver

COPY . .
RUN pnpm build

This skips electron entirely and only rebuilds the native modules the server runtime actually uses.

The Forgejo Actions Workflow

The pipeline:

  1. Checkout the workspace repo (which contains the Dockerfile and Kubernetes manifests)
  2. Clone upstream outsourc-e/hermes-workspace via git clone — not actions/checkout, which would try to resolve the repo via Forgejo’s own base URL
  3. Copy the custom Dockerfile into the upstream source
  4. Build and push to git.ictq.xyz
  5. Bump the image tag in the Kubernetes manifests, commit, and push — triggering ArgoCD
- name: Checkout upstream hermes-workspace
  run: git clone --depth=1 https://github.com/outsourc-e/hermes-workspace.git upstream

- name: Merge upstream source with custom Dockerfile
  run: cp Dockerfile upstream/Dockerfile

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: ./upstream

One important detail: the tag bump commit back to the same repo would trigger another workflow run — infinite loop. Fix with paths-ignore:

on:
  push:
    branches: [main]
    paths-ignore:
      - 'templates/deployment.yaml'

The deployment manifest is the only file the bot touches, so excluding it breaks the loop cleanly.


Kubernetes Deployment

The setup follows the same pattern as Hermes Agent: Forgejo registry, ExternalSecret via OpenBao, Traefik private ingress, NFS storage.

Security Context

Same issue as Hermes Agent: the entrypoint uses gosu to drop privileges, which requires root at startup. The pod needs runAsUser: 0 with the process dropping to uid 10010 after init.

securityContext:
  runAsUser: 0
  runAsNonRoot: false
  fsGroup: 10010
  fsGroupChangePolicy: OnRootMismatch
  seccompProfile:
    type: RuntimeDefault

Persistent Home Directory

Hermes Workspace writes state to /home/workspace/.hermes. Without a persistent volume this directory either fails to create (permission errors) or resets on every pod restart. The fix: mount a PVC at /home/workspace:

volumes:
  - name: hermes-workspace
    persistentVolumeClaim:
      claimName: hermes-workspace
containers:
  - name: hermes-workspace
    volumeMounts:
      - name: hermes-workspace
        mountPath: /home/workspace

Backed by nfs-nas-01, 2Gi — enough for session data, memory files, and workspace state.

Cross-Namespace Service Discovery

The workspace talks to Hermes Agent in a different namespace. Kubernetes DNS handles this cleanly:

env:
  - name: HERMES_API_URL
    value: "http://hermes-agent.hermes-agent.svc.cluster.local:8642"
  - name: HERMES_DASHBOARD_URL
    value: "http://hermes-agent.hermes-agent.svc.cluster.local:9119"

<service>.<namespace>.svc.cluster.local — no NetworkPolicy headaches as long as both namespaces are on the same cluster network.

Ingress

spec:
  ingressClassName: traefik-private
  tls:
    - secretName: tls-hermes-workspace
      hosts:
        - hermes-workspace.ictq.xyz

Private ingress only — this doesn’t need to be internet-facing.


Conductor, Operations, and Swarm

These are the three features that differentiate Workspace from a basic chat UI.

Conductor

The mission orchestrator. You give it a high-level task, it decomposes it into subtasks, spawns parallel subagents, and shows a live worker grid as they run. The terminal spawn tree view from the previous post is the same thing — Conductor is just the web UI version of it, considerably more usable for monitoring complex delegation trees.

Each running agent shows status, tool call count, token consumption, and elapsed time. You can pause, kill, or inspect individual agents mid-run.

Operations

The live agent console. It polls /api/gateway/sessions and shows every active session on the gateway: status (running/thinking/complete/failed/idle), staleness, tokens, and error messages. This is the monitoring layer — Conductor starts agents, Operations watches them.

Status is derived from session metadata with a staleness heuristic: sessions with tokens that haven’t updated in 30+ seconds are likely complete, sessions without tokens that haven’t updated in 60+ seconds are idle. Explicit status fields take precedence when present.

Useful when you have multiple long-running tasks and want to see which ones are stuck.

Swarm

The most ambitious feature. A swarm.yaml in the workspace root defines a team of specialized agents, each with fixed roles, tools, skills, and greenlightRequiredFor rules. The workspace renders this as an isometric office environment where agents move between desks, showing real-time behavior based on their session status.

The default swarm.yaml defines ten specialists:

  • Orchestrator — decomposes missions, routes to specialists, manages human approval gates
  • Builder — scoped implementation, tests, minimal diffs
  • Reviewer — independent code review, merge gate
  • QA — browser smoke tests, regression verification
  • Researcher — GBrain-first research with external verification
  • Ops Watch — gateway/cron/MCP health monitoring
  • KM Agent — knowledge management, Obsidian, drift audits
  • Maintainer — upstream dependency tracking, patch hygiene
  • Strategist — planning, wedges, kill criteria
  • Inbox Triage — capture routing, task/research/defer decisions

The greenlightRequiredFor mechanism is the safety layer. Actions like merge, publish, destructive, or credential-change require explicit human approval regardless of what the agent wants to do. An Orchestrator that decomposes a task into a Builder implementing code and a Reviewer approving the PR still needs your greenlight before anything merges.

For a homelab context, the Swarm is most interesting for: an Ops Watch agent that monitors cluster health, a Researcher that tracks upstream changes to tools you depend on, and a Maintainer that handles dependency update PRs.


Repo Structure

Everything lives in a single Forgejo repo:

hermes-workspace/
├── Dockerfile                        ← custom build, skips electron
├── Chart.yaml
├── .forgejo/
│   └── workflows/
│       └── build.yaml               ← clone upstream, build, push, bump tag
└── templates/
    ├── namespace.yaml
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    ├── serviceaccount.yaml
    ├── pvc.yaml
    ├── externalsecret.yaml          ← HERMES_PASSWORD + HERMES_API_TOKEN
    └── externalsecret-registry.yaml ← imagePullSecret for git.ictq.xyz

ArgoCD watches the templates/ directory and syncs on every tag bump commit.


Current State

With the workspace deployed:

  • Chat with full session persistence and memory browser
  • Conductor for monitoring delegation trees visually
  • Terminal access directly in the browser
  • Skills browser showing the 2,000+ available skills
  • Jobs screen for managing scheduled cron tasks

The Swarm feature is next — defining a swarm.yaml tailored to the actual workflows I run: Azure platform engineering, FinOps analysis, homelab ops, and blog/documentation work. Each of those maps reasonably well to one of the default specialist roles.