Self-hosted Hermes assistant infrastructure for running a private AI assistant with Telegram, dashboard/API access, git-backed agent state, an Obsidian vault, and optional queue-based local ML jobs.
This repository is the public/platform layer. Personal identity, memories, private workflows, and user-specific skills live in a separate private agent-state repository so this repo can evolve independently from each user's assistant state.
- Hermes Agent gateway: self-hosted runtime with dashboard and OpenAI-compatible API.
- Telegram channel: allowlisted Telegram DM access with a single runtime owner.
- Independent agent state: private git-backed Hermes state tree for context files, cron jobs, avatars, and user-owned skills.
- Two-scope skills model: repo-owned platform skills in
skills-factory/, user-owned skills inagent-state/skills/. - Obsidian vault infrastructure: dedicated Docker volume synchronized with Syncthing over a Tailscale sidecar.
- TaskNotes lifecycle MCP: bounded create/get/list/update/complete/archive/delete/tag tools backed by native gbrain, with fail-closed profile and Git transaction guards.
- Google Drive vault backups: daily rotating backup slots via rclone.
- Optional Mnemosyne conversation memory: opt-in semantic recall/capture layer separate from the curated Obsidian vault, backed by an internal TEI embeddings service (no host port).
- Optional Mnemosyne encrypted backup: separate rclone
cryptbackup lane to Google Drive slots for the Mnemosyne memory store; local staging is not encrypted, encryption begins at the rclonecryptremote, and full recovery is operator-controlled. - Optional auxiliary ML service: internal
aux-mlcontainer for FIFO, one-at-a-time long-running OCR jobs through llama.cpp. - Multi-provider LLM config: Ollama Cloud, Z.AI/GLM, DeepSeek, and other OpenAI-compatible providers can be configured.
- Security checks: gitleaks and a custom PII guard in CI and optional pre-commit hooks.
Domain-specific behavior, such as Brazilian credit-card invoice extraction, belongs in a user's private state-repo skills unless it is explicitly added to skills-factory/. The public repo currently ships the infrastructure needed to support OCR and custom skills, not that personal extraction workflow itself.
flowchart LR
User[User] --> Telegram[Telegram Bot]
User --> Dashboard[Hermes Dashboard]
Telegram --> Gateway[Hermes Gateway]
Dashboard --> Gateway
Gateway --> Agent[Josemar Agent]
Agent --> Models[LLM Providers<br/>Ollama Cloud / Z.AI / DeepSeek]
Agent --> CoreSkills[Repo Core Skills<br/>/opt/josemar/skills]
Agent --> StateSkills[User State Skills<br/>/opt/data/skills]
Agent --> GBrain[Native gbrain CLI]
Agent --> TaskNotes[Bounded TaskNotes MCP]
TaskNotes --> GBrain
GBrain --> Vault[Obsidian Vault<br/>obsidian-vault volume<br/>canonical curated vault]
CoreSkills --> AuxML[aux-ml API<br/>optional]
AuxML --> Llama[llama.cpp Router<br/>OCR models]
Agent --> StateTree[Hermes State Tree<br/>hermes-data volume / /opt/data]
StateTree <--> StateRepo[Private Agent State Repo]
Vault <--> Syncthing[Syncthing]
Syncthing <--> Tailscale[Tailscale Sidecar]
Vault --> Backup[rclone Backup]
Backup --> GDrive[Google Drive Slots]
%% Opt-in Mnemosyne layer (separate from the curated vault)
Agent -. opt-in .-> Mnemosyne[Mnemosyne<br/>semantic conversation memory]
Mnemosyne -. embeddings .-> TEI[TEI Embeddings<br/>internal only / no host port]
Mnemosyne --> MnemoStore[(mnemosyne.db<br/>/opt/data/mnemosyne/data)]
MnemoStore -. opt-in backup .-> MnemoBackup[rclone crypt uploader<br/>separate service]
MnemoBackup --> MnemoGDrive[Google Drive<br/>mnemosyne-crypt slots]
The canonical curated vault path is gbrain -> Obsidian vault. Mnemosyne is a separate, opt-in operational/conversation-memory layer; it is not a vault replacement and does not alter gbrain's keyword-only retrieval. The TEI embeddings service is internal-only (no host port published); it is only present when the embeddings overlay is applied. The Mnemosyne encrypted backup lane is a separate rclone crypt uploader service to its own Google Drive slots, distinct from the vault backup.
The main repository can stay public because user-specific assistant state is isolated in a private nested repo mounted at agent-state/.
flowchart TB
PublicRepo[Public Platform Repo] --> Image[Docker Image]
PublicRepo --> CoreSkills[skills-factory<br/>repo-owned skills]
PublicRepo --> Compose[docker-compose.yml]
PrivateRepo[Private Agent State Repo] --> Personality[SOUL.md / memories/USER.md / memories/MEMORY.md / AGENTS.md]
PrivateRepo --> UserSkills[skills/*]
PrivateRepo --> Cron[cron/jobs.json]
PrivateRepo --> Avatars[avatars/*]
Image --> Runtime[Hermes Runtime]
CoreSkills --> Runtime
Compose --> Runtime
PrivateRepo <--> StateTree[Runtime /opt/data Git Repo]
StateTree --> Runtime
The state sync script only versions paths listed in .sync-manifest, uses the remote state repo as the blessed conflict winner, and can auto-commit/push state changes from the running assistant.
flowchart LR
Hermes[Hermes Container<br/>/opt/data/obsidian] <--> GBrain[Native gbrain CLI]
GBrain <--> Vault[(obsidian-vault volume)]
Vault <--> Syncthing[Syncthing Container]
Syncthing <--> Tailscale[Tailscale Sidecar<br/>private network]
Tailscale <--> Devices[Laptop / Mobile Devices]
Vault --> Backup[obsidian-backup Container]
Backup --> RcloneConfig[(obsidian-rclone-config)]
Backup --> SlotState[(obsidian-backup-state)]
Backup --> Drive[Google Drive<br/>slot-1 ... slot-N]
The vault persists in its own Docker volume, syncs through Syncthing, and is backed up by rotating rclone snapshots. Native gbrain sync already uses local-only Git history; the TaskNotes MCP reuses it for safe automatic commits. The repository has no remote consumer, its .git/ directory must be excluded from Syncthing, and it is separate from agent-state versioning.
Mnemosyne is an opt-in semantic conversation-memory layer, separate from the curated Obsidian vault. It is not enabled by default and is selected by the MNEMOSYNE_DEPLOY_MODE repo variable (see the operator table below). When the pilot is enabled, the static MEMORY.md/USER.md files remain at their versioned paths as archived rollback material and are not injected into prompts.
flowchart LR
Turn[Primary user turn] -->|passive capture<br/>sync_turn| Mnemo[Mnemosyne store<br/>/opt/data/mnemosyne/data]
Mnemo -->|pre-turn recall| Agent[Hermes Agent]
Mnemo -. embeddings .-> TEI[TEI Embeddings<br/>internal / no host port]
Mnemo -. opt-in backup .-> Crypt[rclone crypt uploader<br/>separate service]
Crypt --> Drive[Google Drive<br/>mnemosyne-crypt slots]
Vault[Obsidian Vault<br/>canonical curated vault] -. keyword-only retrieval<br/>unchanged .-> Agent
- Mnemosyne: semantic conversational recall and capture, opt-in. Passive user-turn capture (
sync_turn) and pre-turn recall are wired; non-conversation contexts (cron, flush, subagent, background, skill_loop) are excluded. No auto-sleep, reflection, or LLM consolidation runs in the pilot. Retrieval quality is evaluated separately (seedocs/mnemosyne-retrieval-quality.md); this README makes no precise ranking claims. - gbrain / Obsidian vault: the canonical curated vault. Retrieval is keyword-only today; the Mnemosyne pilot does not enable gbrain embeddings or alter the gbrain wrapper's behavior.
- Static
MEMORY.md/USER.md: archived rollback material. While the Mnemosyne pilot is enabled they are not injected into prompts; disabling the pilot restores them via the init rollback path.
See docs/mnemosyne-operations.md for activation, recovery, and security details, and docs/mnemosyne-retrieval-quality.md for the retrieval quality gate.
| Value | Overlays applied (fixed order) | Prerequisites |
|---|---|---|
off (default / unset) |
base only | none |
pilot |
base + embeddings + mnemosyne | none beyond the base deploy secrets |
backup |
base + embeddings + mnemosyne + mnemosyne-backup | positive MNEMOSYNE_BACKUP_EXPORT_INTERVAL (integer, no leading zeros, <= 10080 minutes) and RCLONE_CONFIG_B64 (base64 rclone config containing a crypt remote named mnemosyne-crypt plus the baseline gdrive remote) |
There is no embeddings-only mode: pilot is the smallest Mnemosyne-enabled mode. Any value other than off, pilot, or backup is rejected before any volume mutation or service teardown. The deploy workflow runs all preflight validation before any docker volume create, volume write, or service teardown. See docs/mnemosyne-operations.md for the full validation order, fail-closed teardown, mode-specific post-start verification, and the operator-controlled recovery lane.
For the encrypted backup, local staging is not encrypted (it holds a compressed plaintext SQLite snapshot); encryption begins at the rclone crypt remote; full recovery is operator-controlled and never automated. See docs/mnemosyne-operations.md for the recovery drill and the secret-recovery requirement.
git clone <this-repo-url> josemar-assistente
cd josemar-assistente
cp .env.example .envClone your private state repo into agent-state/:
git clone <your-private-agent-state-repo-url> agent-stateIf you do not have a state repo yet, initialize from the template:
cp -r templates/agent-state-template/ agent-state
cd agent-state
git init
git add -A
git commit -m "Initial state"
cd ..Set the required runtime variables:
TELEGRAM_BOT_TOKEN=your-telegram-token
PRIMARY_TELEGRAM_ID=123456789
WORKSPACE_STATE_REPO=https://github.com/username/private-agent-state.git
WORKSPACE_REPO_TOKEN=your-github-pat
HERMES_DASHBOARD_SESSION_TOKEN=<openssl rand -hex 32>
HERMES_DASHBOARD_BASIC_AUTH_USERNAME=admin
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=<openssl rand -hex 24>
HERMES_DASHBOARD_BASIC_AUTH_SECRET=<openssl rand -hex 32>Use a username other than admin if this dashboard is reachable through any
remote access path.
Set provider keys used by your configured model strategy:
DEEPSEEK_API_KEY=your-deepseek-key
ZAI_API_KEY=your-zai-key
OLLAMA_API_KEY=your-ollama-cloud-keyOptionally enable web search and extract by setting a Tavily key (auto-detected by Hermes when present):
TAVILY_API_KEY=your-tavily-api-keySee .env.example for the full variable list.
docker compose build
docker compose up -d
docker compose logs -f hermesAccess:
- Dashboard:
http://localhost:9119with Basic Auth - API server (if enabled):
http://127.0.0.1:8642
The dashboard host port binds to 127.0.0.1 by default. If publishing it through
Cloudflare Tunnel, keep the tunnel origin pointed at http://localhost:9119.
Cloudflare Access can be added as defense-in-depth after verifying Hermes
Desktop compatibility with the extra access layer.
If the Cloudflare tunnel runs on a different host and must reach the Docker VM
over the LAN, set HERMES_DASHBOARD_BIND_IP to the VM LAN address instead of
127.0.0.1. Do not use 0.0.0.0.
Enable auxiliary ML only when needed:
# In .env
AUX_ML_ENABLED=true
COMPOSE_PROFILES=aux-ml
docker compose up -d --buildjosemar-assistente/
├── agent-state/ # Nested private git repo for assistant state
├── aux-ml/ # Optional FastAPI + llama.cpp queue service
├── browser-tunnel/ # Optional hardened OpenSSH reverse-tunnel sidecar image
├── credentials/ # Local credentials, not versioned
├── docs/ # Operations runbooks
├── laptop/linux/ # Optional on-demand Linux laptop launcher (Mint-tested)
├── scripts/ # Workspace sync, backup, privacy tooling
├── skills-factory/ # Repo-owned core skills shipped in image
├── templates/agent-state-template/ # Starter private state repo template
├── tests/ # Python unit tests
├── .github/workflows/ # Deploy, stop, runner test, privacy scan
├── docker-compose.yml # Service topology and persistent volumes
├── docker-compose.browser-control.yml # Optional browser-control overlay
├── docker-compose.embeddings.yml # Optional local embedding service (TEI) overlay; NOT enabled by default. See docs/memory-embeddings-evaluation.md.
├── docker-compose.mnemosyne.yml # Optional Mnemosyne pilot overlay (semantic conversation memory); requires the embeddings overlay. See docs/mnemosyne-operations.md.
├── docker-compose.mnemosyne-backup.yml # Optional Mnemosyne encrypted-backup overlay (separate rclone crypt uploader); layered last. See docs/mnemosyne-operations.md.
├── Dockerfile.hermes # Custom Hermes image
└── .env.example # Environment template
| Service | Purpose |
|---|---|
hermes |
Main Hermes gateway, Telegram channel, dashboard/API, agent runtime. |
aux-ml |
Optional internal queue API for long-running OCR jobs. |
embeddings |
Optional local Hugging Face Text Embeddings Inference (TEI) service for semantic memory/gbrain embeddings. NOT enabled by default; only present when the docker-compose.embeddings.yml overlay is applied. See docs/memory-embeddings-evaluation.md for the issue #86/#65 evaluation. |
tailscale |
Private-network sidecar for Syncthing connectivity and (optionally) Tailscale Serve for browser control. |
syncthing |
Syncs the Obsidian vault to trusted devices. |
obsidian-backup |
Runs daily rclone backups into rotating Google Drive slots. |
mnemosyne-backup-uploader |
Optional separate rclone uploader for the Mnemosyne encrypted-backup lane. Reads the staging volume read-only, uploads through an rclone crypt remote to rotating Google Drive slots, writes only its own state volume. Only present when the docker-compose.mnemosyne-backup.yml overlay is applied (MNEMOSYNE_DEPLOY_MODE=backup). See docs/mnemosyne-operations.md. |
browser-tunnel |
Optional hardened OpenSSH reverse-tunnel sidecar for remote browser control. Only started under the browser-control Compose overlay/profile. See docs/browser-control.md. |
| Volume | Purpose |
|---|---|
hermes-data |
Hermes runtime state and the private state git worktree at /opt/data. Includes gbrain state at /opt/data/.gbrain (PGLite database, config, cache). Runtime-private files are ignored by the state repo. |
aux-ml-shared |
Dedicated handoff volume for files intentionally shared with aux-ml. |
obsidian-vault |
Obsidian notes and attachments plus local-only Git history required by native gbrain sync. The history has no remote consumer and .git/ is excluded from Syncthing. |
syncthing-config |
Syncthing identity and folder/device config. |
tailscale-state |
Tailscale node identity and login state. |
obsidian-rclone-config |
rclone config used by vault backup container. |
obsidian-backup-state |
Rotating backup slot pointer. |
mnemosyne-backup-staging |
Optional Mnemosyne backup staging volume. Exporter writes immutable generations here (read-write in hermes, read-only in the uploader). Only present with the docker-compose.mnemosyne-backup.yml overlay. Local staging is not encrypted. |
mnemosyne-backup-state |
Optional Mnemosyne uploader state (slot rotation, last-uploaded-generation). Writable only by the mnemosyne-backup-uploader service; mounted read-only into hermes. Only present with the docker-compose.mnemosyne-backup.yml overlay. |
mnemosyne-backup-recovery |
Optional disposable Mnemosyne recovery handoff volume. Written only by the short-lived mnemosyne-backup-recover step (recovery profile) and consumed transiently by short-lived docker compose run hermes invocations; never mounted into the long-running hermes service. Only present with the docker-compose.mnemosyne-backup.yml overlay. |
browser-tunnel-state |
Persistent Ed25519 SSH host key for the optional browser-tunnel sidecar so laptop known_hosts stays stable across redeploys. |
embedding-model-cache |
Public embedding model weights cache for the optional embeddings service. Contains only downloaded public model weights; no private data. Only present with the docker-compose.embeddings.yml overlay. |
Skills are intentionally split by ownership:
| Scope | Location | Owner | Use |
|---|---|---|---|
| Core platform skills | skills-factory/ copied to /opt/josemar/skills |
This repo | Stable runtime capabilities shared by all deployments. |
| User state skills | agent-state/skills/ synced to /opt/data/skills |
Private state repo | Personal workflows, user-specific automations, domain-specific processors. |
Hermes discovers both scopes through config/hermes-config.yaml (skills.external_dirs).
Runtime-created user skills should be written under /opt/data/skills/<skill>/ with a
SKILL.md; workspace-sync auto-registers those files in .sync-manifest so the
private state repo versions them on the next sync.
Josemar pins Hermes so that skill enable/disable toggles and the skill-creation
policy are backed by git-tracked state instead of the noisy, sensitive
/opt/data/config.yaml (which is deliberately untracked).
- Automatic skill patching/creation is disabled.
config/hermes-config.yamlsetsskills.creation_nudge_interval: 0(no creation nudges),skills.write_approval: true(skill writes require approval), andcurator.enabled: false(no background skill curator). The user-ownedcreating-skillsskill is retained so manual/user-approved creation remains possible. Memory nudge is untouched. - Native dashboard/CLI toggles survive redeploys. The Hermes dashboard
PUT /api/skills/toggleand thehermes skillsCLI flow through a Josemar helper (scripts/josemar_skill_state.py, copied into the image at/opt/hermes/hermes_cli/josemar_skill_state.py) that atomically writes a canonical JSON sidecar first and then invokes nativesave_configunder one advisory lock. A state write failure fails the dashboard/CLI save rather than silently diverging. - Per-profile sidecar paths. Only the dedicated toggle JSON is versioned,
never the full config:
- Default (base
HERMES_HOME) ->hermes/skill-toggles/default.json - Named profile
<canonical>->hermes/skill-toggles/profiles/<canonical>.json - Other
HERMES_HOMEpaths are rejected. Sidecar schema is exactly{"version":1,"disabled":[...],"platform_disabled":{"<platform>":[...]}}, one line, sorted/deduped string lists, explicit empty arrays retained, and arbitrary platform keys allowed.
- Default (base
- Persistence timing. A dashboard/CLI toggle writes the local sidecar
immediately; remote durability happens at the next periodic workspace sync
(no Git/network inside dashboard requests). The periodic
hermes-workspace-sync-cron.shdelegates to the helper'ssync-and-applyoperation so one advisory lock covers git sync, remote merge, and the sidecar/policy apply — dashboard writes and sync never race. - Remote-wins conflicts. Workspace sync uses remote-wins merge resolution, so a conflicting remote sidecar overwrites a local one on merge. This is intentional for a single-user state repo.
- Redeploy restoration. On startup,
docker-hermes-init.shmigrates existing toggle keys into absent sidecars (only when the keys exist and only for absent sidecars, so a pre-feature deployment's toggles survive the upgrade and an emptydefault.jsonis not created for a feature-less config), overwrites the runtime config from the repo template, runs workspace clone/sync/seed, and then applies the sidecars back to the default/named configs while enforcing the policy keys and preserving unrelated config. Malformed sidecars surface clearly and never modify config. - Session reset. Toggling a skill does not reset an already-built prompt
for the current session; the change takes effect on the next session/prompt
build. Run
hermes setupor start a new session to pick up the new toggle state immediately. - Why full config stays untracked.
config.yamlcontains secrets, host-specific paths, and Hermes schema defaults that change across versions. Tracking it would leak secrets and create noisy diffs. The narrow sidecars contain only toggle state, so they are safe to version and survive redeploys without dragging unrelated config along.
Josemar pins Hermes so the agent's model selections are backed by
git-tracked state instead of the sensitive, untracked /opt/data/config.yaml.
- Canonical state file.
agent-state/hermes/models.yamlis the single root-only configuration. There are no profiles or multiplexing in this PR. The template ships a matchingtemplates/agent-state-template/hermes/models.yaml. - Strict selection-only v1 contract. ONLY
provider/modelselection is allowed. The file carries exactly:model.{provider, default}— default model for primary agent turnsfallback_providers[].{provider, model}— ordered fallback listauxiliary.vision.{provider, model}— vision/OCR task model routingcron.{model, model_provider}— fleet cron defaults (blank = inherit default) Individual cron job overrides remain incron/jobs.json(per-jobmodel/providerfields) and are NOT duplicated here.
- Forbidden in this file.
base_url,api_mode,extra_body, timeouts, token limits,fallback_chain, credentials/secret keys, provider definitions, deployment topology, or any other Hermes config. Those stay inconfig.yaml/.envand are never versioned here. - Validation. State changes are validated before sync commit; invalid files (unknown keys, forbidden fields, or schema violations) are rejected and never reach the runtime config.
- Source of truth. This file is the source of truth for model selections.
Dashboard model changes are NOT source of truth — they live only in the
untracked runtime
config.yamland are overwritten on the next sync/restart. To change the model durably, editagent-state/hermes/models.yaml, commit, and let sync/restart apply it. - Persistence timing. State changes are applied at sync/start. The
workspace sync mirrors
hermes/models.yamlto/opt/data/hermes/models.yamland the container init applies it to the runtime config on startup. - Rollback. Delete or revert
hermes/models.yamlin the state repo, then sync/restart. The runtime config restores the repo model defaults fromconfig/hermes-config.yamlon the next start.
Current repo-shipped skills:
gbrain: native gbrain vault interface (search, get, capture, put, link, backlinks) used directly via the pinnedgbrainCLI. Keyword-only search, no embeddings. Operator activation viajosemar-gbrain reindex; periodic manual-edit reconciliation viajosemar-gbrain refreshevery 5 minutes by default.tasknotes: bounded durable-task lifecycle through thetask_*MCP tools. Native gbrain remains the backend and sole task writer. Seedocs/tasknotes-mcp.mdfor prerequisites and recovery.aux-ml: skill interface for queue-based auxiliary ML jobs.workspace-sync: skill interface for workspace git sync, status, commit, and push flows.
sequenceDiagram
participant Init as docker-hermes-init.sh
participant StateTree as Runtime /opt/data State Tree
participant Remote as Private State Repo
participant Hermes as Hermes
Init->>StateTree: Ensure state repo exists
Init->>StateTree: Run workspace-sync.sh
StateTree->>Remote: Pull/merge remote state
StateTree->>Remote: Push resulting state
Init->>Hermes: Ensure script-only workspace-sync cron job
Init->>Hermes: Start gateway
Important state-sync variables:
WORKSPACE_STATE_REPOWORKSPACE_REPO_TOKENWORKSPACE_GIT_BRANCHWORKSPACE_SYNC_ON_STARTWORKSPACE_SYNC_INTERVAL- Hermes script-only cron interval in minutes; set0to disable periodic sync.WORKSPACE_GIT_USER_EMAILWORKSPACE_GIT_USER_NAME
Run unit tests:
python3 -m unittest discover -s tests -vRun scoped contract tests:
python3 -m unittest tests.gbrain.test_gbrain_wrapper_contract -vSet up optional pre-commit hooks:
./scripts/setup-pre-commit.shManual privacy checks:
python3 scripts/pii_guard.py --staged --fail-on mediumCredentials go under credentials/<service>/ and are mounted read-only into Hermes. Do not commit real credentials.
AGENTS.md: root project architecture and assistant guidance.credentials/README.md: credential setup and storage rules.docs/aux-ml.md: auxiliary ML API, queue, model lifecycle, and OCR operations.docs/obsidian-operations.md: Syncthing, Tailscale, rclone backup, and restore runbook.docs/gbrain-operations.md: gbrain activation, reindex, vault swap, schema pack workflow, and troubleshooting.docs/memory-embeddings-evaluation.md: issue #86/#65 evaluation of the optional local embedding service and Mnemosyne memory layer (not enabled by default).docs/mnemosyne-operations.md: Mnemosyne pilot activation, encrypted-backup overlay, recovery lane, deploy-mode validation, and security boundaries.docs/mnemosyne-retrieval-quality.md: Mnemosyne Portuguese retrieval quality gate, activation thresholds, and the FaQuAD-IR benchmark.docs/tasknotes-mcp.md: TaskNotes profile gate, local Git/Syncthing prerequisites, tool outcomes, locking, and recovery.docs/browser-control.md: optional remote browser control via a reverse SSH tunnel over Tailscale..github/workflows/AGENTS.md: deployment, stop, privacy scan, and runner workflow documentation.templates/agent-state-template/README.md: starting point for a private state repo.
MIT