| name | agent-provisioning |
| description | Create new Hermes agents — Docker Compose, Photon/iMessage numbers, signal-cli registration, Windows adaptions. |
| version | 1.0.0 |
| author | Justin + Hermes |
| tags | ["agent","provisioning","photon","imessage","docker","signal","windows","nirvana"] |
Agent Provisioning
End-to-end workflow for creating new Hermes agents in the Nirvana ecosystem — from Docker Compose to iMessage number provisioning to signal registration.
DOX Framework (Nirvana Agent Identity)
The Nirvana-Intelligence monorepo uses the DOX framework (https://github.com/agent0ai/dox) for agent identity and project structure:
AGENTS.md files are binding work contracts for their subtrees. They follow a strict section order: Purpose, Ownership, Local Contracts, Work Guidance, Verification, Child DOX Index. They are NOT agent personalities. The root AGENTS.md sets global rules; child AGENTS.md files scope to their subtree.
SOUL.md in $HERMES_HOME is the agent's identity/personality. Hermes auto-loads it via load_soul_md() in agent/prompt_builder.py. It replaces the default "You are Hermes Agent..." identity. No extra config, no --personality flag, no env var needed.
- Never overwrite
AGENTS.md files with agent personality content. AGENTS.md = DOX contracts, SOUL.md = agent identity.
- DOX is a single-file framework — clone https://github.com/agent0ai/dox, copy its
AGENTS.md to your project root. The agent reads it and builds the hierarchy. No installation, no dependencies, no runtime.
- The DOX hierarchy: root
AGENTS.md → child directory AGENTS.md files → agent SOUL.md for personality.
- Before editing any file, read the DOX chain from root to target. After meaningful changes, update affected AGENTS.md files.
DOX AGENTS.md section order (mandatory):
- Purpose — what this subtree is for
- Ownership — who maintains it
- Local Contracts — binding rules for this scope
- Work Guidance — how to do the work correctly
- Verification — how to check correctness
- Child DOX Index — links to child AGENTS.md files
The canonical template AGENTS.md lives at /ssd/Nirvana-Intelligence/agents/templates/nirvana-agent/AGENTS.md on portal-1. See it for the full DOX-compliant template structure.
- NEVER batch-delete DNS records without first auditing what exists. Before touching MX, SPF, DKIM, or any DNS records across domains, LIST every existing record first. Some domains may have production email (MS365, SES, Zoho) that batch operations will destroy. Show the user the audit before any modification. The user will react with extreme frustration if you blindly overwrite production DNS.
Canonical Template
The authoritative agent template lives in the Nirvana-Intelligence repo (private, github.com/chainchopper/Nirvana-Intelligence), cloned locally at C:\Users\iAMBLACK\Nirvana-Intelligence\. Portal-1 does NOT have a clone of this repo.
agents/templates/nirvana-agent/
├── AGENTS.md # DOX format — template contracts and usage rules
├── docker-compose.yml # bind-mount compose (copy verbatim, no {{VARS}})
├── .env.example # fully documented env template with every possible key
├── config.yaml # minimal Hermes config
├── SOUL.md.example # DOX soul template (identity, branding, directives)
└── generate.sh # spawn a new agent: ./generate.sh <agent-name>
To spawn a new agent:
cd C:/Users/iAMBLACK/Nirvana-Intelligence/agents/templates/nirvana-agent
./generate.sh <agent-name>
docker compose up -d
The template uses no {{VARS}} — files are copied verbatim. All customization happens in data/.env, data/config.yaml, and data/SOUL.md.
- Creating a new named agent (e.g. "Aubrey", "Ethoscope", "Chase")
- Provisioning a new Photon/iMessage number for an agent
- Setting up a Windows-compatible Docker Compose for an agent
- Registering a signal-cli number for an agent
Prerequisites
- Hermes profile created:
hermes profile create <name>
- DeepSeek (or other provider) API key in profile's
.env
- SOUL.md written with agent personality
- Photon dashboard access (for device-code approval)
- Agent containers on fast disks — on IAMBLACK (Windows), the canonical agent deployment root is
J:\\NIRVANA-AGENTS\\ (all-caps, hyphenated — this is the user's convention for significant root directories). Each agent has its own self-contained directory with the bind-mount pattern (./data:/opt/data, .env inside data/). Do NOT deploy to J:\\agents\\ or any other location — always check the root exists and READ its AGENTS.md before creating anything.
Workflow
1. Docker Compose (Portal-1 / Linux)
Canonical pattern: bind mount — ./data:/opt/data. The agent's entire identity (.env, config.yaml, SOUL.md, sessions/, memories/, skills/, state.db) lives in data/ alongside the compose file. Portable, auditable, backup-friendly. Copy the directory = copy the agent.
services:
agent:
image: nousresearch/hermes-agent:latest
container_name: nirvana-agent-<name>
restart: unless-stopped
network_mode: host
env_file:
- data/.env
environment:
- HERMES_HOME=/opt/data
- HERMES_ALLOW_ROOT_GATEWAY=1
volumes:
- ./data:/opt/data
Why bind mounts over named volumes:
- The agent directory IS the agent — copy it anywhere,
docker compose up -d, it runs
.env travels with the data it configures — no separate credential file one level up
- Easy to audit:
ls ~/.hermes/agents/<name>/data/ shows everything
- Backup:
tar czf agent.tar.gz ~/.hermes/agents/<name>/
- Named volumes hide data in
/var/lib/docker/volumes/ — opaque and non-portable
Platform extensions — add to the base compose as needed:
Signal agents: add signal-cli binary mount + data mount, Java install entrypoint
Photon/iMessage agents: add npm install sidecar entrypoint
API-only agents: the base compose above is sufficient — enable api_server platform in data/config.yaml
2. Windows Docker Compose
For local Windows development (IAMBLACK machine), agents live on fast SSDs under J:\\\\NIRVANA-AGENTS\\\\ (all-caps convention — the user uses CAPS to mark significant custom root directories). Each agent directory is fully self-contained with the bind-mount pattern (./data:/opt/data, .env inside data/):
J:\\\\NIRVANA-AGENTS\\\\
├── aubrey\\\\ # Photon/iMessage agent
│ ├── docker-compose.yml
│ ├── data\\\\ # bind-mounted as /opt/data
│ │ ├── .env
│ │ ├── config.yaml
│ │ ├── SOUL.md
│ │ ├── state.db
│ │ └── sessions/
│ └── sidecar-index.mjs
├── chase\\\\ # API-only agent
│ ├── docker-compose.yml
│ └── data\\\\
│ ├── .env
│ ├── config.yaml
│ ├── SOUL.md
│ └── state.db
├── ethoscope\\\\ # Signal agent (+15133273916)
│ ├── docker-compose.yml
│ ├── data\\\\
│ │ ├── .env
│ │ ├── config.yaml
│ │ ├── SOUL.md
│ │ └── state.db
│ └── signal-data\\\\ # Signal registration data
└── magneto\\\\ # Signal agent (+19317174760)
├── docker-compose.yml
├── data\\\\
│ ├── .env
│ ├── config.yaml
│ ├── SOUL.md
│ └── state.db
└── signal-data\\\\ # Signal registration data
Key differences from Linux:
- Port mapping instead of
network_mode: host — "127.0.0.1:<port>:8642" (API) or "127.0.0.1:<port>:8000" (Photon)
- Bind mounts work on Windows via Docker Desktop (drive must be shared in Settings → Resources → File Sharing)
MSYS_NO_PATHCONV=1 required for all docker commands in git-bash
- See repo templates:
windows/template/docker-compose.template.yml
Deploy checklist for Windows agents:
- Copy compose from template, configure for the agent
- Write
data/.env with DEEPSEEK_API_KEY + platform-specific keys
- Write
data/SOUL.md with agent identity
- Write
data/config.yaml with model and platform config
- For Photon: copy
sidecar-index.mjs alongside compose
docker compose up -d
- Verify:
docker exec <container> cat /opt/data/gateway_state.json
- Check logs:
docker exec <container> grep "photon\|api_server\|✓" /opt/data/logs/gateway.log | tail -5
Never share credentials between agents — each agent's .env is its own. Grep-check before starting: grep '^PHOTON_PROJECT_ID=' J:/NIRVANA-AGENTS/*/data/.env — every agent must have a unique project ID (or no Photon at all).
3. Migrating Existing Agents to Bind Mounts
Convert an agent from named Docker volumes to ./data:/opt/data without losing state:
cd ~/.hermes/agents/<name>
docker compose down
mkdir -p data
docker run --rm \
-v nirvana-agent-data-<name>:/vol \
-v $(pwd)/data:/out \
alpine cp -a /vol/. /out/
sudo chown -R 10000:10000 data/
sudo chmod 755 data/
mv .env data/.env
docker compose up -d
sleep 25
docker compose logs --tail 10
docker exec <container> cat /opt/data/gateway_state.json
Post-migration checklist:
Common migration failures:
| Symptom | Cause | Fix |
|---|
PermissionError: /opt/data/.env | data/ owned by wrong UID | sudo chown -R 10000:10000 data/ |
PermissionError: /opt/data/gateway.lock | Files not writable by container user | sudo chown -R 10000:10000 data/ |
open data/.env: permission denied (compose) | data/ directory not traversable | sudo chmod 755 data/ |
| Signal daemon gets empty port | SIGNAL_HTTP_URL from env_file not in shell | Hardcode port in compose environment: AND entrypoint |
| Container starts but gateway fails | File ownership wrong after copy | Re-run chown + chmod, then docker compose down && docker compose up -d |
Safety rules for migration:
- NEVER delete the old named volume until verified. Run
docker volume ls | grep nirvana to find it.
- Signal registration data in
~/.local/share/signal-cli-<name>/ is on a SEPARATE mount — this migration does not touch it.
- After verifying
docker compose logs shows clean startup and platform connections, remove old volume: docker volume rm nirvana-agent-data-<name>.
- Hermes container UID: the
nousresearch/hermes-agent:latest image runs as hermes:hermes with UID 10000 (not 1000). All bind-mounted files must be owned by or readable by UID 10000 inside the container. Check with: docker run --rm nousresearch/hermes-agent:latest id hermes.
Photon Dashboard API: https://app.photon.codes/api/...
Spectrum Cloud API: https://spectrum.photon.codes/...
Full flow — use the Dashboard REST API (not hermes photon setup) when creating a separate project for a new agent:
token = "..."
proj = POST("https://app.photon.codes/api/projects", json={"name":"Aubrey","location":"United States"})
proj = PATCH(f"https://app.photon.codes/api/projects/{proj['id']}",
json={"name":"Aubrey", "spectrum":True})
proj = GET(f"https://app.photon.codes/api/projects/{proj['id']}")
Why API instead of hermes photon setup: hermes photon setup always uses the "Hermes Agent" project. Running it under a cloned profile writes the parent's credentials to the clone's .env — it does NOT create a new project. For separate iMessage lines per agent, create projects via the Dashboard API with the existing device token.
All Dashboard API calls go to https://app.photon.codes/api/... with Authorization: Bearer <token>. The Spectrum host (spectrum.photon.codes) uses gRPC, not REST — don't hit it directly.
3. Signal Registration
Signal numbers are unique — one active connection per number. Plan accordingly.
Self-Contained signal-cli Pattern (Windows Docker)
Do NOT download signal-cli inside the container at startup. Bundle it in the agent directory and mount read-only:
cp -r /path/to/signal-cli-0.14.5 J:/NIRVANA-AGENTS/magneto/signal-cli
This avoids network failures (wget can't reach GitHub from inside Docker) and version drift. The signal-cli binary is self-contained (JAR files + shell wrapper) and works on any Linux container.
Port rules: Always use high ports for the daemon (18082+). Never use low ports (25, 80, 443, etc.) for signal-cli. The daemon port must be hardcoded in both environment: AND the entrypoint command — env_file vars are not available in shell expansions.
signal-cli binary location (Windows): J:\\SharedTemp\\signal-cli-0.14.5 — the current stable version fully extracted with bin/signal-cli (POSIX shell script) and lib/ (JAR files). Copy into agent directory: cp -r J:/SharedTemp/signal-cli-0.14.5 J:/agents/magneto/signal-cli. Mount as ./signal-cli:/opt/signal-cli:ro in docker-compose.
Two paths — choose based on the number type:
A. signal-cli link (preferred — real phones with Signal app)
If the number belongs to a physical phone with Signal installed, use the device linking flow. No captcha needed:
signal-cli link -n "HermesAgent"
After linking, the daemon auto-discovers the account. This is the path the Hermes docs recommend — use hermes gateway setup → Signal for an interactive wizard. On portal-1, Hermes CLI is at ~/hermes-agent/venv/bin/hermes.
B. Captcha registration (Telnyx / virtual numbers)
For virtual numbers without a physical Signal app, you must register via captcha:
Version compatibility:
- signal-cli 0.13.12: deprecated by Signal servers (StatusCode 499)
- signal-cli 0.14.5: current stable (as of June 2026). Registration data from 0.13.x is NOT forward-compatible — accounts will show "User is not registered" after upgrade and require re-registration. Plan for re-registration when upgrading across the 0.13→0.14 boundary.
- Match the version portal-1 uses. If you upgrade signal-cli, expect to re-register all numbers (requires CAPTCHA per account).
Dedicated daemon pattern (portal-1): One daemon container serves all agents in multi-account mode. Do NOT start per-agent daemons — they compete for port 18082. All agents share SIGNAL_HTTP_URL=http://127.0.0.1:18082 pointing to the nirvana-signal-daemon container.
Registration flow (via daemon JSON-RPC):
- Get captcha token from https://signalcaptchas.org/registration/generate.html (must be solved manually)
- Register via daemon:
curl -s -X POST http://127.0.0.1:18082/api/v1/rpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"register","params":{"account":"+1XXXXXXXXXX","captcha":"signalcaptcha://..."},"id":"1"}'
- For Telnyx numbers: poll Telnyx Messaging API for the 6-digit SMS verification code
- Verify:
curl -s -X POST http://127.0.0.1:18082/api/v1/rpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"verify","params":{"account":"+1XXXXXXXXXX","verificationCode":"123456"},"id":"1"}'
4. Agent .env
Minimum .env for a new agent:
DEEPSEEK_API_KEY=sk-...
PHOTON_PROJECT_ID=<spectrumProjectId>
PHOTON_PROJECT_SECRET=<secret>
PHOTON_ALLOWED_USERS=*
PHOTON_HOME_CHANNEL=<agent's iMessage number>
For agents that receive ANY number (verification relay): PHOTON_ALLOWED_USERS=*
5. SOUL.md Identity
Hermes automatically loads $HERMES_HOME/SOUL.md as the agent's identity (the load_soul_md() function in agent/prompt_builder.py). No extra config, no --personality flag, no env var needed. The docker-compose just mounts it:
volumes:
- ./SOUL.md:/opt/data/SOUL.md:ro
Write the SOUL.md with the agent's role, tone, boundaries, and behavior rules. It replaces the default "You are Hermes Agent..." identity.
Pitfalls
-
Container-first deployment (HARD RULE) — Justin wants ALL agents in their own Docker containers. Never create a Hermes profile on the host as the primary agent deployment. hermes profile create creates a config directory, but the agent itself must run in a container. The host stays clean; agents are isolated.
-
CRITICAL: Never share auth.json between agents — auth.json contains the Photon device token (credential_pool.photon), project credentials (photon_project), and user registration (photon_user). When you docker cp the host's auth.json into a container, the container agent gains the host's Photon identity. Both agents then share the same iMessage line, same project, same credentials. The user cannot tell which agent they're talking to. Each agent must get its own Photon device login (hermes photon setup inside the container) creating its own auth.json with its own project and number.
-
CRITICAL: Cross-container credential contamination — when writing a container agent's .env, NEVER copy the host agent's Photon PHOTON_PROJECT_ID / PHOTON_PROJECT_SECRET. Each container agent with its own Photon number MUST have its OWN Photon project (separate Dashboard project with its own spectrumProjectId and projectSecret). If a container .env points to the host's project, the container agent will hijack the host's Photon connection.
-
MANDATORY pre-start check for container agents: before ever starting a new container agent with Photon, grep BOTH .env files: grep '^PHOTON_PROJECT_ID=' <host-.env> <container-.env>. If the project IDs match, the container was written with the host's credentials — STOP and fix the container .env with its OWN project before starting. Starting a container with the host's project ID will either hijack the connection immediately or rotate the secret on first gateway run, breaking the host agent. The user's explicitly stated project info overrides whatever is in the .env file ("the .env it lies").
-
Recovery from auth.json contamination: when a container agent has been given the host's auth.json:
- Stop the container immediately:
docker compose down
- Remove the contaminated auth.json:
rm data/auth.json (or delete from volume)
- Run
hermes photon setup INSIDE the container to create a fresh device login with its own project
- On the HOST, verify
auth.json was not corrupted: hermes photon status should show the host's original project
- If the host's project secret was rotated by the hijacking agent, fetch the fresh secret from Photon dashboard at
app.photon.codes
- Restart host gateway first, verify
state: connected, then restart container
-
Recovery from credential contamination: when a container agent has already hijacked the host's Photon connection:
- Stop BOTH the host gateway AND the container agent immediately
- Get a fresh project secret from the Photon dashboard at
app.photon.codes (the host's project — find it by spectrumProjectId). The secret was rotated by the hijacking agent; the old one returns 401.
- Update host
.env with the fresh secret: sed the PHOTON_PROJECT_SECRET line
- Update container
.env with its OWN PHOTON_PROJECT_ID, PHOTON_PROJECT_SECRET, PHOTON_ALLOWED_USERS, and PHOTON_HOME_CHANNEL — NOT the host's values
- Restart host gateway first, verify Photon shows
state: connected via grep photon gateway_state.json
- Only then start the container agent with its own credentials
- Never restart the container agent until its
.env has been verified to contain its own project ID, not the host's
-
Container .env changes need full recreate — docker restart caches env from container creation. After changing Photon values in .env: docker stop <container> && docker rm <container> && cd agent-dir && docker compose up -d. Verify with docker exec <container> env | grep PHOTON_.
-
HARD RULE: NEVER bulk-kill node processes — taskkill /IM node.exe, pkill node, Stop-Process node, killall node are FORBIDDEN on the Windows host. These commands kill every Node.js process on the machine including running servers, Docker containers, dev tools, sidecars, Pixel Streaming, NPU-STACK, and Electron apps. Only kill specific PIDs identified via netstat -ano | findstr \"LISTENING:<port>\".. Switch to high port (8790+) via PHOTON_SIDECAR_PORT. NEVER kill all node.exe to free a port — target only the specific PID. The reaper that cleans orphaned sidecars is DISABLED on Windows (if sys.platform == \"win32\": return), so zombie processes never auto-clean and must be handled manually. Full recipe: references/photon-port-conflict-windows.md.
-
Maddy config is fragile — don't sed-edit it — Maddy's config parser is strict: no semicolons on directive lines, semicolons only for inline commands inside blocks. Sed-based edits (sed -i 's|old|new|' maddy.conf) routinely produce invalid configs that crash Maddy with cryptic errors like "character not allowed in directive name: ;" or "unexpected directive: host". Fix: write the full config from a known-good template using write_file on the host, then docker cp it into the container. Modifying live configs with sed is a losing battle. Restart Maddy with docker restart maddy after config changes and verify: docker logs maddy 2>&1 | tail -3 should show "server started" not "Error:".\n- Never nuke volumes with down -v that contain registration data — Signal registration data (account keys, UUIDs, pre-key bundles) lives in the signal-cli data volume. docker compose down -v permanently destroys this. Before down-v: check what volumes exist with docker volume ls | grep <agent>, back up with docker run --rm -v <volume>:/vol -v $(pwd):/out alpine cp -a /vol/. /out/, or use plain docker compose down (no -v) which preserves volumes.
-
READ BEFORE YOU ACT — HARD RULE — always survey existing files, directories, and container state before creating, moving, or modifying anything. Before writing a new compose file, check if one already exists. Before creating a directory, check what directories are already there. Before deleting DNS records, list every existing record first. The user has deployed agent infrastructure that you didn't create — read it before touching it. Acting without reading is the single most frequent cause of the user's frustration.\n- DOX CHAIN — walk the AGENTS.md hierarchy before ANY file touch — the user's repos use the DOX framework where AGENTS.md files are binding work contracts. Before touching ANY file: (1) find the nearest AGENTS.md by walking up from the target path, (2) READ it, (3) follow its rules. When the user points to a directory like J:\\NPU-STACK\\ or J:\\NIRVANA-AGENTS\\, the FIRST action is ls + read AGENTS.md, not mkdir or docker compose. Concrete failures this rule prevents: building agents in J:\\agents\\ when the real agents were in J:\\NIRVANA-AGENTS\\, guessing Magneto was "AEI Chief Intelligence Officer" when his actual identity doc (AtGPT OG.txt) says "Official AI Overseer for Atmospheric Energy Incorporated," writing ethoscope's compose from scratch when a working Signal-enabled compose already existed at his real location. The user WILL notice and WILL be furious if you recreate things that already exist.\n- Research the agent BEFORE writing its SOUL.md — don't guess what an agent does from its name or from docker-compose labels. Read the source identity documents the user points to (e.g., AtGPT OG.txt for Magneto at G:\\Black Nation Express\\ATMOSPHERIC ENERGY\\). Labels like nirvana.role=aei-intelligence are generic deployment tags, not the agent's true identity. The user keeps authoritative identity documents in project directories — ask where they are if the SOUL.md is missing. A wrong SOUL.md produces an agent with the wrong personality, tone, and knowledge domain. When the user shows you React component files (like Magneto Master .md which is a UI source file), keep looking — the actual identity is elsewhere.
-
Agent container ≠ application deployment — when the user says "look at the repo to understand what X does," they mean read it for the persona/soul, NOT deploy the entire application stack. Ethoscope is a 12K-file monorepo application already running on Docker elsewhere; I mistakenly cloned it and tried to docker-compose the full build, which locked up Docker Desktop. Read for context, then create the agent container.
-
Pure agent containers (API-only, no messaging) need minimal compose: env_file, SOUL.md mount, hermes gateway run --no-supervise. No signal-cli download, no Photon sidecar bootstrap. Ethoscope and Chase follow this pattern — platform-specific setup (Photon, Signal) comes later when needed.
-
API server port is 8642, not 8000 — the gateway listens on 8000 for internal services; the API server platform binds to 8642 by default. Port mapping: 127.0.0.1:<host>:8642. Must also set API_SERVER_HOST=0.0.0.0 in environment for Docker port forwarding (defaults to 127.0.0.1 which Docker can't forward).
-
Write config.yaml at container startup — the nousresearch/hermes-agent image has no default config. The entrypoint MUST write config.yaml with model.default, model.provider, model.base_url, and any gateway.platforms.<name>.enabled: true entries before starting the gateway. Use a heredoc in the compose command: block.
-
Shared API keys are fine — all agents can use the same DEEPSEEK_API_KEY. Each agent's .env file stores its own copy (fully self-contained). Only Photon secrets must be per-agent.
-
Gateway logs are inside the container at /opt/data/logs/gateway.log — not on stdout. Use MSYS_NO_PATHCONV=1 docker exec <container> cat /opt/data/logs/gateway.log to debug.
-
Photon requires the platform plugin installed — having PHOTON_PROJECT_ID and PHOTON_PROJECT_SECRET in env is NOT sufficient. The Photon platform plugin must be installed at /opt/hermes/plugins/platforms/photon/ for the adapter to load. The plugin is NOT in the base Docker image — it lives in the container's writable layer and is destroyed on docker compose down. Without it: gateway starts with only 2 platforms even though Photon is enabled in config, and hermes photon setup returns "invalid choice: 'photon'". Verify: ls /opt/hermes/plugins/platforms/ must include photon/. See nirvana-container-agents skill, references/photon-plugin-persistence.md.
-
Standard Windows agent port assignments (current) — all API servers map to container port 8642:
| Agent | Host Port | Platform | Internal Daemon |
|---|
| aubrey | 8201→8642 | Photon iMessage | sidecar on 8790 |
| chase | 8203→8642 | API Server | none |
| ethoscope | 8202→8642 | Signal +15133273916 | daemon on 18083 |
| magneto | 8204→8642 | Signal +19317174760 | daemon on 18082 |
Daemon ports must be UNIQUE across agents — never use 8080 (reserved by other services). Use 18082+ range.
-
Portal-1 Hermes CLI path — Hermes is a symlink at ~/.local/bin/hermes → /home/fanalogy/hermes-agent/venv/bin/hermes. Only on login PATH (.profile adds .local/bin). Non-login SSH sessions need the full venv path. Proper install: ~/.local/bin/uv tool install --editable /home/fanalogy/hermes-agent — uv manages the venv, binary, and updates. The update command: cd /home/fanalogy/hermes-agent && /home/fanalogy/hermes-agent/venv/bin/hermes update (or just hermes update from a login shell)., ghostin, and other agents on portal-1 are production. When building local containers, never modify or restart portal-1 agents unless explicitly directed.
-
hermes photon setup reuses parent project — Running under a cloned profile (hermes -p <name> photon setup) does NOT create a new project — it writes the same "Hermes Agent" project credentials to the clone. Use the Dashboard API directly for separate projects.
-
PATCH spectrum enable requires both fields — PATCHing {"spectrum":true} alone returns 422. Must include {"name":"...", "spectrum":true}.
-
SMS receptionist pattern (Aubrey) — For automated verification-code catching: create a dedicated agent with PHOTON_ALLOWED_USERS=* whose sole job is receiving and relaying SMS codes. Other agents use this agent's number as their home channel. No human should touch verification codes.
-
MSYS path translation on Windows git-bash — docker exec and docker cp paths get mangled. See references/windows-msys-pitfalls.md.
-
Docker gateway logs — docker logs <container> stdout does NOT show platform connection status. Gateway writes detailed logs to /opt/data/logs/gateway.log inside the container. Use docker exec <container> sh -c 'grep -i photon /opt/data/logs/gateway.log | tail -5' to check platform connectivity.
-
SSH key selection — check ~/.ssh/config before guessing keys. Host entries there (e.g. portal-1) have the right IdentityFile.
-
Don't over-provision Photon projects — the user already has approved device codes. hermes photon setup initiates a NEW device-code flow each time it runs under a profile without an auth.json. Check auth.json for existing tokens before running setup. The Dashboard API (app.photon.codes/api/projects) lists existing projects.
-
Photon send_message requires PHOTON_SIDECAR_TOKEN in .env — the send_message tool's standalone Photon path reads PHOTON_SIDECAR_TOKEN from the .env file at session start, not from live os.environ. Adding it via export mid-session won't work. The token must be in the profile's .env before the gateway starts. Without it, outbound Photon sends fail with "Photon standalone send requires a running sidecar with PHOTON_SIDECAR_TOKEN set." The gateway handles inbound Photon without this (it generates its own token), but outbound send_message needs it explicitly. Fix: echo "PHOTON_SIDECAR_TOKEN=<value>" >> ~/.hermes/.env and restart gateway.
-
Host must not have same messaging platform as container agents — when the host Hermes gateway has a platform adapter enabled (e.g., Signal) that points to the same number/account as a container agent, the host intercepts all messages before the container agent sees them. The user sends a message to Magneto on Signal; the host gateway processes it and creates a session locally instead of routing to Magneto's container. Fix: remove the platform from the host entirely — delete platform-specific env vars from host .env, delete the platform entry from platform_toolsets in config.yaml, restart host gateway. Verify: grep -ri <platform> ~/AppData/Local/hermes/.env ~/AppData/Local/hermes/config.yaml must return empty. After restart, gateway_state.json must NOT list the removed platform. If platform persists: check for orphaned gateway processes that may have cached the config.
-
Never speculate about files you didn't create — if you see a file and don't know its origin or purpose, do NOT mention it or propose extracting it. Only reference files you created or the user explicitly discussed. If corrected on this, stop immediately — don't explain or defend.
-
signal-cli 0.14.5 daemon does NOT support --config flag — signal-cli daemon --help shows --http, --dbus, --socket, --tcp, --no-receive-stdout, but NO --config or --data-path argument. The daemon ALWAYS uses the default config directory (~/.local/share/signal-cli). To use a custom data directory, mount it to the default path: - ./signal-data:/opt/data/.local/share/signal-cli. Do NOT try --config /opt/signal-data — it will fail with error: unrecognized arguments: '--config'. The SIGNAL_CLI_CONFIG_DIR env var is also NOT used by the daemon. — in YAML block scalars (|), backslashes are literal. A line ending with \\\\ in YAML = \\\\ in the shell command (two literal characters \\), which signal-cli parses as an argument and crashes with error: unrecognized arguments: '\\'. Fix: put the daemon command on a single line with no backslash continuation: nohup /opt/signal-cli-0.14.5/bin/signal-cli daemon --http 127.0.0.1:18082 > /tmp/signal-daemon.log 2>&1 &. Never use backslash-newline continuations when calling signal-cli daemon from a compose command: block. Verify with: grep -n "nohup.*daemon" docker-compose.yml — the output should be one line, no trailing backslashes. — Docker Compose env_file injects vars into the container PROCESS environment, not the COMPOSE interpreter. Shell expansions in command: (like ${SIGNAL_HTTP_URL} or $(grep -oP ...)) run at COMPOSE parse time, before env_file vars are loaded. The signal daemon must use HARDCODED port values in both the compose environment: section AND the entrypoint command. Example: command: - | ... signal-cli daemon --http 127.0.0.1:18082. Do NOT use ${SIGNAL_HTTP_URL} or ${SIGNAL_PORT} in shell expansions within the command block — they will be empty at parse time., the conversation context compacts and the agent loses track of in-progress work. After any restart: (a) re-check the current state of all containers with docker ps, (b) re-read the DOX chain, (c) report what's running vs what was in progress before continuing. Never assume the state is the same as before the restart.
-
Portal-1 DNS resolution failures — hermes update / git fetch may fail with "Could not resolve host: github.com" even when nslookup works. Root cause: nscd caching a stale negative DNS entry (triggered by IPv6 lookup attempts when no IPv6 default route exists). Fix: sudo systemctl restart nscd. The fanalogy user has (ALL) NOPASSWD: ALL sudo.
-
Cloudflare Email Routing blocks MX records — if Email Routing is enabled for a zone, the Cloudflare API rejects MX record creation (code 890190). The API token also needs Email Routing read permission to inspect existing rules. Either disable Email Routing in the Cloudflare dashboard first, or grant the token Email Routing scope.
-
Cloudflare Email Routing worker rules are dashboard-only — the REST API rejects "type":"worker" routing rules with code 2003. Wrangler doesn't support [[email_rules]]. Only the Cloudflare Dashboard can create worker-as-destination routing rules. Full details: references/cloudflare-email-worker-limitation.md.
-
NEVER create email routing rules blindly — each domain needs its catch-all rule created in the dashboard. The API can't help. Ask the user to confirm before touching any domain with existing email (MS365, SES, Zoho).
-
signal-cli data directory structure — signal-cli expects data files under a data/ subdirectory: signal-data/data/accounts.json, signal-data/data/<account-id>, signal-data/data/<account-id>.d/. Files at the root level (e.g., signal-data/accounts.json) are IGNORED by the daemon. After migrating from Docker volumes or copying backup data, always move files into the data/ subdirectory. Verify with: ls J:/NIRVANA-AGENTS/<agent>/signal-data/data/ — must show accounts.json and account folder(s). Root-level duplicates should be deleted. The mount path in docker-compose should be ./signal-data:/opt/data/.local/share/signal-cli (since HOME=/opt/data inside the container, and signal-cli uses ~/.local/share/signal-cli).
-
Photon disabling for non-Photon agents — the Docker image ships with Photon plugin pre-installed. It auto-loads and creates an adapter if photon: enabled: true appears in config.yaml platforms section, causing errors when no Photon credentials exist. For Signal-only or API-only agents, explicitly set photon: enabled: false in the platforms: section of config.yaml. Also strip all PHOTON_* vars from the agent's .env. Recreate the container (not just restart) after changes.
environment:
- HERMES_HOME=/opt/data
- HERMES_ALLOW_ROOT_GATEWAY=1
- API_SERVER_HOST=0.0.0.0
ports:
- "127.0.0.1:8203:8642"
The entrypoint MUST write a config.yaml with gateway.platforms.api_server.enabled: true and the model section. Each agent needs a unique API_SERVER_KEY in .env.
Sharing Google Workspace with agents:
- Copy credential files into the container's data volume:
docker cp ~/.hermes/google_token.json <container>:/opt/data/
docker cp ~/.hermes/google_client_secret.json <container>:/opt/data/
- Verify:
docker exec <container> /opt/hermes/.venv/bin/python /opt/data/skills/productivity/google-workspace/scripts/setup.py --check
- Test:
docker exec <container> /opt/hermes/.venv/bin/python /opt/data/skills/productivity/google-workspace/scripts/google_api.py gmail search "is:unread" --max 1
Credentials are portable — the same token works across containers as long as the OAuth client_id matches. The google-workspace skill must already be installed in the container (it usually is — check /opt/data/skills/productivity/google-workspace/).
Migrating agents between drives:
-
Stop container, docker compose down from old location, copy files (.env, SOUL.md, docker-compose.yml, sidecar-index.mjs) to new directory on fast drive (J:\agents<name>), docker compose up -d
-
Named volumes survive the move (compose project name derives from directory name, so J:\agents\aubrey\ produces volume aubrey_nirvana-data-aubrey — same as before)
-
Don't use down -v unless you want to wipe the volume
-
Photon project secret rotation (CRITICAL) — hermes photon setup ROTATES the project secret on the Spectrum project. The NEW secret is written to auth.json (credential_pool.photon_project[].project_secret), but the agent's .env STILL has the OLD one. Checklist after credential rotation:
- Read
auth.json for the current secret: grep project_secret ~/AppData/Local/hermes/auth.json
- Update agent's
.env with the new secret
- Full container recreate —
docker restart does NOT re-read .env (Docker caches env at container creation). Must docker stop && docker rm && cd agent-dir && docker compose up -d
- Verify:
docker exec <container> sh -c 'env | grep PHOTON_SECRET' shows the new value
Symptom of stale secret: Photon sidecar crashes with SpectrumCloudError: Invalid credentials (status: 401).
-
Photon host sidecar port is 8790, not 8789 — the host Hermes agent's Photon sidecar runs on port 8790 (from PHOTON_SIDECAR_PORT=8790 in .env) to avoid conflicts with container agent sidecars on 8789. When sending iMessages from the host via curl, target port 8790 with the X-Hermes-Sidecar-Token header. Container agents use default 8789. Full send pattern: references/photon-host-send.md.
-
API server port is 8642, not 8000 — when configuring Docker port mappings for API-only agents (Chase, Ethoscope), the Hermes API server binds to port 8642 by default, not 8000. Port 8000 is the gateway's internal port. Docker port mapping must be 127.0.0.1:<host_port>:8642. Additionally, API_SERVER_HOST=0.0.0.0 must be set in the container environment for Docker port forwarding to work (the default 127.0.0.1 bind is unreachable from outside the container). An API_SERVER_KEY env var is also required — any unique string works. The health endpoint is at /health.
-
Photon send_message needs PHOTON_SIDECAR_TOKEN in .env — when the agent tries to send an outbound iMessage via send_message(action='send', target='photon:...'), it spawns a standalone Photon sidecar that requires PHOTON_SIDECAR_TOKEN in the environment. The gateway auto-generates a token for inbound connections, but standalone outbound sends read from os.environ (loaded from .env at session start). Without it, sends fail with "Photon standalone send requires a running sidecar with PHOTON_SIDECAR_TOKEN set." The token value is in auth.json under credential_pool.photon[0].access_token — add it to the profile's .env as PHOTON_SIDECAR_TOKEN=<token>. A gateway restart is needed for the change to take effect.
gateway:
platforms:
photon:
enabled: true
Write this to $HERMES_HOME/config.yaml inside the container. Without it, the gateway reports "No messaging platforms enabled" even with correct env vars.
API server for API-only agents: the Hermes API server listens on port 8642, not 8000. For Docker containers, you MUST:
-
Map "127.0.0.1:8xxx:8642" (not :8000)
-
Set API_SERVER_HOST=0.0.0.0 in the container environment (default 127.0.0.1 is unreachable from the Docker host due to loopback isolation)
-
Set API_SERVER_KEY=<unique-key> in .env — the API server refuses to start without it, even on loopback
-
cPanel/CloudLinux users: nscd DNS cache poisoning — When hermes update or git fetch fails with "Could not resolve host: github.com" but nslookup/dig work, nscd (Name Service Cache Daemon) has cached a stale negative DNS response. IPv6 is typically enabled without a default route on cPanel servers, causing the initial DNS failure that gets cached. Diagnose: strace -e trace=connect -f git ls-remote https://github.com/... 2>&1 | grep nscd — if you see /var/run/nscd/socket, nscd is the culprit. Fix: sudo systemctl restart nscd (requires passwordless sudo — fanalogy has NOPASSWD: ALL on portal-1). Full recipe: references/portal-1-nscd-dns-fix.md. — it modifies the host's auth.json and .env, not the container's. Container agents need their own isolated Photon config. For separate projects per agent, use the Dashboard REST API at app.photon.codes/api/projects.
-
Docker image sidecar version mismatch — the nousresearch/hermes-agent:latest image (as of 2026-06-12, sha256:3394ab3...) ships an older Photon sidecar (542 lines, no WATCH_STDIN) that crashes with exit code 1 when spawned by the current adapter. Fix: (1) copy the host's working sidecar: cp ~/.hermes/hermes-agent/plugins/platforms/photon/sidecar/index.mjs ./sidecar-index.mjs, (2) add a volume mount in docker-compose: - ./sidecar-index.mjs:/opt/hermes/plugins/platforms/photon/sidecar/index.mjs:ro, (3) nuke stale volumes with docker compose down -v before first up, (4) ensure config.yaml in the volume has model.default, model.provider, model.base_url, and gateway.platforms.photon.enabled: true (the entrypoint should write this before starting), (5) run npm install inside the container if spectrum-ts version doesn't match. Gateway logs go to files inside the container (/opt/data/logs/gateway.log), not stdout — use docker exec to read them. Full diagnosis and fix recipe: references/photon-sidecar-docker-fix.md.
-
Gateway logs are inside the container, not stdout — docker logs <container> shows entrypoint echo lines and startup banners but NOT platform connection attempts, errors, or agent interactions. The gateway writes to /opt/data/logs/gateway.log inside the container. Always check: docker exec <container> grep -i "photon\\|✓\\|error" /opt/data/logs/gateway.log | tail -10. Also: docker exec <container> cat /opt/data/gateway_state.json for platform state.
-
Check logs before debugging — when a container fails to connect a platform, check gateway_state.json and gateway.log before spending cycles on code-level debugging. 90% of failures are config, credentials, or port conflicts visible in the logs. Running speculative manual sidecar tests, comparing file md5s, and tracing adapter code should come AFTER reading the logs, not before.
-
API server port is 8642, not 8000 — the Hermes gateway defaults to port 8000 for internal communication, but the api_server platform binds to 8642 by default. Docker port mapping must target 8642: - "127.0.0.1:8203:8642". If you map to 8000, the connection establishes but the server returns empty replies (CURLE_GOT_NOTHING).
-
API server MUST bind 0.0.0.0 for Docker — the default bind is 127.0.0.1, which Docker cannot forward from the host. Set API_SERVER_HOST=0.0.0.0 in the compose environment: section. Without this, docker exec inside the container can reach it, but the host cannot.
-
API server requires API_SERVER_KEY — even for loopback-only binds on 127.0.0.1, the api_server platform refuses to start without API_SERVER_KEY set in the container's .env. Any non-empty string works for local development.
6. Memory Configuration
After provisioning, configure the agent's memory provider. Hermes ships 9 external providers — see references/memory-providers.md for the full comparison. The pragmatic default for container agents:
memory:
provider: holographic
Workflow (config file approach):
- Check current:
docker exec <container> hermes memory status
- Patch
data/config.yaml: set memory.provider: holographic (or other provider)
- Restart the container:
docker compose restart (config is read at gateway start)
- Verify:
docker exec <container> hermes memory status → should show Provider: holographic, Status: available ✓
Or use the interactive wizard inside the container: docker exec -it <container> hermes memory setup.
DB stored at $HERMES_HOME/memory_store.db for Holographic (profile-isolated). Each provider's data is scoped per profile.
References
references/cloudflare-email-worker-limitation.md — Cloudflare Email Routing: worker rules are dashboard-only, API rejects them
references/cloudflare-dns-patterns.md — Cloudflare API: zone ops, Email Routing, MX/SPF/DKIM/DMARC records
references/cloudflare-email-routing.md — Cloudflare Email Routing → Worker → Tunnel → Maddy email architecture
references/mail-bridge-architecture.md — Cloudflare Worker → Tunnel → Bridge → Maddy mail pipeline
references/maddy-mail-setup.md — Maddy mail server: multi-domain config, accounts, IMAP, catch-all
references/signal-profile-management.md — Signal profile name/about updates via daemon JSON-RPC
references/signal-registration-telnyx.md — Signal registration via Telnyx SMS
references/telnyx-sim-provisioning.md — Telnyx SIM lifecycle: registration codes, voice enable, SMS/MMS
references/windows-msys-pitfalls.md — MSYS path translation survival guide
references/photon-troubleshooting.md — Photon sidecar debugging: secret rotation, 401 errors, config.yaml, sidecar deps
references/cloudflare-email-routing.md — Cloudflare Email Routing → Worker → Tunnel → Maddy email architecture: full stack setup, DNS records, multi-domain provisioning, pitfalls
references/google-workspace-sharing.md — Share host Google Workspace OAuth tokens with container agents
references/photon-sidecar-docker-fix.md — Docker image sidecar version mismatch: diagnosis, fix, EADDRINUSE
references/google-oauth-portal-1.md — Google OAuth token locations on portal-1
references/maddy-mail-setup.md — Maddy mail server: accounts, passwords, IMAP, catch-all, cPanel conflicts
references/cloudflare-dns-patterns.md — Cloudflare API: zone ops, Email Routing, MX/SPF/DKIM/DMARC records
references/mail-bridge-architecture.md — Cloudflare Worker → Tunnel → Bridge → Maddy mail pipeline, Workers, Tunnels
references/mail-bridge-pattern.md — HTTP→SMTP bridge for Cloudflare Worker ↔ Maddy mail relay
references/telnyx-sim-provisioning.md — Telnyx SIM lifecycle, registration codes, voice enabling, SMS/MMS
references/memory-providers.md — Hermes memory provider comparison and setup for agents
references/signal-registration-telnyx.md — Signal registration via Telnyx SMS: CAPTCHA, daemon JSON-RPC, SMS polling
references/signal-data-structure.md — signal-cli data directory layout and bind mount patterns
references/telnyx-sms-catcher.md — Telnyx inbound SMS webhook catcher: Python HTTP server + CF Tunnel quick setup for verification code retrieval
references/signal-registration-telnyx.md — Signal registration via Telnyx SMS: CAPTCHA, daemon JSON-RPC, SMS polling
references/signal-data-structure.md — signal-cli data directory layout and bind mount patterns
Templates
templates/docker-compose.api-agent.yml — API-only agent (chase pattern): model config, SOUL.md mount, no messaging platform
templates/docker-compose.photon-agent.yml — Photon/iMessage agent (aubrey pattern): sidecar mount, npm install entrypoint, model + photon config