Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# (e.g. on Windows checkouts with autocrlf=true).
*.sh text eol=lf
services/v1-parity/dashboard/entrypoint.sh text eol=lf
hermes/entrypoint.sh text eol=lf
services/hermes/entrypoint.sh text eol=lf
scripts/llamacpp/run-llama-server.sh text eol=lf
# nginx config is COPYed into the codebase-memory-ui image; keep LF so nginx
# doesn't choke on CRLF after a Windows checkout.
Expand Down
10 changes: 5 additions & 5 deletions docs/hermes-agent.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Hermes Agent (Docker-mode)

> ⚠️ **Naming note — v2 is the only stack (V1 top-level tree retired 2026-07-24, commit `62540bf`; see [`LEGACY-CLEANUP.md`](LEGACY-CLEANUP.md)). The repo was later flattened (2026-07-24, commit `2d4bd9c`): the `v2/` directory no longer exists — its contents live at the repo root; there is no v2, there is only Ordo.** Hermes is the stack's assistant-agent layer and **is the default agent**. The stack models an agent as a **data manifest** ([`../services/hermes/agent.yaml`](../services/hermes/agent.yaml)) that the renderer wires into a single `agent` compose service (the hermes web UI ships as the separate `hermes-dashboard` service-plugin, profile `hermes-ui`). The agent contract (chat via model-gateway, tools via mcp-gateway, GPU via the ops-controller `/jobs` scheduler, `.env` read-only) is documented in [`agents.md`](agents.md). Hermes' persistent brain still lives under **`data/hermes/`** (one root, the primary checkout) and the Discord/`SOUL.md`/state notes below remain accurate. The agent image is built from the root **`hermes/Dockerfile`** (that directory is a kept, live Ordo build source — see [`LEGACY-CLEANUP.md`](LEGACY-CLEANUP.md)) per [`history/CUTOVER.md`](history/CUTOVER.md) (`docker build -t ordo/agent-hermes:latest ./hermes`) and selected via `agent: hermes` in `ordo.yaml`. The stack is brought up entirely from the repo root: edit `ordo.yaml` (template `ordo.example.yaml`), render with `ordo render` (`python -m ordo.cli render --out out`), then `docker compose -p ordo … up` from `out/` — see [`operator-guide.md`](operator-guide.md) and [`history/CUTOVER.md`](history/CUTOVER.md).
> ⚠️ **Naming note — v2 is the only stack (V1 top-level tree retired 2026-07-24, commit `62540bf`; see [`LEGACY-CLEANUP.md`](LEGACY-CLEANUP.md)). The repo was later flattened (2026-07-24, commit `2d4bd9c`): the `v2/` directory no longer exists — its contents live at the repo root; there is no v2, there is only Ordo.** Hermes is the stack's assistant-agent layer and **is the default agent**. The stack models an agent as a **data manifest** ([`../services/hermes/agent.yaml`](../services/hermes/agent.yaml)) that the renderer wires into a single `agent` compose service (the hermes web UI ships as the separate `hermes-dashboard` service-plugin, profile `hermes-ui`). The agent contract (chat via model-gateway, tools via mcp-gateway, GPU via the ops-controller `/jobs` scheduler, `.env` read-only) is documented in [`agents.md`](agents.md). Hermes' persistent brain still lives under **`data/hermes/`** (one root, the primary checkout) and the Discord/`SOUL.md`/state notes below remain accurate. The agent image is built from its co-located build context **`services/hermes/`** (`Dockerfile` alongside the `agent.yaml` manifest) via `docker build -t ordo/agent-hermes:latest ./services/hermes` and selected via `agent: hermes` in `ordo.yaml`. The stack is brought up entirely from the repo root: edit `ordo.yaml` (template `ordo.example.yaml`), render with `ordo render` (`python -m ordo.cli render --out out`), then `docker compose -p ordo … up` from `out/` — see [`operator-guide.md`](operator-guide.md) and [`history/CUTOVER.md`](history/CUTOVER.md).

[Hermes Agent](https://github.com/NousResearch/hermes-agent) is the stack's assistant-agent layer. It runs as two compose services — `agent` (Discord / Telegram messaging) and `hermes-dashboard` (web UI, container port 9119, published by Caddy at `https://${CADDY_TAILNET_HOSTNAME}:8447/` — served at its origin root behind a plain SSO proxy, no forwarded-prefix base injection) — that come up with the rest of the stack.

Expand Down Expand Up @@ -126,14 +126,14 @@ Design rationale and known limitations: `docs/superpowers/specs/2026-04-21-herme

## Updating Hermes

The Hermes upstream SHA is pinned in `hermes/Dockerfile` as `ARG HERMES_PINNED_SHA=...`. To upgrade:
The Hermes upstream SHA is pinned in `services/hermes/Dockerfile` as `ARG HERMES_PINNED_SHA=...`. To upgrade:

1. Check recent commits: `git ls-remote https://github.com/NousResearch/hermes-agent.git main` — pick a SHA.
2. Edit `hermes/Dockerfile`, change the `ARG HERMES_PINNED_SHA` default.
3. Rebuild the image from the root `hermes/` build context (per `history/CUTOVER.md`): `docker build -t ordo/agent-hermes:latest ./hermes` (run from the repo root).
2. Edit `services/hermes/Dockerfile`, change the `ARG HERMES_PINNED_SHA` default.
3. Rebuild the image from the `services/hermes/` build context: `docker build -t ordo/agent-hermes:latest ./services/hermes` (run from the repo root).
4. `cd out && docker compose -p ordo up -d agent hermes-dashboard` (recreates with the rebuilt image).

You can also override without editing the file: `docker build --build-arg HERMES_PINNED_SHA=<sha> -t ordo/agent-hermes:latest ./hermes`.
You can also override without editing the file: `docker build --build-arg HERMES_PINNED_SHA=<sha> -t ordo/agent-hermes:latest ./services/hermes`.

## Troubleshooting

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,8 @@ ordo-ai-stack/
│ ├── codebase-memory/ # Headless codebase-memory MCP
│ ├── codebase-memory-ui/ # Codebase-memory 3D graph UI service
│ ├── worker/ # Background job worker (Dockerfile, worker.py)
│ ├── hermes/ # Hermes agent build context (Dockerfile, entrypoint.sh, plugins/, seed/) + its agent.yaml manifest
│ └── … # edge, model-gateway, mcp-gateway, monitoring, voice, native, …
├── hermes/ # Hermes agent build context (Dockerfile, entrypoint.sh, plugins/, seed/); manifest at services/hermes/agent.yaml
├── ordo/ # Render substrate (Python package): `ordo render`, `ordo detect`, etc.
├── catalog/ # Curated model catalog (models.yaml)
├── auth/ # Edge auth: auth/caddy (Caddyfile), auth/oauth2-proxy (SSO allowlist)
Expand Down
6 changes: 3 additions & 3 deletions docs/runbooks/bounded-hermes.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ and `/health`; it has none of these verbs.)

## What Hermes can still do

Via `hermes/ops_client.py` (the wrapper that talks to `ops-api`):
Via `services/hermes/ops_client.py` (the wrapper that talks to `ops-api`):

- `OpsClient().list_containers()` → `GET /containers`
- `OpsClient().container_logs(name, tail=N)` → `GET /containers/{name}/logs`
Expand Down Expand Up @@ -61,7 +61,7 @@ are three ways to bridge that gap:
OpsClient().restart_container("open-webui")
```
2. **Hermes plugin** (future). Register a `pre_tool_call` hook
(similar to `hermes/plugins/push-through/`) that intercepts the
(similar to `services/hermes/plugins/push-through/`) that intercepts the
built-in docker / terminal tools and routes them through
`OpsClient`. Smaller blast radius than forking upstream.
3. **Fork upstream** (last resort). Maintain a fork of
Expand Down Expand Up @@ -98,7 +98,7 @@ module) to retain more.
(see `docs/operator-guide.md`). Pattern:
`_: None = Depends(verify_token)` → do work → `_audit.record(...)`
→ return.
3. Add a method on `OpsClient` in `hermes/ops_client.py`.
3. Add a method on `OpsClient` in `services/hermes/ops_client.py`.
4. Migrate any caller that needs it.
5. Test, commit, then from `out/`: `docker compose -p ordo restart
ops-api agent`.
Expand Down
2 changes: 1 addition & 1 deletion docs/runbooks/secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,5 +201,5 @@ HuggingFace, Tavily, etc.). Hook into pre-commit if you want.
| `scripts/secrets/decrypt.sh` fails with `Failed to get the data key` | `SOPS_AGE_KEY_FILE` env var not set or key file unreadable | Set `SOPS_AGE_KEY_FILE=$HOME/.config/sops/age/keys.txt` and verify `chmod 600` |
| `Error unmarshalling input json: invalid character` on .env.sops decrypt | SOPS 3.7.x doesn't auto-detect dotenv format | Use `--input-type=dotenv --output-type=dotenv` flags. The decrypt script already does this. |
| Container starts but immediately exits with `cookie_secret must be 16, 24, or 32 bytes` | `OAUTH2_PROXY_COOKIE_SECRET` was generated with `openssl rand -base64 32` (44 chars) | Regenerate with `LC_ALL=C tr -dc 'a-zA-Z0-9' </dev/urandom \| head -c 32`, edit `secrets/.env.sops`, restart |
| Hermes can't reach Discord but token "looks right" | Bridge from `_FILE` to env var didn't run | Confirm `hermes/entrypoint.sh` sources the bridge BEFORE calling the Discord SDK, and that the secret file at `/run/secrets/discord_token` exists in the container |
| Hermes can't reach Discord but token "looks right" | Bridge from `_FILE` to env var didn't run | Confirm `services/hermes/entrypoint.sh` sources the bridge BEFORE calling the Discord SDK, and that the secret file at `/run/secrets/discord_token` exists in the container |
| `docker compose up` fails with a missing bind-mount source for `.../discord_token` or `.../github_backup_pat` | `~/.ai-toolkit/runtime/secrets/` isn't populated (`out/docker-compose.yml` mounts file-form tokens from there via `OPERATOR_SECRETS_DIR`) | Run `scripts/secrets/decrypt.sh` first to populate `~/.ai-toolkit/runtime/secrets/` |
File renamed without changes.
File renamed without changes.
8 changes: 4 additions & 4 deletions services/hermes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ the `<project>/agent-<id>` convention as `ordo/agent-hermes:latest`, and reused
`hermes-dashboard` plugin (V1 runs one Hermes image as both the gateway and the dashboard).

This is an **operator-specific** image: it wraps the operator's Hermes `data/` (SOUL.md, skills,
automation) on top of the pinned Hermes base. V1 builds it from `C:\dev\ordo-ai-stack\hermes`
(`Dockerfile`, multi-stage: node builds the SPA, python runtime installs Hermes at a pinned SHA).
Project buildable image, so `ordo preflight` reports a missing one as "build first".
automation) on top of the pinned Hermes base. It builds from this co-located context
`services/hermes/` (`Dockerfile`, multi-stage: node builds the SPA, python runtime installs Hermes
at a pinned SHA). Project buildable image, so `ordo preflight` reports a missing one as "build first".

## Build
```
docker build -t ordo/agent-hermes:latest C:/dev/ordo-ai-stack/hermes
docker build -t ordo/agent-hermes:latest C:/dev/ordo-ai-stack/services/hermes
```

Referenced (not duplicated) — the Hermes build context + the operator's `data/` are the single
Expand Down
12 changes: 4 additions & 8 deletions services/hermes/agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@ description: >
The default Ordo agent — a persistent, skill-driven orchestrator. Chats via the model-gateway,
runs tools through the mcp-gateway, and requests GPU work through the ops-controller scheduler.
default: true
# image omitted -> resolves to <project>/agent-hermes:latest (the operator builds this from their
# Hermes data/); pin an explicit image here to ship a prebuilt one.
# The agent-hermes image is built OUT-OF-BAND (no in-repo Dockerfile — it's built from the operator's
# Hermes tree), so `build.external` declares that: the substrate test exempts it from the
# "must resolve to a Dockerfile" check. METADATA only; never rendered into compose.
build:
external: true
# image omitted -> resolves to <project>/agent-hermes:latest, built from this co-located build
# context (services/hermes/ + its Dockerfile — the default, so no explicit `build:` is needed);
# pin an explicit image here to ship a prebuilt one instead.
# The agent-hermes image's default CMD is `hermes --help` (prints usage and exits); the gateway is
# started explicitly so the container runs the persistent messaging gateway (mirrors V1 compose).
command: [hermes, gateway]
Expand Down Expand Up @@ -39,7 +35,7 @@ environment:
PYTHONIOENCODING: utf-8
# Scheduler (GPU leases, /status, /model-config) — the `ordo serve` control plane.
OPS_CONTROLLER_URL: http://ops-controller:9000
# Container/compose verbs (ops-router tools via hermes/ops_client.py) live on the
# Container/compose verbs (ops-router tools via services/hermes/ops_client.py) live on the
# SEPARATE Bearer-gated ops-api service — NOT the scheduler (which 404s them; audit P0-2).
OPS_API_URL: http://ops-api:9000
# STT provider base URL — the local faster-whisper service (voice profile). Harmless if voice off.
Expand Down
2 changes: 1 addition & 1 deletion hermes/entrypoint.sh → services/hermes/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# hermes/entrypoint.sh — container startup.
# services/hermes/entrypoint.sh — container startup.
# 1. As root: ensure $HERMES_HOME is writable by the unprivileged hermes user
# (bind mounts from the host can land with mismatched ownership on Docker
# Desktop / virtiofs; without a chmod here, hermes crash-loops on
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
the model defaulting to `terminal: docker ...` (which has no socket and always
fails with "Cannot connect to the Docker daemon").

The OpsClient is the canonical hermes/ops_client.py copied into this plugin
directory at Docker build time (see hermes/Dockerfile).
The OpsClient is the canonical services/hermes/ops_client.py copied into this plugin
directory at Docker build time (see services/hermes/Dockerfile).
"""
from __future__ import annotations

Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion services/v1-parity/dashboard/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ async def auth_middleware(request: Request, call_next):


MODEL_GATEWAY_URL = os.environ.get("MODEL_GATEWAY_URL", "http://model-gateway:11435").rstrip("/")
MODEL_GATEWAY_API_KEY = os.environ.get("MODEL_GATEWAY_API_KEY", os.environ.get("LITELLM_MASTER_KEY", "local")).strip()
MODEL_GATEWAY_API_KEY = (os.environ.get("MODEL_GATEWAY_API_KEY") or os.environ.get("LITELLM_MASTER_KEY", "")).strip()
COMFYUI_URL = os.environ.get("COMFYUI_URL", "http://comfyui:8188").rstrip("/")
MODELS_DIR = Path(os.environ.get("MODELS_DIR", "/models"))
SCRIPTS_DIR = Path(os.environ.get("SCRIPTS_DIR", "/scripts"))
Expand Down
9 changes: 8 additions & 1 deletion tests/substrate/test_build_contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,17 @@ def test_folder_id_differs_from_image_name_resolves_to_folder():
assert RESOLVE(img) == ctx, f"{img} resolved to {RESOLVE(img)!r}, expected {ctx!r}"


def test_hermes_resolves_to_its_in_repo_context():
"""agent-hermes has a co-located in-repo build context (services/hermes/ + Dockerfile), so it
must resolve to that real folder — NOT external. Guards the reorg: a manifest that re-declares
hermes external, or a move that strands its Dockerfile, fails here."""
assert RESOLVE("ordo/agent-hermes:latest") == "services/hermes"
assert (ROOT / "services/hermes/Dockerfile").is_file()


def test_external_agents_declared_out_of_band():
"""Pluggable agent images with no in-repo Dockerfile must be declared external (not silently
treated as pullable) — so a NEW agent lacking both a Dockerfile and build.external fails CI."""
assert RESOLVE("ordo/agent-hermes:latest") == buildspec.EXTERNAL
assert RESOLVE("ordo/agent-openai-agent:latest") == buildspec.EXTERNAL


Expand Down
10 changes: 5 additions & 5 deletions tests/test_hermes_ops_client.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""Tests for hermes.ops_client.OpsClient — the ops-api HTTP wrapper.
"""Tests for the Hermes OpsClient — the ops-api HTTP wrapper.

Moved here from hermes/test_ops_client.py (audit P1-8): the pytest CI job
Moved here from the sibling copy (audit P1-8): the pytest CI job
only collects tests/, so the sibling copy never ran. The module is loaded
by file path (importlib spec_from_file_location) rather than
``from hermes.ops_client import ...`` so collection doesn't depend on
``hermes/`` being importable as a package (it has no __init__.py).
``from services.hermes.ops_client import ...`` so collection doesn't depend
on the build context being importable as a package (it has no __init__.py).

Also updated for the fixed wiring (audit P0-2): OpsClient now talks to
**ops-api** (OPS_API_URL, default http://ops-api:9000), not the
Expand All @@ -25,7 +25,7 @@
from httpx import Response

ROOT = Path(__file__).resolve().parents[1]
OPS_CLIENT_PY = ROOT / "hermes" / "ops_client.py"
OPS_CLIENT_PY = ROOT / "services" / "hermes" / "ops_client.py"

_spec = importlib.util.spec_from_file_location("hermes_ops_client_under_test", OPS_CLIENT_PY)
ops_client_mod = importlib.util.module_from_spec(_spec)
Expand Down
Loading