Hermes Team Deployment

A multi-user enterprise agent architecture — 25 team members, one gateway, per-user isolation, shared context, AWS Bedrock LLM, and Google Workspace identity.

Hermes Multiplex Google Chat AWS Bedrock Honcho GCP + Portainer Google Drive
25
Team Members
1
Gateway Process
4
Containers Total
~1.1GB
RAM Required
~$50/mo
GCP VM Cost

What This Architecture Delivers

One Bot, 25 Users — A single Google Chat bot serves the entire team. No per-user bot tokens, no per-user gateways. The multiplex gateway routes each message to the right user's profile.
Per-User Isolation — Alice's projects, memory, sessions, and secrets are invisible to Bob. Tirith filesystem authorization blocks cross-user access at the tool level.
Shared Team Context — Honcho memory provider creates a shared workspace for team-wide knowledge alongside per-user private memory. Users see both.
AWS Bedrock LLM — All 25 agents route through a single AWS Bedrock provider. IAM-based auth, standard AWS credential chain, no per-user API keys needed.
Google Drive Integration — Team files live on Google Drive. Per-user OAuth tokens give each agent access to their own Drive while a shared service account handles team-level documents.

Stack Overview

LayerTechnologyRole
Identity & InterfaceGoogle Chat (Google Workspace)Users DM the bot or interact in shared spaces. Google identity is the auth layer.
Agent RuntimeHermes Agent v0.18+ (multiplex mode)Single gateway process, 25 isolated profiles, Tirith filesystem authorization.
LLM InferenceAWS Bedrock (Claude, Nova, etc.)IAM-authenticated, no API keys. Models selected per-profile or globally.
MemoryHoncho (self-hosted)Shared workspace for team context + per-peer observations for private memory.
DatabasePostgreSQL + pgvectorHoncho backing store. Vector embeddings for semantic memory search.
File StorageGoogle DriveTeam documents stored in Drive. Per-user OAuth for personal files.
DeploymentGCP Compute Engine + PortainerSingle VM, Docker Compose stack, managed via Portainer web UI.

User Experience

Alice opens Google Chat, searches for "Hermes," starts a DM. She asks "summarize the Q3 engineering doc from my Drive." The agent searches her Google Drive via OAuth, finds the doc, and summarizes it. She then asks "what's the team deadline for Q3?" — the agent searches Honcho's shared workspace and responds with the date Bob entered last week. Alice never sees Bob's private Drive files. Bob never sees Alice's.

The architecture is invisible to users. They interact with a Google Chat bot. They get their own private agent with their own memory. They benefit from team knowledge without any setup. The 4 containers, the multiplex gateway, the Bedrock IAM auth — none of it is visible from Google Chat.

System Architecture

                            ┌─────────────────────────────────────────────┐
                            │               GOOGLE WORKSPACE               │
                            │  (alice@co.com, bob@co.com, ... x25)         │
                            │         ┌───────────────────┐                │
                            │         │  Google Chat Bot  │                │
                            │         │  "Hermes Team"    │                │
                            │         └────────┬──────────┘                │
                            └──────────────────┼───────────────────────────┘
                                               │ Cloud Pub/Sub
                                               ▼
  ┌─────────────────────────────────────────────────────────────────────────────────┐
  │                           GCP COMPUTE ENGINE (1 VM)                              │
  │                                                                                  │
  │  ┌──────────────────────────────────────────────────────────────────────────┐   │
  │  │                     PORTAINER (DOCKER MANAGEMENT)                         │   │
  │  │                      Port 9443 (web UI)                                   │   │
  │  └──────────────────────────────────────────────────────────────────────────┘   │
  │                                                                                  │
  │  ┌──────────────────────────────────────────────────────────────────────────┐   │
  │  │                      DOCKER COMPOSE STACK                                 │   │
  │  │                                                                           │   │
  │  │  ┌──────────────────────────────────────────────────────────────────┐    │   │
  │  │  │  CONTAINER 1: HERMES GATEWAY (multiplex mode)                    │    │   │
  │  │  │  ┌───────────────────────────────────────────────────────────┐  │    │   │
  │  │  │  │  GATEWAY PROCESS (1)                                        │  │    │   │
  │  │  │  │  ├── Google Chat adapter (Pub/Sub → events)                 │  │    │   │
  │  │  │  │  ├── Profile Router (email → profile mapping)              │  │    │   │
  │  │  │  │  └── Session Dispatch (per-profile agent loop)              │  │    │   │
  │  │  │  │                                                             │  │    │   │
  │  │  │  │  PROFILES:                                                  │  │    │   │
  │  │  │  │  ├── alice  → HERMES_HOME=/profiles/alice                  │  │    │   │
  │  │  │  │  │            Model: bedrock/us.anthropic.claude-sonnet-4    │  │    │   │
  │  │  │  │  │            Tirith: /workspaces/alice/**                  │  │    │   │
  │  │  │  │  │            Honcho peer: alice                            │  │    │   │
  │  │  │  │  │            Google OAuth: alice@co.com token              │  │    │   │
  │  │  │  │  │                                                          │  │    │   │
  │  │  │  │  ├── bob    → HERMES_HOME=/profiles/bob                    │  │    │   │
  │  │  │  │  ├── carol  → HERMES_HOME=/profiles/carol                  │  │    │   │
  │  │  │  │  └── ... x25                                                │  │    │   │
  │  │  │  └───────────────────────────────────────────────────────────┘  │    │   │
  │  │  └──────────────────────────────────────────────────────────────────┘    │   │
  │  │                                                                           │   │
  │  │  ┌────────────────────────┐  ┌──────────────────────┐  ┌──────────────┐  │   │
  │  │  │  CONTAINER 2: HONCHO   │  │  CONTAINER 3:        │  │  CONTAINER 4:│  │   │
  │  │  │  API (port 8000)       │  │  PostgreSQL + pgvector│  │  Redis Cache │  │   │
  │  │  └────────────────────────┘  └──────────────────────┘  └──────────────┘  │   │
  │  └──────────────────────────────────────────────────────────────────────────┘   │
  │                                                                                  │
  │  VOLUMES:                                                                       │
  │  ├── profiles/alice/  → Alice's HERMES_HOME (config, memory, skills, secrets)   │
  │  ├── profiles/bob/    → Bob's HERMES_HOME                                        │
  │  ├── profiles/...     → 23 more                                                  │
  │  ├── workspaces/      → Per-user project dirs (Tirith-scoped)                    │
  │  ├── shared/          → Team-wide read-only assets                               │
  │  └── honcho-db/       → PostgreSQL data volume                                    │
  └─────────────────────────────────────────────────────────────────────────────────┘
                                               │
                    ┌──────────────────────────┼──────────────────────────┐
                    │                          │                          │
                    ▼                          ▼                          ▼
  ┌─────────────────────────┐  ┌──────────────────────────┐  ┌──────────────────────────┐
  │     AWS BEDROCK         │  │      GOOGLE DRIVE        │  │    HONCHO WORKSPACE       │
  │  (Claude, Nova, Llama)   │  │  (Team docs + per-user)  │  │  (Shared team context)    │
  │  IAM auth via AWS SDK   │  │  OAuth2 per-user tokens   │  │  + Per-peer private mem   │
  └─────────────────────────┘  └──────────────────────────┘  └──────────────────────────┘

Request Flow (Alice sends a message)

1
Alice messages the bot in Google Chat. The message includes her identity — spaces/AAA1111, users/123456789, alice@co.com.
2
Google Chat API publishes the event to the Pub/Sub topic hermes-chat-events. No public endpoint needed — the subscription is pulled.
3
Hermes Google Chat adapter pulls the event from Pub/Sub. The adapter stamps source.profile via the email → profile mapping hook.
4
Multiplex gateway routes to Alice's profile. Session key becomes agent:alice:google_chat:... — isolated from every other user.
5
Profile runtime scope activates — Alice's config, secrets (AWS Bedrock IAM role), Tirith allowlist, Honcho peer, and Google Drive OAuth token are loaded.
6
LLM call — Alice's agent sends the prompt to AWS Bedrock via the Converse API. IAM credentials are resolved from the instance role or profile-level AWS config.
7
Tool execution — If the agent reads Alice's Drive, it uses her OAuth token. If it reads team context from Honcho, it searches the shared workspace (visible to all) plus its peer observations (visible only to Alice). Tirith blocks any file path outside Alice's workspace.
8
Response is sent back through the Google Chat REST API — posted in Alice's DM, in the same thread as her message.

Key Design Decisions

Multiplex mode over per-user containers — One gateway process for 25 users vs 25 containers. ~1.1GB RAM vs ~5GB. One set of credentials vs 25. The isolation cost is Tirith allowlists per profile, not a Docker process per user.
Honcho over OpenViking for memory — Honcho's native workspace + peer model maps directly to shared team context + private per-user memory. OpenViking has no tenant concept.
AWS Bedrock over per-user API keys — Bedrock's IAM auth means the VM instance role handles all credentials. No per-user API keys to issue or rotate. All 25 profiles share the same Bedrock allocation.
Google Chat over custom web portal — Google Workspace SSO is built in. Every user is authenticated by their Google identity. No custom auth infrastructure needed.

Data Flow, Permissions & Routing

This diagram tracks every data type through the system — where it enters, where it's stored, who can access it, and how it's authenticated at each hop.

  ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
  │                                                                                                  │
  │                            DATA FLOW MAP — where chats, docs, and memories go                    │
  │                                                                                                  │
  └──────────────────────────────────────────────────────────────────────────────────────────────────┘
                       │
        ┌──────────────┼──────────────┐                       ┌─────────────────────┐
        │              │              │                       │                     │
        ▼              ▼              ▼                       ▼                     ▼
  ┌──────────┐  ┌──────────┐  ┌──────────┐            ┌──────────────┐   ┌──────────────┐
  │  CHATS   │  │   DOCS   │  │ MEMORIES │            │  AWS BEDROCK  │   │ GOOGLE DRIVE │
  │          │  │          │  │          │            │  (LLM infra)  │   │ (file store)  │
  └─────┬────┘  └─────┬────┘  └─────┬────┘            └──────┬───────┘   └──────┬───────┘
        │              │              │                       │                  │
        ▼              │              │                       │                  │
  ┌─────────────┐      │              │          ┌────────────┴────┐   ┌─────────┴────────┐
  │ Google Chat │      │              │          │  IAM role on   │   │  Per-user OAuth  │
  │ API → Pub/Sub      │              │          │  GCP VM        │   │  token per email │
  │ (SA auth)   │      │              │          └─────────────────┘   └──────────────────┘
  └──────┬──────┘      │              │
         │             │              │
         ▼             │              │
  ┌────────────────────────────────────────────────────────────────────────────┐
  │  HERMES GATEWAY (multiplex, single process)                                 │
  │                                                                             │
  │  1. Google Chat adapter pulls event from Pub/Sub                           │
  │     Auth: SA key JSON (mounted file)                                        │
  │                                                                             │
  │  2. Profile router: pre_gateway_dispatch hook                               │
  │     Maps user email → profile name                                          │
  │     Auth: Google Workspace identity (verified by Chat API)                   │
  │                                                                             │
  │  3. Session dispatch: creates isolated session per profile                  │
  │     Session key: agent:{profile}:google_chat:{dm|space}:{id}                │
  │     Auth: session key → namespaced DB                                       │
  └────────────────────────────────┬───────────────────────────────────────────┘
                                   │
          ┌────────────────────────┼────────────────────────┐
          │                        │                        │
          ▼                        ▼                        ▼
  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
  │  ALICE profile    │  │  BOB profile     │  │  23 more...      │
  │  HERMES_HOME:     │  │  HERMES_HOME:    │  │                  │
  │  /profiles/alice/ │  │  /profiles/bob/  │  │                  │
  └────────┬─────────┘  └────────┬─────────┘  └──────────────────┘
           │                      │
           │        ┌─────────────┼──────────────────────────────────────────┐
           │        │             │                                          │
           ▼        ▼             ▼                                          │
  ┌──────────────────────────────────────────────────────┐                    │
  │              AGENT LOOP (per profile)                 │                    │
  │                                                       │                   │
  │  ┌──────────────┐    ┌──────────────┐                 │                   │
  │  │ CHAT FLOW    │    │ FILE FLOW    │                 │                   │
  │  │              │    │              │                 │                   │
  │  │ User msg →   │    │ User asks    │                 │                   │
  │  │ SOUL.md →    │    │ about doc →  │                 │                   │
  │  │ model →      │    │ Drive API →  │                 │                   │
  │  │ tool exec →  │    │ OAuth token  │                 │                   │
  │  │ response     │    │ → file data  │                 │                   │
  │  └──────┬───────┘    └──────┬───────┘                 │                   │
  │         │                   │                          │                   │
  │         ▼                   ▼                          │                   │
  │  ┌──────────────────────────────────┐                  │                   │
  │  │ MEMORY FLOW                      │                  │                   │
  │  │                                  │                  │                   │
  │  │ Agent reasons → Honcho API       │                  │                   │
  │  │ → Workspace (team) or peer(user) │                  │                   │
  │  │ → pgvector semantic search      │                  │                   │
  │  │ → Auto-extract via Deriver       │                  │                   │
  │  └──────────────────────────────────┘                  │                   │
  │                                                       │                   │
  │  PERMISSION CHECKS (every tool call):                  │                   │
  │  ├── Tirith: file paths against profile allowlist      │                   │
  │  ├── Honcho: workspace vs peer isolation               │                   │
  │  ├── Drive: OAuth token → file-level access            │                   │
  │  └── Bedrock: IAM role → model invoke permissions      │                   │
  └──────────────────────────────────────────────────────┘                   │
                                                                             │
           ┌─────────────────────────────────────────────────────────────────┘
           ▼
  ┌──────────────────────────────────────────────────────────────────────┐
  │  HONCHO MEMORY (PostgreSQL + pgvector)                               │
  │                                                                       │
  │  ┌──────────────────────────────┐   ┌──────────────────────────────┐  │
  │  │  WORKSPACE: "team"           │   │  PEER: "alice" (private)     │  │
  │  │                              │   │                              │  │
  │  │  What goes in:               │   │  What goes in:               │  │
  │  │  • Team deadlines, status    │   │  • Alice's preferences       │  │
  │  │  • Shared space debates      │   │  • Alice's project notes    │  │
  │  │  • Decisions & reasoning     │   │  • Alice's Drive index       │  │
  │  │  • Cross-user knowledge      │   │  • Alice's session history   │  │
  │  │                              │   │                              │  │
  │  │  Who can read: ALL profiles  │   │  Who can read: Alice only    │  │
  │  │  Who can write: Deriver +    │   │  Who can write: Deriver +    │  │
  │  │    any profile agent         │   │    Alice's agent only        │  │
  │  └──────────────────────────────┘   └──────────────────────────────┘  │
  │                                                                       │
  │  DERIVER (background):                                                 │
  │  Reads every conversation → extracts facts → classifies as            │
  │  team (→ workspace) or personal (→ peer) → generates embeddings       │
  │  → runs dream consolidation (dedup, link, summarize)                  │
  │  Auth: internal Honcho API key                                        │
  └──────────────────────────────────────────────────────────────────────┘

Data Flow Summary by Type

Data Type
Path Through System
Auth at Each Hop
Permissions
💬 Chat messages
User → Google Chat → Pub/Sub (pull) → Hermes adapter → Profile router → Agent loop → Honcho extract → Chat REST reply
Google Workspace OAuth / SA key / session namespace
read write
📄 Google Drive docs
User Drive → OAuth token resolution → Drive API → agent context → Honcho index (metadata only) → response back to user
Per-user OAuth token (drive.file) or team SA (drive.readonly)
read create no delete
🧠 Team memory
Conversations → Deriver (background) → Honcho workspace peer → pgvector → semantic search → agent recall → all 25 profiles can query
Honcho API (internal Docker network, no external exposure)
read all write all
🔒 Private memory
User conversations → Deriver → Honcho per-peer observations → pgvector → agent recall → ONLY that user's profile can query
Honcho peer isolation (API enforces per-peer boundaries)
read own write own no cross
⚙️ Profile configs
config.yaml, .env, SOUL.md on disk → loaded per-profile at session start → Tirith-enforced at runtime
Filesystem permissions + Tirith allowlists
read own no cross
🤖 LLM inference
Agent prompt → AWS Bedrock Converse API → model response → agent reasoning → tool dispatch (may chain multiple Bedrock calls)
IAM role (VM instance identity, no static keys)
invoke
🗄️ Deployment config
docker-compose.yml, Caddyfile → git-tracked → portainer manages containers → gcloud deploys Cloud Run apps
Git history (audit trail) + deployer profile only
admin deployer

Permission Boundaries

  CAN ALICE ACCESS BOB'S...
  ┌──────────────────────────────┬───────┬───────┐
  │ Resource                      │ Alice │ Bob   │
  ├──────────────────────────────┼───────┼───────┤
  │ Bob's DMs with the bot        │  ❌   │  ✅   │  (session isolation)
  │ Bob's Drive files             │  ❌   │  ✅   │  (OAuth scope: drive.file)
  │ Bob's Honcho private peer     │  ❌   │  ✅   │  (Honcho peer isolation)
  │ Bob's profile configs         │  ❌   │  ✅   │  (Tirith path allowlist)
  │ Bob's workspace files         │  ❌   │  ✅   │  (Tirith path allowlist)
  │ Shared team memory            │  ✅   │  ✅   │  (Honcho workspace peer)
  │ Shared skills directory       │  ✅   │  ✅   │  (read-only volume mount)
  │ AWS Bedrock (LLM)             │  ✅   │  ✅   │  (shared IAM role)
  │ Google Chat bot connection    │  ✅   │  ✅   │  (shared SA key)
  │ Deployer config (Docker, Caddy)│  ❌   │  ❌   │  (deployer profile only)
  └──────────────────────────────┴───────┴───────┘

Data Boundaries & Privacy

A critical architectural property: what data leaves your infrastructure vs what stays entirely within your GCP VM. Every external API call should be justified and bounded.

✅ Self-Hosted — Never Leaves Your GCP VM
💬 Chat message content (conversations)
All user messages to the bot remain on the VM. Processed by Hermes gateway in-memory. Stored in Honcho PostgreSQL database on the local Docker volume. Never sent to any external API except the LLM inference call.
📄 Google Drive file contents
Files accessed via Drive API are fetched into the agent's context temporarily, then discarded. Only metadata (file name, ID, type) stored in Honcho for indexing. Full file content never persisted outside the VM's memory.
🧠 Team + private memory (Honcho DB)
All Honcho observations, workspace knowledge, and per-peer memories live in the local PostgreSQL container. Includes vector embeddings for semantic search. Backed up to GCP Cloud Storage (your bucket, your region). No third party ever reads this data.
⚙️ Profile configs, SOUL.md, secrets
YAML configs, credential files, .env, Tirith rules, skills, plugins — all on the local filesystem. Backed up to your GCP bucket. Encrypted in-transit and at-rest within GCP.
🗄️ Session state, gateway logs
Active session databases, gateway configuration, cron definitions — all on local Docker volumes. No external telemetry. No usage tracking.
🔐 OAuth tokens + API keys
Google Chat SA key, Google Drive OAuth tokens, AWS IAM instance role — all on the VM's filesystem or GCP instance metadata. The IAM role is never a static file; it's an instance identity that cannot be exfiltrated.
🔄 Externally Sent — Data Shared with Third Parties
🤖 LLM prompts (to AWS Bedrock)
What is sent: The assembled prompt — system instructions (SOUL.md), conversation history, retrieved Honcho context, tool call outputs, and the user's current message. This is the only data that leaves your infrastructure for inference.
AWS Bedrock data handling: Prompts are processed in AWS's chosen region (us-east-1 or us-west-2). AWS does NOT use your prompts or model outputs for training (Bedrock's enterprise T&Cs). Data is encrypted in transit (TLS 1.3) and at rest within Bedrock. AWS retains input/output for up to 30 days for abuse detection by default; you can opt out via bedrock:DisableDataInference IAM policy or request zero-retention via AWS Support.
What is NOT sent: Your files (raw Drive content remains on VM), your Honcho database (embeddings never shared), your profile secrets, your OAuth tokens (the IAM role authenticates the VM, not the prompt). Only the text that the agent assembled into the prompt leaves.
📤 Google Chat API calls
What is sent: Outbound messages posted to Google Chat DMs/spaces — the agent's response text. Google handles delivery through their infrastructure. Message content is visible to Google (Chat is a Google service).
Inbound: User messages come FROM Google Chat via Pub/Sub. Google sees the messages in transit (they're the chat platform). This is unavoidable — you chose Google Chat as the interface.
📁 Google Drive API calls
What is sent: API requests to read/write Drive files — file IDs, search queries, OAuth tokens. The file content requested is delivered back to the VM. Google sees the API calls (they're accessing Google's service). No file content is stored or forwarded beyond the VM.
☁️ Cloud Storage backup data
What is sent: Encrypted backups of Honcho DB dumps, profile configs, and state files. Stored in YOUR GCP bucket in YOUR chosen region. Google Cloud has access to the bucket (it's their infrastructure). Enable customer-managed encryption keys (CMEK) or client-side encryption to ensure Google cannot read backup contents.

Self-Hosted Data (Stays on VM)

Data TypeStorage LocationBackup TargetCan You Self-Host It?
All conversationsHoncho PostgreSQL (Docker volume)GCP Cloud Storage (your bucket)✅ Always was
User memory (observations)Honcho PostgreSQL (Docker volume)GCP Cloud Storage (your bucket)✅ Always was
Vector embeddingspgvector in PostgreSQLGCP Cloud Storage (your bucket)✅ Always was
Profile configsLocal filesystemGCP Cloud Storage (your bucket)✅ Always was
Google Drive file contentsAgent memory (ephemeral, per-request)Not stored (ephemeral)✅ Always was
Secrets & credentialsLocal filesystem / GCP instance metadataGCP Cloud Storage (your bucket)✅ Always was
Gateway logs & session stateDocker volumesGCP Cloud Storage (your bucket)✅ Always was
Dashboard plugins & scriptsLocal filesystemGCP Cloud Storage (your bucket) + git✅ Always was

Data Shared with External Providers

Data TypeWhere SentPurposeCan You Avoid It?
LLM prompts (assembled agent context)AWS BedrockModel inference — the agent needs an LLM to function✅ Self-host an LLM (vLLM/Ollama on GPU VM). Budget adds ~$1,000-2,000/mo for GPU.
Chat messages (inbound/outbound)Google Chat APIUser interface — the bot lives in Google Chat⚠️ Switch to a self-hosted chat platform (Matrix, Mattermost). Requires changing the adapter.
Drive API requestsGoogle Drive APIFile access — the bot reads/writes Drive docs⚠️ Self-host file storage (Seafile, Nextcloud). Requires changing the filesystem skill.
Encrypted backup objectsGCP Cloud StorageDisaster recovery✅ Use client-side encryption. Google cannot read your data if you encrypt before upload.
Core principle: the only data that leaves your VM is what the LLM needs to see for the current request. No raw files, no database dumps, no credential material is sent to Bedrock — only the assembled text prompt. Every other system component (memory, configs, sessions, logs) stays on your infrastructure. The Caddy proxy, Honcho API, and Redis are never exposed to the internet.

Opting Out of Bedrock Data Retention

By default, AWS retains Bedrock inputs and outputs for up to 30 days for abuse detection. To opt out entirely:

1
IAM policy: Attach a deny policy to the VM's instance role: Deny: bedrock:DisableDataInference — this disables inference data logging.
2
AWS Console: Go to Bedrock → Settings → Model evaluation data storage → select "Do not store." This applies at the account level.
3
Verify: After 30 days, request AWS Support to confirm zero retention. Your prompts are never used for model training regardless (Bedrock enterprise terms prohibit this).

Pros, Cons & Roadmap

Honest Assessment Known Limits Future Work

Every architectural decision has tradeoffs. This page lays out what the system does well, where it falls short, and what comes next. Use it to decide whether this architecture fits your team and to plan your investment in future improvements.

✅ Pros
One process serves 25 users
Multiplex gateway means a single Hermes process routes 25 profiles. No per-user containers, no 25x resource overhead. Total RAM: ~1.1GB for the entire stack.
Per-user isolation by default
Tirith filesystem allowlists, Honcho peer isolation, per-profile HERMES_HOME, namespaced session keys. Alice's data is invisible to Bob's agent at every layer.
Shared team knowledge compounds
Honcho's Deriver automatically extracts facts from every conversation. Team memory grows without anyone filing a document. Cross-user learning (Bob's deadline → Alice knows it) is automatic.
No external dependencies
Everything runs on GCP native services (Compute Engine, Cloud Storage, optionally Cloud SQL). No third-party auth providers, no external databases, no Cloudflare, no VPN.
Full data sovereignty
All configs, memory, sessions, and credentials live on your infrastructure or your Google account. Standard formats (YAML, JSON, PostgreSQL, SQLite). No proprietary lock-in.
Natural language interface
Non-technical users interact via Google Chat — the same app they already use. Deploy apps, manage configs, search documents, all via DM. No terminal, no UI training.
Cost-effective at scale
~$50-70/mo VM + ~$0.80/mo backups. For 25 users, that's ~$2-3/user/month. LLM costs are separate (Bedrock) and scale with usage, not users.
Agent-driven deployment
The deployer profile lets users deploy apps and plugins via natural language. The agent handles Docker, Caddy routes, git tracking, and Cloud Run. No Portainer login needed for the team.
Version-controlled configs
Git auto-commit with health checks. Every config change has an audit trail (who changed what, when). Rollback any file in under 2 minutes.
❌ Cons & Limitations
Per-user Drive OAuth not shipped
Issue #15602 — the Google Workspace skill uses a global token path frozen at import time. Per-user Drive tokens require a 20-line patch. This is the single biggest gap for multiplex teams.
Tirith does not intercept execute_code
Tirith guards terminal, read_file, write_file, and search_files. But execute_code runs arbitrary Python that can call os.open() directly, bypassing path allowlists. This is an acknowledged architectural gap, not a bug.
Single VM is a single point of failure
If the GCP VM goes down, everything goes down. Recovery from total VM loss takes ~45 minutes (Cloud Storage → new VM → restore). No HA, no failover without Cloud SQL + GKE migration.
Google Chat message size limit
4,000 characters per message. Long agent responses are auto-split into multiple messages. This works but can be noisy for complex outputs. No rich formatting beyond inline images.
LLM latency for long responses
Claude Haiku is fast (~1-2s), but complex reasoning chains (multi-tool, multi-call) take 5-15 seconds. Users see "Hermes is thinking…" during this time. Not suitable for real-time conversational flow.
Pub/Sub = GCP dependency for bot operation
The Google Chat bot relies on Cloud Pub/Sub. If you want to migrate off GCP, you would need to replace the Pub/Sub subscription with a webhook endpoint or alternative messaging backend.
No native SSO for deployed apps
Apps deployed via the deployer agent (Cloud Run or Docker) don't automatically inherit Google Chat's auth. Cloud Run apps need IAP configured per-service. Docker apps need Caddy basic auth or an OAuth2 proxy.
Learning curve for Hermes config
Profile configs, Tirith rules, Honcho peers, multiplex routing, cron setup — the admin needs to understand 6-8 config layers. Not turnkey. The Installation Quick Start and templates reduce this but don't eliminate it.
No per-user model selection
All profiles use the same Bedrock model (or choose from a shared pool). There is no per-user model routing, per-user cost tracking, or per-user rate limiting without custom work.

Roadmap & Future Work

Items ordered by impact. The top three are recommended before scaling beyond 25 users:

BUILD Per-user Google Drive OAuth token resolution ~20 lines, ~30 min
Patch google_api.py to resolve tokens per-profile at call time. Same pattern as PR #59339 (Anthropic OAuth). Without this, per-user Drive access doesn't work in multiplex mode. This is the highest-impact fix available today.
CONFIG Set GOOGLE_CHAT_MAX_MESSAGES=10 1 line, ~1 min
Prevents the adapter from serializing all users behind a single message flow control. Different-profile messages run concurrently. Only within-session messages are ordered.
BUILD Deployer lockfile ~50 lines, ~1 hr
Prevents concurrent deploy operations from corrupting shared state. Python implementation with stale-lock detection, timeout, and user attribution. See Race Conditions page for full code.
CONFIG Git auto-commit with health check ~30 lines, ~30 min
Auto-commit script with gateway health check and error log inspection. Prevents half-written config states from entering git history. See Checkpoints page for full code.
UPSTREAM Watch Hermes v0.19+ release Monitoring
May ship per-user OAuth fix (#15602), improved multiplex diagnostics, and additional profile-level isolation features. Test in staging before upgrading production.
EVALUATE Migrate Honcho DB to Cloud SQL ~2 hrs
Replaces container PostgreSQL with managed Cloud SQL. Automatic backups, point-in-time recovery (7 days), cross-zone HA. Recommended when accumulated team knowledge makes even 6 hours of potential data loss unacceptable. ~$15-25/mo additional cost.
EVALUATE IAP for deployed Cloud Run apps ~1 hr per app
Adds Google Workspace SSO to apps deployed via the deployer agent. No passwords, same identity as Chat. Requires the deployer's SA to have IAP Admin role. Currently manual per-app; could be automated in the deployer skill.
EVALUATE GKE migration for HA beyond 50 users ~1 week
If the team grows beyond 50 users or requires 99.9%+ uptime, migrate from single VM to GKE Autopilot. The architecture doesn't change — just the orchestration layer. Honcho DB on Cloud SQL is the prerequisite.
UPSTREAM Hermes multiplex dashboard improvements Monitoring
The dashboard already reports gateway topology (profile list, status). Future releases may add per-profile metrics, session inspector, and credential status. Track the #comp/dashboard label on the Hermes repo.
CONFIG Per-user Bedrock cost tracking ~30 min
Tag Bedrock API calls with HERMES_PROFILE via AWS request tagging. Enables per-user cost breakdown in AWS Cost Explorer. Currently all profiles share a single IAM role — costs are aggregate.

Decision Framework

If you need...This system is...Consider instead...
25 users, per-user isolation, team memory✅ Excellent fit
Zero-config, turnkey deploy⚠️ Admin needs to understand 6-8 config layersDust.tt or other managed agent platforms
99.9%+ uptime, HA failover⚠️ Requires Cloud SQL + GKE migrationStart on single VM, migrate as needed
Per-user Google Drive access⚠️ Requires 20-line patch (#15602)Use team SA with drive.readonly for now
Non-technical users deploying apps✅ Agent-driven deployer profile
Enterprise compliance / SOC 2⚠️ Possible with Cloud Logging + IAP + auditNeeds custom compliance configuration
50+ users⚠️ VM may need resizing. Evaluate GKE.Start on single VM, plan GKE at 50+
Real-time conversational speed⚠️ 5-15s LLM latency for complex tasksUse a flash model (Haiku) for simple queries

Hermes Multiplex Gateway

Core Runtime v0.18+ Single Process 25 Profiles

The Hermes multiplex gateway is the architectural centerpiece. It runs as a single process that serves all 25 profiles, each with isolated HERMES_HOME, credentials, memory, and session state.

How Multiplex Works

  ONE GATEWAY PROCESS
  ┌─────────────────────────────────────────────────────────────────────┐
  │                                                                     │
  │  GOOGLE CHAT ADAPTER (1 connection, 1 bot token)                    │
  │  Cloud Pub/Sub pull → inbound events                                │
  │                                                                     │
  │         │                                                           │
  │         ▼                                                           │
  │  PROFILE ROUTER                                                     │
  │  ┌──────────────────────────────────────────────────────────────┐   │
  │  │  pre_gateway_dispatch hook:                                   │   │
  │  │  event.source.email → EMAIL_TO_PROFILE[alice@co.com] = alice  │   │
  │  │  stamps source.profile = "alice"                              │   │
  │  └──────────────────────────────────────────────────────────────┘   │
  │         │                                                           │
  │         ▼                                                           │
  │  SESSION DISPATCH                                                   │
  │  ┌──────────────────────────────────────────────────────────────┐   │
  │  │  Session key: agent:alice:google_chat:dm:users/123456789     │   │
  │  │  Isolated from: agent:bob:google_chat:dm:users/987654321     │   │
  │  │  Isolated from: agent:carol:google_chat:dm:users/555666777   │   │
  │  └──────────────────────────────────────────────────────────────┘   │
  │         │                                                           │
  │         ▼                                                           │
  │  PER-PROFILE CONTEXT LOADER                                         │
  │  ┌──────────────────────────────────────────────────────────────┐   │
  │  │  source.profile = "alice" →                                   │   │
  │  │  HERMES_HOME = /home/hermes/profiles/alice/                   │   │
  │  │  Loads: config.yaml, MEMORY.md, USER.md, skills/*,            │   │
  │  │         Honcho peer context, AWS credentials (per-profile)    │   │
  │  └──────────────────────────────────────────────────────────────┘   │
  │         │                                                           │
  │         ▼                                                           │
  │  AGENT LOOP (runs in profile context)                               │
  │  ├── Tirith checks every file tool call against profile allowlist   │
  │  ├── Honcho API call uses profile's peer for memory read/write      │
  │  ├── Bedrock API call uses profile's (or shared) AWS credentials    │
  │  └── Google Drive API call uses profile's OAuth token               │
  │                                                                     │
  └─────────────────────────────────────────────────────────────────────┘

Configuration

# docker-compose.yml (Hermes container)
services:
  hermes:
    image: nousresearch/hermes-agent:latest
    environment:
      - PROFILE=default
      - GATEWAY_MULTIPLEX_PROFILES=true          # Enable multiplex mode
      - AWS_PROFILE=hermes                       # Bedrock IAM profile
    volumes:
      - ./profiles:/home/hermes/profiles         # 25 profile dirs
      - ./workspaces:/home/hermes/workspaces     # Per-user project dirs
      - ./shared:/home/hermes/shared:ro          # Team-wide assets
    ports:
      - 9292:9292
# profiles/alice/config.yaml (per-profile)
model:
  provider: bedrock
  default: us.anthropic.claude-sonnet-4-6

terminal:
  backend: local
  cwd: /home/hermes/workspaces/alice
  tirith:
    fail_open: false
    allowed_paths:
      - /home/hermes/workspaces/alice/**
      - /home/hermes/shared/**

memory:
  provider: honcho

skills:
  external_dirs:
    - /home/hermes/shared/skills/      # Team skills (read-only)

Profile Routing Hook

# plugins/hooks/google_chat_user_mapper.py
"""Maps Google Chat user emails to Hermes profiles."""
import os

EMAIL_TO_PROFILE = {
    'alice@company.com': 'alice',
    'bob@company.com': 'bob',
    'carol@company.com': 'carol',
    # ... 25 entries
}

def pre_gateway_dispatch(event):
    if event.platform != 'google_chat':
        return event
    email = getattr(event.source, 'email', None)
    if email and email in EMAIL_TO_PROFILE:
        event.source.profile = EMAIL_TO_PROFILE[email]
    return event

Tirith Isolation

Tirith enforces per-profile filesystem access at the tool level. When Alice's agent calls terminal, read_file, write_file, or search_files, Tirith checks the path against the profile's allowlist before executing. Any path outside /workspaces/alice/** or /shared/** is blocked.

⚠️
Limitation: Tirith blocks Hermes tool calls but does not intercept raw os.open() in Python scripts executed via execute_code. For complete kernel-level isolation, each profile would need its own container or user namespace. For a trusted team of 25, Tirith's tool-level gate is sufficient for 95%+ of cross-user access scenarios.

Google Chat Integration

Identity Layer No Public Endpoint Pub/Sub Pull

Google Chat is the user-facing interface. Each team member interacts with a single bot — "Hermes Team" — through Google Chat DMs or shared spaces. The integration uses Cloud Pub/Sub pull subscriptions, meaning the Hermes gateway does not need a public URL, tunnel, or TLS certificate.

Architecture

  USER (Alice)
  │
  ├── Google Chat DM: "@Hermes Team summarize my Drive"
  │
  ▼
  ┌──────────────────────────────────────────────────┐
  │  GOOGLE CHAT API                                 │
  │  Publishes event to Cloud Pub/Sub topic          │
  │  projects/hermes-chat/topics/hermes-events       │
  └──────────────────────────────────────────────────┘
  │
  ▼
  ┌──────────────────────────────────────────────────┐
  │  PUB/SUB PULL SUBSCRIPTION                       │
  │  projects/hermes-chat/subscriptions/hermes-sub   │
  │  Service Account: hermes-chat-sa@...             │
  │  Roles: Pub/Sub Subscriber + Viewer              │
  └──────────────────────────────────────────────────┘
  │
  ▼
  ┌──────────────────────────────────────────────────┐
  │  HERMES GOOGLE CHAT ADAPTER                      │
  │  (inside Hermes gateway container)                │
  │  Polls subscription every 100ms                  │
  │  Parses event → SessionSource                    │
  │  Routes to profile via email mapper              │
  └──────────────────────────────────────────────────┘

  OUTBOUND:
  Agent replies via Chat REST API
  POST https://chat.googleapis.com/v1/spaces/{space}/messages
  Uses same Service Account (acts as the bot)

Setup Summary

StepActionGCP Console
1Create GCP projectconsole.cloud.google.com
2Enable Google Chat API + Cloud Pub/Sub APIAPIs & Services → Library
3Create Service Account for the botIAM & Admin → Service Accounts
4Create Pub/Sub topic + pull subscriptionPub/Sub → Topics
5Grant Chat API publisher role on topicTopic IAM: chat-api-push@system.gserviceaccount.com
6Grant SA subscriber role on subscriptionSubscription IAM: your SA + Pub/Sub Subscriber
7Configure Chat app (point to Pub/Sub topic)APIs → Google Chat API → Configuration
8Restrict visibility to your WorkspaceChat API Configuration → Visibility
9Install bot via DM or spaceGoogle Chat → New Chat → Search "Hermes Team"
ℹ️
Outbound only needs the Service Account JSON — Hermes uses the SA to authorize Chat REST API calls for replies. The SA key file is mounted into the Hermes container and referenced via GOOGLE_CHAT_SERVICE_ACCOUNT_JSON.

Message Behavior

ScenarioBehavior
User DMs the botRouted to user's profile via email mapper. Private session.
User mentions bot in shared spaceRouted to user's profile via email mapper. Public reply in thread.
Bot needs to share with whole spaceAgent can post to any space it has access to, regardless of profile.
User replies in a threadHermes detects thread.name and replies in same thread. Separate session per thread.
Unauthorized user messages botRestricted by GOOGLE_CHAT_ALLOWED_USERS — message ignored.
⚠️
Privacy note for shared spaces: All space members can see the bot's replies. For questions involving personal data, the SOUL.md should instruct users to DM the bot rather than ask in a shared space.

Team Deployment with Multiplex

With GATEWAY_MULTIPLEX_PROFILES=true, a single Google Chat bot serves all 25 team members — each routed to their own profile with isolated memory, session, and credentials. From the user's perspective, it's just a bot in their chat; the routing is invisible.

How Users Interact

1
Alice opens Google Chat → New Chat → searches for "Hermes Team" → starts a DM. She says "summarize my Q3 draft." The bot responds with her data, her Drive files, her memory.
2
Bob opens Google Chat → same bot, different DM. He says "what's the Q3 deadline?" The bot checks shared team context (Honcho workspace) for the answer. Bob's private Drive files are invisible to Alice's agent.
3
Alice mentions the bot in #team-space — "What's my PTO balance?" The email mapper routes to Alice's profile. The reply is visible to everyone in the space. Alice learns to DM for personal questions.

What Users Experience

ActionUser SeesWhat Happens Backstage
First DM to bot"Hermes is thinking…" → replyGateway receives ADDED_TO_SPACE event. Email mapper resolves alice@co.com → profile "alice". New session created.
Second DM (next day)Bot remembers context from yesterdaySession key agent:alice:google_chat:dm:spaces/AAA... loads previous conversation via Honcho.
@mention in shared spaceReply in thread, same spaceEmail mapper routes to alice profile. Reply posted via Chat REST API with thread.name.
Say "delete that file""I cannot delete files — I have restricted access."SOUL.md rule + stripped delete tools + drive.file scope. Three layers block it.

Routing Flow (End to End)

  User sends message in Google Chat
         │
         ▼
  Cloud Pub/Sub → Google Chat adapter
         │
         ▼
  pre_gateway_dispatch hook:
  Checks event.source.email
  ┌──────────────────────────────────────┐
  │  EMAIL_TO_PROFILE = {                │
  │    'alice@co.com': 'alice',          │
  │    'bob@co.com': 'bob',             │
  │    'carol@co.com': 'carol'          │
  │  }                                    │
  │  event.source.profile = matched name  │
  └──────────────────────────────────────┘
         │
         ▼
  Profile scope activates:
  ├── HERMES_HOME = /home/hermes/profiles/alice/
  ├── Session: agent:alice:google_chat:dm:...
  ├── Memory: Honcho workspace + peer alice
  ├── Config: model, Tirith rules, SOUL.md
  └── (Future) Google Drive OAuth token resolved by email
         │
         ▼
  Agent processes → reply via Chat REST API
  POST https://chat.googleapis.com/v1/spaces/{space}/messages

SOUL.md Template for Team Bots

## Google Chat Team Etiquette
- You serve 25 team members. Each has their own private profile.
- In DMs: full agent behavior. You have access to the user's
  private memory, their Drive files, and shared team context.
- In shared spaces: your replies are visible to everyone.
  If a question involves personal data, tell the user:
  "This involves your personal data — could you DM me instead?"
- You CANNOT delete files in Google Drive. Refuse politely.
- You can create new documents in Drive when asked.
- You can share information from team context (Honcho workspace)
  publicly in shared spaces.
- When in doubt about privacy, ask the user to DM you.
🚫
BLOCKER for multiplex — requires a code patch. The multiplex gateway routes messages per-user, but the Google Workspace skill's OAuth token resolution is still single-account (issue #15602). The token path is frozen at module import time, not resolved per-profile at call time. This means only the last user to authorize Drive will have working access — everyone else's token is silently overwritten.
Fix before deploying to users: Patch google_api.py to use a lazy token resolver that reads from google_tokens/<email>.json at call time. Same 20-line pattern as PR #59339 (Anthropic OAuth, already merged). The PR description and diff at github.com/NousResearch/hermes-agent/pull/59339 show the exact architecture. Without this, per-user Drive access is broken in multiplex mode.

Honcho Memory Provider

Shared + Private Workspace Model Self-Hosted

Honcho is the most important component for team deployment. It's what turns 25 isolated agent profiles into a team that learns collectively. Without it, each user's bot starts fresh every session — no shared context, no cross-user learning, no team knowledge growth.

Why Honcho — Not File-Based Memory

Standard Hermes memory (MEMORY.md, USER.md) and static knowledge stores like Obsidian vaults or plain markdown files can't support a team of 25 users. Here's why Honcho is necessary:

CapabilityHonchoHermes MEMORY.mdObsidian VaultPlain Markdown
Per-user private memory✅ Isolated observation peers per profile✅ Isolated per-profile file❌ Single vault, no per-user scoping❌ No isolation
Shared team knowledge✅ User peer "team" visible to all profiles❌ Per-profile only — no sharing✅ Everyone can read the vault❌ No sharing
Automatic extraction✅ Deriver extracts facts from conversations automatically❌ Manual write only❌ Manual note creation only❌ Manual only
Semantic search (vector embeddings)✅ pgvector — find concepts, not keywords❌ Keyword search only⚠️ With plugins, but no service❌ grep only
Cross-user learning✅ Bob enters a deadline → Alice's agent knows it❌ Alice can't read Bob's file⚠️ If Bob manually writes a note
Session continuity✅ Honcho recalls past sessions, preferences, decisions⚠️ Basic file recall, no structure❌ No session recall
Background consolidation✅ Dream runs offline — summarizes, deduplicates, links facts
Scales to 25+ profiles✅ Built for multi-agent❌ Per-profile, no cross-talk

📈 The Team Learning Loop

Alice asks "what's the Q3 deadline?" → agent checks Honcho workspace → Bob had already entered it in his DM yesterday → Alice gets the answer. Bob never had to "share" anything. Honcho's Deriver extracted it from Bob's conversation automatically and stored it in the shared peer. The team's knowledge compounds organically — every conversation feeds the collective, every user benefits from every other user's learnings, without anyone filing a document.

Architecture — Workspace + Peers

  ┌─────────────────────────────────────────────────────────────────────┐
  │                      HONCHO (Container:8000)                        │
  │                                                                     │
  │  WORKSPACE: "hermes-team"                                           │
  │  ┌──────────────────────────────────────────────────────────────┐  │
  │  │  USER PEER: "team"                                           │  │
  │  │  ├── Repository of team-shared facts                          │  │
  │  │  ├── Session summaries from shared spaces                     │  │
  │  │  ├── Team decisions, project status, deadlines                │  │
  │  │  └── Visible to ALL profiles                                  │  │
  │  │                                                               │  │
  │  │  AI PEERS (one per profile):                                  │  │
  │  │  ├── "alice" → Alice's private observations                   │  │
  │  │  │             Her preferences, her project notes,            │  │
  │  │  │             her Google Drive file indices                  │  │
  │  │  ├── "bob"   → Bob's private observations                     │  │
  │  │  └── ... x25                                                  │  │
  │  └──────────────────────────────────────────────────────────────┘  │
  │                                                                     │
  │  DATABASE: PostgreSQL + pgvector                                    │
  │  └──────────────────────────────────────────────────────────────────┘  │
  │                                                                     │
  │  DERIVER (background worker)                                        │
  │  Extracts observations from messages, builds peer representations,  │
  │  generates session summaries, runs dream consolidation              │
  └─────────────────────────────────────────────────────────────────────┘

Configuration

# honcho.json (shared across all profiles via mount)
{
  "baseUrl": "http://honcho:8000",
  "hosts": {
    "hermes": {
      "enabled": true,
      "workspace": "hermes-team",
      "peerName": "team",
      "aiPeer": "hermes",              # Default AI peer
      "recallMode": "hybrid",          # Auto-inject + tools
      "writeFrequency": "async",       # Background writes
      "observationMode": "directional"
    }
  }
}

Each profile references the same honcho.json but Hermes auto-creates a per-profile AI peer (hermes.alice, hermes.bob, etc.) via the --clone pattern during profile creation.

What Gets Stored Where

ContentStored InVisible To
Team deadlines, project statusWorkspace (user peer: "team")All 25 profiles
Shared space conversationsWorkspace (user peer: "team")All 25 profiles
Alice's personal preferencesAI peer: "alice" (observations)Alice only
Alice's project notesAI peer: "alice" (observations)Alice only
Alice's Drive file indexAI peer: "alice" (observations)Alice only
Bob's preferencesAI peer: "bob" (observations)Bob only
Workspace is the shared brain. When Alice asks "what's the Q3 deadline?", the agent queries Honcho's user peer and gets the team-visible answer. When Bob updates a deadline, it's available to everyone immediately — no document to file, no meeting to attend, no wiki to update. The team's collective knowledge grows with every conversation.
This is not possible with file-based memory. MEMORY.md is isolated per-profile — Alice cannot access Bob's file at all. An Obsidian vault is shared but requires manual note-taking — Bob has to remember to write something down. Honcho's Deriver does the extraction automatically in the background, so the team's knowledge grows even when users don't consciously "share" anything.
The team learning curve. Week 1: Alice's agent knows what Alice knows. Bob's agent knows what Bob knows. Week 4: Alice's agent knows everything Alice + Bob + Carol + team chat have discussed. The compound effect means the system becomes more useful the more people use it — opposite of a static document that goes stale.

What This Means For Your Team

ScenarioWithout HonchoWith Honcho
Bob learns the Q3 deadline is Dec 15Bob's agent knows it. Alice's agent has no idea. Next week Alice asks and gets "I don't know."Deriver extracts "Q3 deadline: Dec 15" from Bob's conversation → stored in workspace peer. Alice asks → agent finds it instantly.
Carol discovers a bug in the deployment scriptCarol's agent notes it. Team finds out via email chain later (if Carol remembers to send it).Deriver extracts the fix → stored in workspace peer. Bob asks "anyone fixed the deploy bug?" → agent has the answer.
Team has a heated debate in the shared space about strategyNothing captured beyond the Chat thread (which scrolls away).Deriver summarizes the debate, stores key decisions and arguments in workspace peer. Two months later, Alice asks "why did we choose AWS?" → agent recalls the entire reasoning chain.
Alice joins mid-yearAlice's agent knows nothing about the team. Weeks of ramp-up.Alice's agent queries the workspace peer → has access to every team decision, project status, and lesson learned since day one. Instant context.
A decision made 6 months ago needs revisitSomeone has to find the old Chat thread or recall from memory.Agent searches Honcho workspace by concept. Finds the decision, the reasoning, the alternatives considered, and the outcome. All automatically captured at the time of discussion.

Self-Hosted Honcho Stack

services:
  honcho-db:
    image: pgvector/pgvector:pg17
    environment:
      POSTGRES_USER: honcho
      POSTGRES_PASSWORD: ${HONCHO_DB_PASSWORD}
      POSTGRES_DB: honcho
    volumes:
      - honcho-db:/var/lib/postgresql/data

  honcho-redis:
    image: redis:7-alpine
    volumes:
      - honcho-redis:/data

  honcho-api:
    build: https://github.com/plastic-labs/honcho.git
    depends_on: [honcho-db, honcho-redis]
    environment:
      DB_CONNECTION_URI: postgresql+psycopg://honcho:...
      CACHE_URL: redis://honcho-redis:6379/0
      AUTH_USE_AUTH: "false"
      LLM_OPENAI_API_KEY: ${OPENAI_API_KEY}
ℹ️
Honcho needs an LLM for the Deriver (background memory extraction). This can be the same Bedrock model used by Hermes, a dedicated smaller model, or a separate provider entirely. Configured via .env in the Honcho container.

AWS Bedrock LLM Provider

Inference IAM Auth Converse API

All 25 Hermes profiles route their LLM calls through a single AWS Bedrock provider. Hermes supports Bedrock natively via the Converse API — not the OpenAI-compatible endpoint. This gives you full access to IAM authentication, Guardrails, cross-region inference profiles, and all foundation models.

How Auth Works

1
GCP VM has an IAM role — The VM is launched with an AWS IAM role (via instance profile or environment variables AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY).
2
Hermes uses standard AWS SDK chainboto3 resolves credentials from environment variables, ~/.aws/credentials, or instance profile. No Bedrock-specific API key needed.
3
Per-profile or shared credentials — Each profile can use a different IAM role (for cost tracking) or share a single role. Configurable via AWS_PROFILE env var per profile or globally.

Configuration

# Hermes config.yaml (global or per-profile)
model:
  provider: bedrock
  default: us.anthropic.claude-3-5-haiku
  # See the full model catalog below for model IDs and pricing

Bedrock Model Catalog — Complete

Every model available on Bedrock as of July 2026, organized by provider. This is the full list from the Bedrock "Models at a glance" page. Prices are per 1M tokens, on-demand Standard tier, US regions.

🤖 Anthropic 13 models
ModelInput $/MOutput $/MNotes
Claude Sonnet 5$2.00*$10.00**Promo until Aug 31, then $3/$15. Frontier-tier.
Claude Mythos 5Requires provider_data_share for abuse detection
Claude Fable 5Requires data sharing with Anthropic. Avoid for privacy.
Claude Opus 4.8$6.00$30.00Most expensive. Deep analysis.
Claude Sonnet 4.6$3.00$15.00Strong general-purpose. Good for deployer profile.
Claude Haiku 4.5$0.80$4.00Haiku successor — faster, more capable
Claude 3.5 Haiku$0.25$1.25✅ Best default for 25 users. Fast, cheap, ZDR.
🌐 Amazon Nova 7 models
ModelInput $/MOutput $/MNotes
Nova PremierMost capable Amazon model
Nova Pro~$0.80~$3.20Good general purpose
Nova Sonic~$0.25~$1.00Fast + speech modality
Nova 2 Lite~$0.06~$0.24Upgrade to Nova Lite
Nova Lite$0.06$0.24✅ Cheapest capable model. Perfect for cron/background.
Nova Micro$0.04$0.08✅ Absolute cheapest. Simple extraction/classification.
🟦 OpenAI 6+ models
ModelInput $/MOutput $/MNotes
GPT-5.6 Sol$5.50$33.00Largest GPT. Opus competitor.
GPT-5.6 Terra$2.75$16.50Mid-tier. Sonnet competitor.
GPT-5.6 Luna$1.10$6.60⚠️ Closest Bedrock alternative to opencode-go:deepseek-v4-flash. Good price/performance.
GPT-5.5$5.50$33.00Previous gen. Expensive.
GPT-5.4$2.75$16.50⚠️ Strong reasoning. Good for deployer profile.
🔵 DeepSeek 3 models
ModelInput $/MOutput $/MNotes
DeepSeek V3.2$0.62$1.85⚠️ Same family as opencode-go:deepseek-v4-flash (your current main). ~90% of V4 capability. Best stopgap until V4 arrives on Bedrock.
DeepSeek V3.1$0.60$1.73Previous version. Slightly cheaper.
DeepSeek R1Reasoning model. Strong but slower.
🚫 Not on Bedrock: opencode-go:deepseek-v4-flash — your current main agent model. DeepSeek V4 released mid-July 2026. Not yet onboarded. Expected Q3-Q4 2026.
🟢 Google Gemma (Open-Weight) 6 models
ModelInput $/MOutput $/MNotes
Gemma 4 31B$0.14$0.40✅ Best price/performance on Bedrock. Excellent as budget default agent. Open-weight (Apache 2.0). ZDR supported.
Gemma 4 26B A4B$0.13$0.40✅ MoE variant. Faster. Good for high-volume.
Gemma 4 E2B$0.04$0.08✅ Ultracheap. On par with Nova Micro.
Gemma 3 27B$0.23$0.38Previous gen. Still capable.
🔶 Meta Llama (Open-Weight) 8+ models
ModelInput $/MOutput $/MNotes
Llama 4 Maverick 17BMoE, multimodal (images), strong agentic. Open-weight.
Llama 4 Scout 17BLightweight. Good for high-volume.
Llama 3.3 70BSolid general-purpose, larger.
Llama 3.1 405BHuge. Expensive but capable.
🟣 Mistral AI 8+ models
ModelInput $/MOutput $/MNotes
Devstral 2 123B$0.40$2.00Coding specialist. Good price.
Mistral Large 3Flagship Mistral. Multilingual.
Pixtral LargeVision model — image understanding
🟡 Other Providers (1-3 models each)
ProviderModelsNotes
🔴 Z.AI (Zhipu)GLM 5, GLM 4.7, GLM 4.7 FlashGLM 5: 1M context, $1/$3.20, strong reasoning. Good Sonnet alternative.
🟤 MiniMaxMiniMax M2.5, M2.1, M2M2.5: $0.30/$1.20, 1M context. Good budget option.
🟠 Moonshot AIKimi K2.5, Kimi K2 ThinkingAgentic/reasoning focused. 1M context.
🟠 QwenQwen3 235B, Qwen3 Coder 480B, Qwen3 VL, Qwen3 Coder NextCoder 480B: specialist coding. VL: vision-language. Coder Next: lightweight.
⚪ xAIGrok 4.3Available on Bedrock. Reasonably priced.
⚪ AI21 LabsJamba 1.5 Large/MiniLong context. Jamba 1.5 Mini for budget.
⚪ WriterPalmyra X4, X5Enterprise-focused.
⚪ NVIDIANemotron Nano 3 30B, Nemotron 3 Super 120BLarge context models.

Your Current Models vs Bedrock

Your team currently runs these models via the opencode-go provider (OpenRouter). Here is exactly how each maps to Bedrock:

Your Current ModelUsed ForOn Bedrock?Bedrock EquivalentPrice DiffGap
opencode-go:deepseek-v4-flash Primary agent — all user conversations, reasoning, tool calls ❌ Not yet DeepSeek V3.2 ($0.62/$1.85) — same architecture, older weights. Or Gemma 4 31B ($0.14/$0.40) — cheaper, different architecture. V3.2: similar pricing.
Gemma 4: 4-5x cheaper.
🟡 Medium — V3.2 is ~90% capability. Gemma 4 is strong but different. DeepSeek V4 expected on Bedrock Q3-Q4 2026.
opencode-go:mimo-v2.5 Dashboard plugins, multimodal tasks, document analysis ❌ Not available Claude 3.5 Haiku ($0.25/$1.25) — images only, no video. Llama 4 Maverick — images only. Qwen3 VL 235B — vision-language. Haiku: similar pricing. 🟡 Medium — mimo-v2.5's native video understanding has no Bedrock equivalent. For image tasks, Haiku covers it well.
Bottom line: Moving from opencode-go to Bedrock means swapping deepseek-v4-flash for DeepSeek V3.2 or Gemma 4 31B as your primary agent. You lose ~10% capability in complex reasoning but gain ZDR data governance, native IAM auth, unified billing, and zero infrastructure management. Your cost stays roughly the same or decreases. When DeepSeek V4 arrives on Bedrock, the gap closes completely.

Model Selection Guide by Use Case

Use CaseRecommended Bedrock ModelModel IDCost for 25 Users
Default agent (replaces opencode-go:deepseek-v4-flash)Gemma 4 31B (budget) or DeepSeek V3.2 (same family) or GPT-5.6 Luna (premium)google.gemma-4-31b / deepseek.v3-2 / openai.gpt-5.6-luna~$50-220/mo depending on tier
Fast Q&A, summaries, Drive readsClaude 3.5 Haikuanthropic.claude-3-5-haiku~$35-80/mo
Complex reasoning, strategy, document reviewClaude Sonnet 4 or GPT-5.4 or GLM 5anthropic.claude-sonnet-4 / openai.gpt-5.4 / zai.glm-5~$150-300/mo (used selectively)
Coding, deploy operationsDevstral 2 123B or Qwen3 Coder Nextmistral.devstral-2-123b / qwen.qwen3-coder-next~$40-80/mo
Image understanding (replaces opencode-go:mimo-v2.5)Claude 3.5 Haiku or Llama 4 Maverickanthropic.claude-3-5-haiku / meta.llama-4-maverickIncluded above
Background / cron / batchNova Liteamazon.nova-lite-v1.0~$1/mo
Ultracheap classificationNova Micro or Gemma 4 E2Bamazon.nova-micro-v1.0 / google.gemma-4-e2b~$0.50/mo

Cost Estimates for 25 Users (Updated)

Using the recommended model stack above with Haiku as default and selective upgrades:

Usage PatternMonthly CostPer User
Light (all Haiku or Gemma 4, 10 msgs/day)~$50-80~$2-3
Moderate (Haiku default, Sonnet for complex, 25 msgs/day)~$160-220~$6-9
Heavy (mixed Haiku/Sonnet/Opus, 50+ msgs/day)~$400-600~$16-24
All on GPT-5.6 Luna or DeepSeek V3.2~$250-350~$10-14
Bedrock Guardrails — AWS Bedrock Guardrails can be applied to all inference calls, providing content filtering, topic denial, and PII redaction across the entire team without any per-user configuration.

Security

With Bedrock's IAM auth, there are no API keys to issue or rotate. The VM's IAM role defines what models can be invoked. If the VM is compromised, the IAM role is the blast radius — not individual API keys. Bedrock also supports cross-region inference profiles for automatic failover and load balancing.

Google Drive Integration

File Storage Per-User OAuth Shared Service Account

Team files live in Google Drive. The architecture supports two access patterns: per-user OAuth tokens for private file access, and a shared service account for team-wide document retrieval.

  GOOGLE DRIVE ACCESS MODEL
  ┌─────────────────────────────────────────────────────────────────────┐
  │                                                                     │
  │  PER-USER ACCESS (via OAuth)                                        │
  │  ┌─────────────────────────────────────────────────────────────┐    │
  │  │  Alice's agent → Alice's OAuth token → Alice's Google Drive  │    │
  │  │  Bob's agent   → Bob's OAuth token   → Bob's Google Drive    │    │
  │  └─────────────────────────────────────────────────────────────┘    │
  │                                                                     │
  │  Access granted when:                                                │
  │  1. User runs /setup-google-drive in their DM                       │
  │  2. Browser OAuth consent flow completes                             │
  │  3. Token stored at ~/.hermes/google_tokens/alice@co.com.json       │
  │  4. Token auto-refreshes, never expires                             │
  │                                                                     │
  │  ──────────────────────────────────────────────────────────────     │
  │                                                                     │
  │  SHARED TEAM ACCESS (via Workspace Service Account)                 │
  │  ┌─────────────────────────────────────────────────────────────┐    │
  │  │  Any profile's agent → Drive SA token → Team Drive/shared    │    │
  │  │  docs folder                                                  │    │
  │  └─────────────────────────────────────────────────────────────┘    │
  │                                                                     │
  │  Service Account has domain-wide delegation authority:              │
  │  - Can access any doc in the team Drive                             │
  │  - Configured via Google Workspace Admin Console                    │
  │  - No per-user consent needed for team docs                         │
  │                                                                     │
  └─────────────────────────────────────────────────────────────────────┘

Setup Sequence

1
Create a Google Cloud project and enable the Drive API. Create OAuth 2.0 credentials (Desktop app type).
2
For team-wide access: Create a Service Account with domain-wide delegation. Configure the Workspace Admin Console to grant it access to the Drive scope (https://www.googleapis.com/auth/drive.readonly).
3
For per-user access: Each user runs /setup-google-drive in their DM. This generates an OAuth URL. The user approves in their browser, pastes back the redirect URL, and the token is stored.
4
Agent searches Drive — The agent calls the Hermes Google Workspace skill ($GAPI drive search ...) which resolves the correct OAuth token based on the active profile's user email.

Multi-Account Token Storage

The Google Workspace skill stores tokens at ~/.hermes/google_tokens/<email>.json. The multiplex profile router sets GOOGLE_CHAT_USER_EMAIL as an env var per-turn, which the Google API tooling reads to select the correct token. If no per-user token exists, it falls back to the team service account.

⚠️
Current limitation: Multi-account OAuth token support is GitHub issue #15602, labeled P3 (not yet merged). The single-account Google Workspace skill works today. Per-user token resolution requires a small custom extension to google_api.py to accept a --user flag that selects the right token file. This is approximately 30 lines of Python.

Drive Security: Preventing Accidental Deletion

Google OAuth scopes do not differentiate between create and delete — a token with drive.file or drive scope can delete any file it can access. Three layers of defense prevent accidental document loss:

Layer 1: OAuth Scope Selection

ScopeReadCreateEditDeleteBest For
drive.readonlyTeam service account (shared docs)
drive.file✅ (own files only)✅ (own files only)Per-user tokens (recommended)
drive (full)✅ (all)✅ (all)Avoid — never use

Recommended setup:

  • Team Drive (shared): Service Account with drive.readonly — agent can search and read team docs but cannot touch them
  • Per-user Drive: User OAuth with drive.file — agent can create and edit files it made, but cannot see or delete existing user files
# In Hermes Google Workspace setup or google_api.py
SCOPES = {
    "team": ["https://www.googleapis.com/auth/drive.readonly"],           # team SA
    "user": ["https://www.googleapis.com/auth/drive.file"]               # per-user OAuth
}

drive.file confines the agent to files it explicitly created or was granted access to. Your existing 5-year-old project spreadsheets are invisible to it. If the agent deletes a draft it made, Drive's 30-day trash provides recovery.

Layer 2: Tool-Level Deletion Stripping

Modify the Hermes Google Workspace skill to remove delete functionality entirely — the agent literally cannot call delete because the tool doesn't exist in its toolset:

# google_api.py — strip all destructive commands
COMMANDS = {
    'search': _drive_search,
    'get': _drive_get,
    # 'delete': _drive_delete,       ← REMOVED
    # 'trash': _drive_trash,         ← REMOVED
    # 'update': _drive_update,       ← REMOVED (optional — keeps edits)
    'create': _drive_create,         # keep if you want doc generation
}

Layer 3: Prompt Hardening

Each profile's SOUL.md includes a non-deletion directive as a behavioral guardrail:

## Google Drive Rules
- You have RESTRICTED access to Google Drive.
- You CANNOT delete, trash, or remove any file.
- You CAN create new files when explicitly asked.
- If a user asks you to delete a file, refuse politely.
- If a user asks you to "clean up" or "remove old files," do not interpret this as delete.
  Instead, state that you cannot perform destructive operations.

Defense in Depth Flow

  User: "delete that spreadsheet"
         │
         ▼
  SOUL.md: "Cannot delete files"           ← Prompt guardrail
         │
         ▼
  Agent looks for delete tool               ← Tool doesn't exist
         │
         ▼
  Google API checks OAuth scope             ← drive.file or readonly
         │
    ┌────┴────┐
    │         │
  readonly  drive.file
    │         │
  403       Checks: did this app CREATE the file?
    │         │
    │    ┌────┴────┐
    │    │         │
    │   Yes       No
    │    │         │
    │  Deletes   403 FORBIDDEN
    │  (trash,    │
    │  30d recover) File safe
    │         │
    └─────────┘
         │
  Even if deleted: 30-day Drive trash recovery
Belt + suspenders. Three independent layers — OAuth scope, tool availability, and prompt instruction — mean a file can only be deleted if all three fail simultaneously. And even then, Drive's 30-day trash provides a final recovery path. For most teams, drive.file + tool stripping is sufficient.

GCP + Portainer Deployment

Infrastructure Single VM Portainer Stack

The entire stack deploys on a single GCP Compute Engine VM, managed through Portainer's web UI. No Kubernetes, no Cloud Run — just Docker Compose on a VM, which is what you know.

Resource Sizing

ResourceRecommendedMinimum
Machine typee2-standard-4 (4 vCPU, 16GB)e2-standard-2 (2 vCPU, 8GB)
Disk100GB persistent SSD50GB persistent SSD
RAM usage~4.5GB (Hermes + Honcho + DB + Redis)~3GB
Monthly cost~$50-70/mo~$30-40/mo
  GCP COMPUTE ENGINE
  ┌─────────────────────────────────────────────────────────────────┐
  │  e2-standard-4 (4 vCPU, 16GB RAM, 100GB SSD)                   │
  │                                                                 │
  │  ┌──────────────────────────────────────────────────────────┐   │
  │  │  PORTAINER (port 9443)                                    │   │
  │  │  ├── Stack: "hermes-team"                                 │   │
  │  │  │   ├── hermes (image: nousresearch/hermes-agent)        │   │
  │  │  │   │   ports: 9292 (gateway)                            │   │
  │  │  │   │   env: GATEWAY_MULTIPLEX_PROFILES=true              │   │
  │  │  │   │   volumes: profiles/, workspaces/, shared/         │   │
  │  │  │   │                                                    │   │
  │  │  │   ├── honcho-api (build from GitHub)                   │   │
  │  │  │   │   ports: 8000                                       │   │
  │  │  │   │   env: DB_CONNECTION_URI, CACHE_URL, LLM key       │   │
  │  │  │   │                                                    │   │
  │  │  │   ├── honcho-db (image: pgvector/pgvector:pg17)        │   │
  │  │  │   │   volumes: honcho-db:/var/lib/postgresql/data      │   │
  │  │  │   │                                                    │   │
  │  │  │   └── honcho-redis (image: redis:7-alpine)             │   │
  │  │  │       volumes: honcho-redis:/data                      │   │
  │  │  └──────────────────────────────────────────────────────┘   │
  │  └──────────────────────────────────────────────────────────┘   │
  └─────────────────────────────────────────────────────────────────┘

Step-by-Step Deployment

1. Create the GCP VM

gcloud compute instances create hermes-vm \
  --zone=us-central1-a \
  --machine-type=e2-standard-4 \
  --image-family=ubuntu-2404-lts \
  --image-project=ubuntu-os-cloud \
  --boot-disk-size=100GB \
  --boot-disk-type=pd-ssd \
  --tags=portainer,hermes

2. Install Docker + Portainer (startup script)

#!/bin/bash
apt update && apt install -y docker.io docker-compose-v2
systemctl enable docker

docker volume create portainer_data
docker run -d --name portainer --restart always \
  -p 9443:9443 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v portainer_data:/data \
  portainer/portainer-ce:latest

3. Connect Portainer

Open https://<VM_EXTERNAL_IP>:9443. Set admin password. Connect to the local Docker environment. Go to Stacks → Add Stack, paste the docker-compose.yml, and deploy.

4. Firewall Rules

# Allow Portainer web UI (restrict to your IP)
gcloud compute firewall-rules create allow-portainer \
  --allow tcp:9443 \
  --source-ranges YOUR_HOME_IP/32

# Allow Hermes gateway admin API (if needed)
gcloud compute firewall-rules create allow-hermes-api \
  --allow tcp:9292 \
  --source-ranges YOUR_VPN_RANGE/24

docker-compose.yml

version: '3.8'
networks:
  hermes-net:

volumes:
  honcho-db:
  honcho-redis:

services:
  hermes:
    image: nousresearch/hermes-agent:latest
    environment:
      - PROFILE=default
      - GATEWAY_MULTIPLEX_PROFILES=true
      - AWS_PROFILE=hermes-team
    env_file:
      - ./hermes.env
    volumes:
      - ./profiles:/home/hermes/profiles
      - ./workspaces:/home/hermes/workspaces
      - ./shared:/home/hermes/shared:ro
      - ./honcho.json:/home/hermes/.hermes/honcho.json
      - ./google-chat-sa.json:/run/secrets/google-chat-sa.json
    ports:
      - "127.0.0.1:9292:9292"
    networks:
      - hermes-net
    restart: unless-stopped

  honcho-db:
    image: pgvector/pgvector:pg17
    environment:
      POSTGRES_USER: honcho
      POSTGRES_PASSWORD: ${HONCHO_DB_PASSWORD}
      POSTGRES_DB: honcho
    volumes:
      - honcho-db:/var/lib/postgresql/data
    networks:
      - hermes-net
    restart: unless-stopped

  honcho-redis:
    image: redis:7-alpine
    volumes:
      - honcho-redis:/data
    networks:
      - hermes-net
    restart: unless-stopped

  honcho-api:
    build: https://github.com/plastic-labs/honcho.git
    depends_on: [honcho-db, honcho-redis]
    environment:
      DB_CONNECTION_URI: postgresql+psycopg://honcho:${HONCHO_DB_PASSWORD}@honcho-db:5432/honcho
      CACHE_URL: redis://honcho-redis:6379/0
      AUTH_USE_AUTH: "false"
      LLM_OPENAI_API_KEY: ${HONCHO_LLM_KEY}
    networks:
      - hermes-net
    restart: unless-stopped

Directory Layout

/opt/hermes-deploy/
├── docker-compose.yml
├── hermes.env                         # Global env vars
├── honcho.json                        # Honcho config
├── google-chat-sa.json                # GCP SA key
├── profiles/
│   ├── alice/config.yaml
│   ├── alice/.env                     # Per-user secrets
│   ├── alice/SOUL.md
│   ├── alice/honcho.json
│   ├── bob/config.yaml
│   ├── bob/.env
│   ...
├── workspaces/
│   ├── alice/                         # Alice's project files
│   ├── bob/
│   ...
└── shared/
    ├── skills/                        # Team skills repo
    ├── docs/                          # Team documentation
    └── templates/

Backup Strategy

DataMethodFrequency
Honcho PostgreSQLdocker exec honcho-db pg_dumpDaily
Profile configsVolume backup (or git-tracked)On change
Google Chat SA keyStored separately (GCP Secret Manager)N/A
Portainer dataVolume backupWeekly

Security Model

Isolation Layers IAM Auth Tirith

The security model uses defense in depth — six layers between a user and another user's data.

Layer 1: Google Workspace Identity

Every user authenticates through Google Workspace. The Chat bot is configured with GOOGLE_CHAT_ALLOWED_USERS — only workspace members can message the bot. An attacker without a valid @company.com account cannot interact with the agent at all.

Layer 2: Profile Isolation

Each user's session runs in its own HERMES_HOME directory. Session keys are namespaced by profile — agent:alice:google_chat:dm:... vs agent:bob:google_chat:dm:.... Alice's conversation history is never loaded into Bob's session.

Layer 3: Tirith Filesystem Authorization

Every file tool call (terminal, read_file, write_file, search_files, patch) is intercepted by Tirith. Paths are checked against the profile's allowlist before execution.

# Alice CAN access:
/workspaces/alice/**
/shared/**
/home/hermes/profiles/alice/config.yaml

# Alice CANNOT access:
/workspaces/bob/**
/home/hermes/profiles/bob/MEMORY.md
/etc/shadow
fail_open: false is critical. When set to false, Tirith blocks any path not explicitly allowed. The default is true (allow all) — you must explicitly set it to false in production.

Layer 4: Honcho Peer Isolation

Honcho uses a workspace + peer model. The user peer (team) is shared across all profiles — this is intentional for team context. Each AI peer (alice, bob, etc.) has private observations that no other peer can query. Honcho's API does not expose cross-peer data.

Layer 5: AWS Bedrock IAM

The VM's IAM role defines the blast radius for LLM access. No API keys in config files. Bedrock Guardrails can be applied to all inference calls for content filtering and topic denial across the entire team.

Layer 6: Per-User Google OAuth Tokens

Each user's Google Drive OAuth token is scoped to their account. Token files are stored in the profile's HERMES_HOME with filesystem permissions. Alice's token cannot read Bob's Drive. Bob's token cannot read Alice's Drive.

Trust Model Summary

ThreatPrevented ByConfidence
Alice reads Bob's project filesTirith path allowlistHigh (tool-level gate)
Alice reads Bob's private memoryHoncho peer isolationHigh (API-level gate)
Alice steals Bob's AWS credentialsIAM role (not per-user keys)High (no keys to steal)
Alice reads Bob's Drive filesPer-user OAuth scopingHigh (Google auth)
Alice executes os.open() to bypass TirithNot prevented in execute_code⚠️ Medium gap
External attacker messages the botGoogle Chat allowed_usersHigh (Google auth)
⚠️
Acknowledged gap: execute_code runs arbitrary Python that can bypass Tirith via os.open(). For a trusted team of 25, this is acceptable — the user running the tool is already authenticated as themselves. If you need to prevent a compromised agent from reading another user's files at the kernel level, you need per-container or per-VM isolation, which defeats the multiplex architecture. This is a conscious tradeoff.

Race Conditions & Concurrency

Architecture Mitigations Queue Design

With 25 users sharing one gateway, concurrent operations are the rule, not the exception. This page documents every race condition in the system, its risk level, and the mitigation. The fundamental principle: messages should queue at the right layer — the Pub/Sub adapter drains immediately into an internal work queue rather than serializing unrelated users.

⚡ Concurrency Model

Each user's messages are serialized (within their session). Different users' messages are concurrent (no cross-blocking). Deploy operations are serialized via a lockfile. This gives the best of both worlds: natural conversation ordering per user, full parallelism across users, and exclusive access only where truly needed.

Architecture: Internal Work Queue

Contrary to the default GOOGLE_CHAT_MAX_MESSAGES=1 which serializes the entire adapter, the correct design uses a higher flow control limit and an internal dispatch queue:

  Pub/Sub subscription ──────────────────────────────────────────────┐
         │                                                            │
         │  GOOGLE_CHAT_MAX_MESSAGES=10 (drains quickly)               │
         ▼                                                            │
  ┌─────────────────────────────────────────────┐                     │
  │  Google Chat Adapter (single pull loop)      │                     │
  │  ├── Polls Pub/Sub every 100ms               │                     │
  │  ├── Receives up to 10 messages per poll     │                     │
  │  ├── For each: apply email mapper → set      │                     │
  │  │   source.profile → enqueue to dispatch    │                     │
  │  └── NEVER blocks on LLM calls               │                     │
  └─────────────────────┬───────────────────────┘                     │
                        │                                             │
                        ▼                                             │
  ┌─────────────────────────────────────────────┐                     │
  │  Internal Dispatch Queue (list + lock)       │                     │
  │                                              │                     │
  │  ┌──────────┐  ┌──────────┐  ┌──────────┐   │                     │
  │  │ Alice:   │  │ Bob:     │  │ Carol:   │   │                     │
  │  │ "summar  │  │ "deploy  │  │ "what's  │   │                     │
  │  │ ize Q3"  │  │ my app"  │  │ the ETA" │   │                     │
  │  └────┬─────┘  └────┬─────┘  └────┬─────┘   │                     │
  │       │             │             │          │                     │
  │       │    ┌────────┘             │          │                     │
  │       │    │                      │          │                     │
  │       ▼    ▼                      ▼          │                     │
  │  ┌─────────────────────────────────────┐     │                     │
  │  │  Session Dispatcher                 │     │                     │
  │  │  ├── Identifies profile per msg     │     │                     │
  │  │  ├── Routes to active or new session│     │                     │
  │  │  └── For deployer profile:          │     │                     │
  │  │      └── Acquires lockfile first    │     │                     │
  │  └─────────────────────────────────────┘     │                     │
  └─────────────────────────────────────────────┘                     │
                        │                                             │
          ┌─────────────┼─────────────┐                               │
          ▼             ▼             ▼                               │
  ┌────────────┐ ┌────────────┐ ┌────────────┐                        │
  │ Alice's    │ │ Bob's      │ │ Carol's    │                        │
  │ agent loop │ │ agent loop │ │ agent loop │                        │
  │ (LLM call) │ │ (LLM call) │ │ (LLM call) │                        │
  │ ═══════╗   │ │            │ │            │                        │
  │ ║ RUN  ║   │ │ ═══════╗   │ │            │                        │
  │ ╚══════╝   │ │ ║ RUN  ║   │ │ ═══════╗   │                        │
  │            │ │ ╚══════╝   │ │ ║ RUN  ║   │  ← all concurrent       │
  │            │ │            │ │ ╚══════╝   │                        │
  └────────────┘ └────────────┘ └────────────┘                        │
        │              │              │                               │
        ▼              ▼              ▼                               │
  ┌─────────────────────────────────────────────────────────────┐     │
  │  Chat REST API (outbound)                                    │     │
  │  Each agent replies independently. No ordering guarantees    │     │
  │  needed — each user sees only their own conversation.        │     │
  └─────────────────────────────────────────────────────────────┘     │
                                                                      │
  Serialized operations (lockfile):
  ┌──────────────────────────────────────────────┐                    │
  │  deployer profile only:                      │                    │
  │  - Deploy plugin/app                         │                    │
  │  - Edit Caddyfile                            │                    │
  │  - Modify shared skills                       │                    │
  │  These wait for the lock. All other ops free. │                    │
  └──────────────────────────────────────────────┘

Race Condition Matrix

#ScenarioRiskExisting ProtectionAdditional Mitigation
1 Two users message the bot simultaneously — Alice's LLM takes 8 seconds. Bob's message arrives during that time. 🟢 Low Pub/Sub serializes delivery. Internal dispatch queue separates them. Alice and Bob get independent agent loops. Set GOOGLE_CHAT_MAX_MESSAGES=10 so the adapter drains Pub/Sub quickly and queues internally. Different-profile messages never block each other.
2 Two users deploy simultaneously — both routes to deployer profile. Both try to edit the Caddyfile, run Docker commands, modify shared files at the same time. 🔴 High None. Deployer is a single profile. Pub/Sub serialization within-session but two sessions can overlap. Deployer lockfile — required before any deploy operation. See lockfile implementation below.
3 OAuth token overwrite — Alice authorizes Drive. Bob authorizes Drive. Bob's token overwrites Alice's in the default single-file token store. 🔴 High None (known issue #15602). The token path is a global constant, not per-profile. Per-profile token resolver patch. The fix is 20 lines following PR #59339's pattern — resolve token from google_tokens/<email>.json at call time instead of a global path frozen at import.
4 Git auto-commit captures half-written state — Cron fires every 4 hours while an agent is mid-edit of a config file. git add picks up the incomplete file. 🟡 Medium Auto-commit is periodic, not continuous. Window of vulnerability is small (seconds). Pre-commit health check. Only commit if the gateway is reachable and no active errors. See health check script below.
5 Cron job vs agent session in same profile — Cron job writes observations to Honcho at the same time as a user's agent session. 🟡 Medium Separate session keys. Honcho uses PostgreSQL transactions for writes. Dedicated cron peer. Configure cron jobs to write to a separate Honcho peer (cron-worker) rather than the user's main peer. Prevents memory interleaving.
6 Honcho Deriver concurrent writes — Two conversations finish simultaneously. Deriver tries to extract observations from both at the same time. 🟢 Low PostgreSQL ACID transactions. Row-level locking prevents corruption. None needed. The worst case is one write blocks briefly. No data loss.
7 Profile config read during write — Agent saves config.yaml while the gateway reads it to load a profile. Partial YAML parse error. 🟡 Medium Config files are small (< 5KB). Write is atomic on most filesystems. Read and write windows are sub-millisecond. Atomic write pattern: write to config.yaml.tmp → fsync → rename to config.yaml. Hermes's default write follows this pattern. Verify before deploying custom write tools.

Mitigation 1: Deployer Lockfile (Essential)

This lockfile prevents two deploy operations from running simultaneously. The deployer skill acquires the lock before starting and releases it on completion or failure:

#!/usr/bin/env python3
# deployer_lock.py — included in deployer profile's skill
import os, time, json, sys

LOCK_PATH = "/opt/data/.deployer.lock"
TIMEOUT = 180  # seconds — max deploy time

def acquire_lock(timeout=TIMEOUT):
    start = time.time()
    while True:
        if not os.path.exists(LOCK_PATH):
            with open(LOCK_PATH, 'w') as f:
                json.dump({"pid": os.getpid(),
                           "start": time.time(),
                           "user": os.environ.get("GOOGLE_CHAT_USER_EMAIL", "unknown")}, f)
            return True
        # Check if lock is stale (> timeout)
        try:
            with open(LOCK_PATH) as f:
                data = json.load(f)
            if time.time() - data.get("start", 0) > TIMEOUT:
                os.remove(LOCK_PATH)
                continue  # retry
        except (json.JSONDecodeError, FileNotFoundError):
            os.remove(LOCK_PATH)
            continue
        if time.time() - start > timeout:
            return False  # timeout
        time.sleep(1)

def release_lock():
    if os.path.exists(LOCK_PATH):
        os.remove(LOCK_PATH)

# Usage in deployer skill:
# if not acquire_lock():
#     return "Another deploy is in progress. Try again in a few minutes."
# try:
#     # ... deploy logic ...
# finally:
#     release_lock()
Critical: Every deployer-level operation must go through this lock. This includes: deploying plugins, modifying shared skills, editing Caddyfile, Docker Compose changes, Cloud Run deployments. Profile-level operations (reading files, running tools within user workspace) do not need the lock — they're Tirith-scoped and inherently isolated.

Mitigation 2: Pre-Commit Health Check

The auto-commit cron should only commit when the system is healthy. A broken gateway or active errors means in-flight edits could be captured mid-write:

#!/bin/bash
# /opt/data/scripts/auto-commit.sh — with health check
cd /opt/data

# 1. Check gateway health
if ! curl -sf http://localhost:9292/health > /dev/null 2>&1; then
  echo "[$(date)] Gateway unhealthy — skipping auto-commit"
  exit 1
fi

# 2. Check no recent errors in last 5 minutes
if journalctl -u hermes-gateway --since "5 min ago" 2>/dev/null | grep -qi "error\|critical\|traceback"; then
  echo "[$(date)] Recent gateway errors — skipping auto-commit"
  exit 2
fi

# 3. Commit if changes exist
git add -A
if ! git diff --cached --quiet; then
  git commit -m "Auto-commit: $(date +%Y%m%d-%H%M%S) — health check passed"
  echo "[$(date)] Auto-commit succeeded"
else
  echo "[$(date)] No changes to commit"
fi

Mitigation 3: Dedicated Cron Peer in Honcho

Cron jobs should write observations to a separate Honcho peer to avoid interleaving with user session data:

# In cron job's honcho.json (override per cron profile)
{
  "baseUrl": "http://honcho:8000",
  "hosts": {
    "hermes": {
      "workspace": "hermes-team",
      "peerName": "team",              # shared workspace — same as user sessions
      "aiPeer": "cron-worker",         # DIFFERENT from user sessions ("alice", "bob")
      "observationMode": "directional" # cron writes facts, but doesn't read user memory
    }
  }
}

This means cron-extracted facts live in the same workspace peer (visible to all users), but are attributed to the cron-worker AI peer rather than interleaving with alice's session observations. When Alice queries "any updates?", the agent searches the workspace peer — both cron and user contributions appear as team knowledge.

Mitigation 4: Pub/Sub Flow Control Configuration

# In Hermes .env — correct adapter settings for concurrent team use
GOOGLE_CHAT_MAX_MESSAGES=10        # Drain Pub/Sub quickly, don't serialize
GOOGLE_CHAT_MAX_BYTES=16777216     # 16 MiB cap on in-flight bytes
# No per-session serialization — the internal queue handles ordering

With MAX_MESSAGES=10, the adapter pulls up to 10 messages per poll. Each is enqueued to the internal dispatch queue independently. Alice's long LLM call (8 seconds) does not block Bob's message from being dispatched to his own profile's agent loop. The only serialization is within a single user's session — Alice's second message waits for her first to complete, which is correct conversational behavior.

Decision Matrix: What Needs Serialization

OperationSerialized?Why
Alice's DM → her agent❌ No — runs in her profile, isolatedDifferent profiles == independent agent loops
Bob's DM → his agent❌ No — same reasonNo shared state between different-user sessions
Alice sends two messages rapidly✅ Yes — within Alice's sessionConversation ordering matters. Second message processed after first completes.
Deploy operations (any user)✅ Yes — deployer lockfileShared system state (Caddyfile, Docker, plugins). Concurrent edits would corrupt.
Auto-commit cron❌ No — health check prevents bad commitsGit handles concurrent commits via its own locking. Only danger is half-written state → prevented by health check.
Drive OAuth setup✅ Yes — per-user token pathSingle-file token store is the bug. Once patched to per-profile, concurrent setup is safe.
Honcho Deriver extraction❌ No — PostgreSQL handles itACID transactions prevent corruption. Writes may block briefly but never corrupt.

Deploying Apps & Plugins

Deployer Profile GCP Native Security Gated

The deployer profile is a privileged Hermes profile that non-technical users interact with via natural language to deploy apps, plugins, and skills. Instead of teaching 25 people to use Docker, Portainer, or the GCP Console, they simply tell the bot what they want. The deployer handles the infrastructure.

🤖 How Users See It

Alice DMs the bot "I built a dashboard plugin" or "deploy my flask app from GitHub." The deployer profile takes over, validates permissions, builds the artifact, deploys it, and returns a URL. Alice never sees Docker, Caddy, Cloud Run, or the filesystem. She gets a working URL in seconds.

What Is the Deployer Profile?

The deployer is a standard Hermes profile with elevated permissions. It has:

CapabilityWhyHow It's Guarded
Shell access to DockerRun containers, manage Compose stacksTirith allowlist: only specific Docker commands
Caddyfile modificationsAdd reverse proxy routes for new appsPre-operation checkpoint + lockfile serialization
Write access to shared volumesInstall plugins and skills globallyDeployer-only: non-deployer profiles mount shared volumes read-only
Cloud Run API access (gcloud)Deploy serverless apps with IAP SSOIAM on the VM's service account
Git operationsAuto-commit config changes for audit trailPre-commit health check

The Three Tiers of Deployment

1 Dashboard Plugins & Hermes Skills
What:
Self-contained JS widgets for the Hermes dashboard, or markdown files for agent skills
User says:
"deploy my plugin from github.com/team/inventory-dashboard"
Deployer does:
Clones repo, runs npm install && npm run build, copies manifest.json and dist/index.js to /opt/data/plugins/<name>/, triggers dashboard rescan
Result:
Plugin appears as a new tab in the Hermes dashboard at https://team-vm.nip.io/dashboard/<name>. Immediate — no container, no port, no restart.
Auth:
Hermes dashboard session (already authenticated as the user who deployed it)
2 Docker Web Apps (Internal Team Tools)
What:
Full web applications (Flask, Streamlit, Node.js, static sites) running on the same VM behind Caddy reverse proxy
User says:
"deploy my inventory app from ghcr.io/team/inventory:v1"
Deployer does:
Acquires deployer lockfile. Pulls the image. Adds a service to docker-compose.yml. Adds a Caddy route at /apps/<name>/*. Reloads Caddy. Generates a random basic auth password. Releases lockfile.
Result:
App live at https://team-vm.nip.io/apps/inventory/ with basic auth. Deployer returns the password in the DM.
Auth:
Caddy basic auth (per-app password) — or switch to GCP IAP by deploying to Cloud Run instead (see Tier 3)
3 Cloud Run Apps (GCP Serverless, Optional IAP SSO)
What:
Serverless applications deployed to Cloud Run. Auto-scaling, scale-to-zero, managed HTTPS. Optional IAP integration for Google Workspace SSO.
User says:
"deploy my reporting app to cloud run from ghcr.io/team/reporting:v1"
Deployer does:
Acquires lockfile. Runs gcloud run deploy reporting --image=ghcr.io/team/reporting:v1 --region=us-central1 --allow-unauthenticated. Optionally enables IAP: gcloud iap web enable --resource-type=run --service=reporting --member="domain:company.com". Releases lockfile.
Result:
App live at https://reporting-xxxxx-uc.a.run.app. No port conflicts, no Caddy config, no server resources consumed. With IAP: users open the link and authenticate with their Google Workspace account automatically.
Auth:
With IAP: same Google identity as Chat. Without IAP: publicly accessible (use only for truly public tools).

Deployer Security Model

Two gates for every deploy: (1) The user's email must be in the deployer's allowed_deployers list. (2) The deployer must acquire the lockfile before starting. If either fails, the operation is rejected with a clear message.

Gate 1: Who Can Deploy

# profiles/deployer/config.yaml
multiplex:
  deployer:
    allowed_deployers:
      - alice@company.com
      - bob@company.com
      - carol@company.com
    # Users not on this list receive: "You are not authorized to deploy.
    # Ask your admin to add your email to the deployer allowlist."

Gate 2: Lockfile (Prevents Concurrent Deploys)

  # Every deploy operation starts with:
  from deployer_lock import acquire_lock, release_lock
  if not acquire_lock(timeout=180):
      return "Another deploy is in progress. Try again in a few minutes."
  try:
      # ... deploy logic ...
  finally:
      release_lock()

See the Race Conditions page for the full lockfile implementation.

Gate 3: Tirith Constraints

# Shared between deployer and deployer-specific config
terminal:
  tirith:
    fail_open: false
    allowed_paths:
      - /opt/data/plugins/**
      - /opt/data/shared/skills/**
      - /opt/data/deployments/**
      - /tmp/deploy/**
    allowed_commands:
      - docker compose -f *
      - docker compose up -d *
      - gcloud run deploy *
      - git clone *
      - npm install
      - npm run build
      - curl -X POST *
Non-deployer profiles cannot run any of these commands. Their Tirith allowlists are restricted to read-only file access and safe tools. The deployer profile is the only one with Docker, Docker Compose, and gcloud access.

Deployer SOUL.md Template

The deployer profile's behavior instructions are critical for safe operation. Copy this into profiles/deployer/SOUL.md:

# Deployer Profile — Instructions

## Your Role
You deploy apps, plugins, and skills for team members. You have elevated permissions.
You are the ONLY profile with Docker and gcloud access.

## Identity Verification
- The user's email is available as GOOGLE_CHAT_USER_EMAIL or in the event source
- Check allowed_deployers list in your config before ANY deploy operation
- If the user is not authorized, respond: "You're not authorized to deploy. Ask your admin to add your email."
- Do NOT reveal the allowed_deployers list contents to unauthorized users

## Deploy Workflow (Always Follow This)
1. Verify the user is authorized. If not, reject immediately.
2. Acquire the deployer lock. If another deploy is in progress, tell the user to wait.
3. Ask: "What type of deploy is this?" (plugin/skill/docker app / cloud run)
4. For plugins/skills: ask for the repo URL or file upload
5. For Docker apps: ask for the image name and desired subpath
6. For Cloud Run: ask for the image name and service name
7. Before modifying any file, create a timestamped checkpoint.
8. Deploy. If something fails, roll back from the checkpoint.
9. Test the deploy. Verify the app responds or the plugin loads.
10. Release the lock.
11. Reply with the URL and any credentials (password for Docker apps).

## Security Rules (NEVER Violate)
- NEVER run rm -rf or delete files arbitrarily
- NEVER modify another user's profile configs without their explicit request
- NEVER share credentials in a shared space — only in DMs
- NEVER run untrusted code from unknown repos
- For unknown repos: inspect the code first or ask the admin
- ALWAYS verify a deploy works before telling the user it's done

## Lockfile Protocol
- acquire_lock() before any deploy
- release_lock() in a finally block
- If acquire_lock times out, tell the user "Another deploy is in progress. Try again in a few minutes."
- Do NOT attempt to remove a stale lock without checking it first

Example Interactions

Alice deploys a dashboard plugin

Alice: deploy my team dashboard plugin from github.com/team/order-tracker
…deployer clones, builds, copies, rescans…
Bot: ✅ Deployed "order-tracker" to the dashboard.
  → https://team-vm.nip.io/dashboard/order-tracker
  It should appear in 5-10 seconds. Let me know if you want to change anything.

Bob deploys a Docker app

Bob: deploy my flask inventory app from ghcr.io/team/inventory:v1
Bot: Checking authorization… authorized ✓
  Acquiring deploy lock… got it ✓
  Creating checkpoint… ✓
  Pulling image…
  Adding Caddy route for /apps/inventory/* → inventory:5000… ✓
  Reloading Caddy… ✓
  Testing endpoint… 200 OK ✓
  Releasing lock… ✓
  ✅ Deployed!
  URL: https://team-vm.nip.io/apps/inventory/
  Password: z7kq9m2x
  Share this password with team members who need access.

Carol deploys to Cloud Run with IAP

Carol: deploy my reports app to cloud run from ghcr.io/team/reports:v2
Bot: Authorized ✓ Lock acquired ✓
  Running gcloud run deploy reports…
  Enabling IAP…
  ✅ Deployed to Cloud Run.
  URL: https://reports-xxxxx-uc.a.run.app
  🔒 IAP enabled — open the link and sign in with your @company.com account.

Updating & Removing Deployments

ActionUser saysDeployer does
Update pluginupdate my plugin from the latest commitPulls latest from same repo, rebuilds, replaces files, rescans dashboard
Update Docker appupdate inventory to use v2 imagePulls new image, restarts container. No Caddy change needed.
Remove pluginremove my order-tracker pluginCreates checkpoint of plugins/order-tracker/, deletes the directory, rescans dashboard
Remove Docker appundeploy inventoryStops container, removes Caddy route, reloads Caddy. Checkpoint saved for 7 days.
List deploymentswhat have I deployed?Lists all plugins, skills, and apps attributed to the user (from git history + docker ps)
Audit trail: Every deploy operation creates a git commit. git log --author="deployer" shows who deployed what, when, and from where. Removal operations save a checkpoint first. Nothing is permanently lost within the 7-day checkpoint window.

Cost Estimation

Monthly Budget Breakdown Scaling

This page breaks down every cost in the system — infrastructure, LLM tokens, backups, and optional upgrades. The goal is to give you a predictable monthly budget with no surprises. Total estimated cost for 25 users: ~$160-300/month, or ~$6-12/user/month fully loaded.

💰 Monthly Budget Summary

Infrastructure: ~$52/mo · LLM tokens: ~$160-220/mo · Backups: ~$1/mo · DNS (optional): ~$0.50/mo
Total: ~$213-273/mo ($8-11/user/mo)

1. Infrastructure — ~$52/mo

Fixed monthly costs that don't change with usage. These are the same whether you have 5 users or 50.

$37.96/mo
GCP Compute Engine — e2-standard-4
4 vCPU, 16 GB RAM, 50 GB persistent disk. Sustained-use discount applied. Enough for 25 concurrent profiles + Honcho + Redis + Caddy. GCP Pricing
$0/mo
Docker & Portainer
Free and open-source. Portainer CE runs on the VM with no license cost.
~$12/mo
Public IP + Egress
Static external IP (~$3/mo if configured) + egress traffic (~$0.12/GB). For 25 users sending ~50 messages/day with ~5KB payloads, egress is negligible (~$1-2/mo). Chat bot API calls are small payloads.
~$2/mo
Domain (optional)
If you skip nip.io and buy a real domain ($12-15/yr). Not required — nip.io is free and works for internal team tools.

2. LLM Inference (AWS Bedrock) — ~$160-220/mo

Variable cost based on usage. The dominant cost driver is the model tier used, not the number of users.

Bedrock Model Pricing (July 2026)

ModelInput ($/M tokens)Output ($/M tokens)Typical Use Case
Claude 3.5 Haiku$0.25$1.25Default — Q&A, summaries, tool calls, Drive reads, quick responses
Claude 3.5 Sonnet v2$3.00$15.00Complex reasoning, code generation, deploy operations, strategy
Claude 3 Opus$15.00$75.00Full document analysis, high-stakes decisions, deep research
Nova Lite$0.06$0.24Background tasks, cron jobs, batch processing (cheapest option)

Per-User Breakdown by Usage Tier

User TypeMsgs/DayHaiku (80%)Sonnet (17%)Opus (3%)Cost/User/Mo
Light 10 8 messages, ~500 in / 200 out each ~$2.20
Moderate 25 20 msgs, ~1,200 in / 400 out 4 msgs, ~2,500 in / 800 out 1 msg, ~4K in / 1.5K out ~$7.80
Power 50 40 msgs, ~2,000 in / 600 out 8 msgs, ~4,000 in / 1,200 out 2 msgs, ~8K in / 3K out ~$21.50

Team-Level Projection

Team CompositionCountMonthly Total
Power users (developers, heavy agent use)5~$107.50
Moderate users (daily use, occasional complex tasks)15~$117.00
Light users (check-in a few times a week)5~$11.00
Total Bedrock (25 users)~$235.50/mo
💡
What drives cost is model tier, not user count. Running all 25 users on Haiku-only would cost ~$45/mo total. The jump to ~$235/mo comes from the ~20% of messages that use Sonnet/Opus. You can tune the model routing per profile to stay within budget — see the model routing config below.

Optimizing: Per-Profile Model Routing

Control costs by setting model routing per profile. A power user who needs Sonnet gets it. A light user who only asks simple questions stays on Haiku:

# profiles/light-user/config.yaml — keep costs low
bedrock:
  default_model: anthropic.claude-3-5-haiku-20241022
  # No Sonnet override — all messages use Haiku
  # Est. cost: ~$2/mo

# profiles/power-user/config.yaml — ramp up when needed
bedrock:
  default_model: anthropic.claude-3-5-haiku-20241022
  overrides:
    - model: anthropic.claude-3-5-sonnet-20241022
      when: task_type == "coding" OR tool_count > 5 OR context > 6000
    - model: anthropic.claude-3-opus-20240229
      when: task_type == "document_review" OR user_query contains "deep analysis"
  # Est. cost: ~$20-25/mo

# profiles/deployer/config.yaml — complex operations need brains
bedrock:
  default_model: anthropic.claude-3-5-sonnet-20241022
  # Est. cost: ~$30-40/mo (but only 1 user)

Background Tasks (Cron Jobs)

Use Nova Lite ($0.06/M input) for all scheduled cron jobs — backup verification, checkpoint cleanup, disk monitoring, auto-commits. These don't need reasoning power:

# honcho.json for cron profile
hermes cron create \
  --name "check-disk" \
  --schedule "daily at 6am" \
  --prompt "Run /opt/data/scripts/check-disk.sh and report if usage > 80%" \
  --profile cron-worker \
  --model provider=bedrock,model=amazon.nova-lite-v1.0

# Cost: ~$0.50-1/mo for all cron jobs combined

3. Storage & Backups — ~$1/mo

ServiceDetailsCost
Cloud Storage bucketpg_dump backups (6-hourly, 7-day retention), profile configs (daily, 14-day), state + plugin snapshots. ~5-10 GB total.~$0.50/mo
VM disk snapshotDaily VM snapshot, 7-day retention. First 5 GB free per region per month.~$0.35/mo
Cloud Storage coldline archiveWeekly off-site archive, 30-day retention. Negligible at this scale.~$0.10/mo
Total backups~$1/mo

4. Optional Upgrades

UpgradeCostWhen to Consider
Cloud SQL for Honcho~$15-25/moAfter 3+ months of accumulated team knowledge. Adds automated backups, point-in-time recovery, cross-zone HA.
Larger VM (e2-standard-8)~$76/mo (+$38 vs e2-4)When running 50+ concurrent profiles or heavy Docker app workloads on the same VM.
GKE Autopilot~$50-100/moBeyond 50 users or when 99.9%+ uptime is required. Replaces single VM with managed Kubernetes.
Real domain + Cloud DNS~$1.25/mo ($15/yr)Optional — replaces nip.io with hermes.yourcompany.com. Cleaner URLs.

5. Full Monthly Breakdown

Category allocation
~$239/mo
25 users, moderate usage
LLM tokens
82%
GCP VM
16%
Backups
<1%
Domain
<1%
CategoryLow UsageModerateHeavy
GCP VM (e2-standard-4)$38$38$38
Static IP + egress$12$12$14
Bedrock tokens (all users)$50$186$450
Cloud Storage backups$1$1$2
Domain (optional)$1$1$1
Total~$102/mo~$238/mo~$505/mo
Per-user~$4/user~$10/user~$20/user
LLM tokens are the only variable. Infrastructure costs are flat. Backups are negligible. Your monthly AWS Bedrock bill will be 80-90% of your total cost. The lever you control is which model each profile uses and for what tasks. A single profile running Opus for every message costs more than 10 profiles on Haiku.

6. What $100/Mo/User Gets You

If you budget $100/user/month for Bedrock, that's $2,500/mo total for 25 users. Here's what that buys:

ScenarioMessages/Day/UserModel SplitToken VolumeCost
All Haiku, 24/7 usage200100% Haiku20M in / 5M out~$11/user
Heavy Sonnet usage10070% Haiku / 30% Sonnet8M in / 2M out~$18/user
Opus for every message20100% Opus3M in / 800K out~$105/user
Sonnet + Opus heavy5050% Sonnet / 50% Opus5M in / 1.5M out~$82/user

To burn $100/user/month, every message would need to use Opus (the most expensive model). With intelligent routing (Haiku default, Sonnet/Opus only when needed), real costs are $5-10/user/month. Your $100/user gut estimate likely came from SaaS agent platforms that charge per-seat licensing — Bedrock has no per-seat fee, you pay only for tokens consumed.

7. Cost Tracking & Alerts

Set up budget alerts to avoid surprises:

1
AWS Budgets: Create a budget in AWS Cost Explorer for the Bedrock service. Set alerts at 80% and 100% of monthly forecast. Send to the admin's email.
2
GCP Budgets: Create a budget for the Compute Engine and Cloud Storage services. Set alerts at 75% and 90%. GCP budgets can trigger Pub/Sub notifications that the bot can consume.
3
Per-user Bedrock tagging: Tag Bedrock API calls with the HERMES_PROFILE to break down costs by user in AWS Cost Explorer. Add --tag key=hermes-profile,value=$PROFILE to the Bedrock provider config.
4
Monthly review cron: Schedule a cron on the deployer profile that runs aws ce get-cost-and-usage on the 1st of each month and summarizes spending by profile. "Bot, how did our Bedrock costs look last month?"

User Onboarding

Setup Flow Admin Tasks User Tasks

Admin Setup (One Time, ~1 Hour)

Phase 1: GCP Infrastructure

1
Create GCP VM — e2-standard-4 with Ubuntu 24.04, 100GB SSD. Install Docker + Portainer via startup script.
2
Configure firewall — Open port 9443 (Portainer) restricted to your IP. Open port 9292 (Hermes API) restricted to office VPN if needed.
3
Deploy stack — Paste docker-compose.yml into Portainer. 4 containers spin up (Hermes, Honcho API, PostgreSQL, Redis).

Phase 2: Google Chat Bot

4
Create GCP project for the Chat bot. Enable Chat API + Pub/Sub API. Create Service Account. Create Pub/Sub topic + subscription.
5
Configure Chat app in Google Cloud Console — point it to the Pub/Sub topic. Restrict visibility to your Workspace.
6
Configure Hermes — Add Google Chat adapter settings to hermes.env. Mount the SA key. Set GOOGLE_CHAT_ALLOWED_USERS.

Phase 3: AWS Bedrock

7
Configure AWS credentials — Set AWS_PROFILE or IAM instance profile on the GCP VM. Ensure Bedrock access is granted for the desired models.

Phase 4: Profiles

8
Create profile directories — Generate profiles/alice/, profiles/bob/, etc. Each with config.yaml, .env, SOUL.md, Tirith allowlist.
9
Deploy email mapper hook — Install the pre_gateway_dispatch hook that maps alice@co.comalice profile.

User Setup (~5 Minutes Each)

1
Open Google Chat → New Chat → search for "Hermes Team" → send a message. The bot responds. Done.
2
Optional: Run /setup-google-drive in the DM to authorize Drive access. Click the OAuth link, approve, paste back the URL.
3
Optional: Run /setup-profile to set preferences — model selection (Sonnet vs Haiku), notification preferences, timezone.

Onboarding Checklist

ItemOwnerDone
Create GCP VM + install Docker/PortainerAdmin
Configure firewall rulesAdmin
Deploy docker-compose stack in PortainerAdmin
Create Google Cloud project for Chat botAdmin
Enable Chat API + Pub/Sub APIAdmin
Create Service Account + download JSON keyAdmin
Create Pub/Sub topic + subscriptionAdmin
Grant IAM roles (Chat publisher, SA subscriber)Admin
Configure Chat app in GCP ConsoleAdmin
Restrict bot visibility to workspaceAdmin
Configure Google Chat adapter in Hermes envAdmin
Install bot in test DMAdmin
Set up AWS Bedrock + IAM roleAdmin
Create 25 profile directories with configsAdmin
Write email → profile mapping hookAdmin
Create Honcho workspace + peersAdmin
Test with Alice's Google accountAdmin
Send onboarding instructions to teamAdmin
Each user DMs the bot (5 min)Users
Optional: Each user authorizes Google DriveUsers

Cost Summary

ItemMonthly Cost
GCP e2-standard-4 VM~$50-70
100GB SSD persistent disk~$10
AWS Bedrock (Claude Sonnet, moderate usage)~$300-500
Google Workspace licenses (included)$0 (already paid)
Honcho Deriver LLM costs~$20-50
Total estimated~$380-630/mo

Hermes Team Deployment — Architecture & Deployment Guide
Hermes v0.18+ · Multiplex Mode · AWS Bedrock · Google Chat · Honcho · GCP

Backup & Recovery Strategy

GCP Native No New Dependencies Disaster Recovery

All backups use GCP native services — no third-party tools, no external storage providers. The strategy protects every data layer from accidental deletion to total VM loss. Recovery is designed to be executed by a non-technical admin via the GCP Console (no CLI required).

What Needs Backing Up

Data LayerContentLoss ImpactBackup Target
1. Honcho PostgreSQLAll shared workspace memory, per-user observations, session summaries, vector embeddingsCritical — complete loss of team knowledge. Conversations would start fresh, no cross-user learning.GCP Cloud Storage bucket
2. Profile configsconfig.yaml, .env, SOUL.md, google_tokens/oAuth files, AWS credentialsCritical — 25 user identities, model configs, Tirith rules. Without these, no one can use the bot.GCP Cloud Storage bucket + git repo
3. Hermes stateSession DBs, gateway state, skill registrations, plugin manifestsMedium — sessions lost but users can continue. Gateway auto-recovers state on restart.GCP Cloud Storage bucket
4. Dashboard pluginsPlugin code in /opt/data/plugins/Medium — team-built tools lost. Can be re-deployed from source.GCP Cloud Storage bucket
5. VM boot diskOS, Docker images, system configLow — replaceable via startup script and docker-compose. The four data layers above are what matter.GCP Snapshot
6. Docker Compose stackdocker-compose.yml, Caddyfile, honcho.jsonMedium — tracked in git. Redeploy from repo.Git repository (separate from data)
  BACKUP TARGETS
  ┌─────────────────────────────────────────────────────────────────────┐
  │                                                                     │
  │  VM (e2-standard-4)                                   █████████████│
  │  ┌─────────────────────────────────────┐             █  DAILY VM  █│
  │  │ Honcho PostgreSQL (pg_dump) ────────┼──►           █  SNAPSHOT  █│
  │  │ Profile configs (rsync) ────────────┼──►           █ (optional) █│
  │  │ Hermes state (rsync) ───────────────┼──►           █████████████│
  │  │ Plugins & skills (rsync) ──────────┼──►                          │
  │  └─────────────────────────────────────┘         ██████████████████│
  │                                                  █  CLOUD STORAGE █│
  │                                                   █  BUCKET        █│
  │                                                   █  - pg_dump.sql █│
  │                                                   █  - profiles/   █│
  │                                                   █  - state/     █│
  │                                                   █  - plugins/   █│
  │                                                   █  7-day retention█│
  │                                                   █  30-day archival█│
  │                                                   ██████████████████│
  │                                                                     │
  │  ┌─────────────────────────────────────┐          ██████████████████│
  │  │ CLOUD SQL (optional upgrade)        │          █  MANAGED PITR  █│
  │  │ Point-in-time recovery: 7 days      │          █  automatic     █│
  │  │ Built-in HA: cross-zone             │          ██████████████████│
  │  └─────────────────────────────────────┘                            │
  └─────────────────────────────────────────────────────────────────────┘

Backup Schedule

Backup Schedule Automated
TypeFrequencyRetentionCommandCost
PostgreSQL dumpEvery 6 hours7 daysdocker exec honcho-db pg_dumpFree
Profile configsDaily14 daysrsync profiles/ → bucket~$0.10/mo
Hermes stateDaily14 daysrsync state/ → bucket~$0.05/mo
Plugins & skillsDaily14 daysrsync plugins/ → bucket~$0.05/mo
VM disk snapshotDaily7 daysgcloud compute snapshots create~$0.50/mo
Off-site archiveWeekly30 days (coldline)rsync → bucket coldline class~$0.10/mo

Total backup cost: ~$0.80/mo — Cloud Storage buckets for data, snapshots for the VM. GCP free tier covers the first 5GB of snapshot storage.

Setup: Cloud Storage Bucket

One-time setup (GCP Console, 5 minutes):
1
Go to Cloud Storage → Buckets → Create. Name: hermes-team-backups-<project-id>. Region: same as your VM (us-central1). Default storage class: Standard. Leave everything else default. Create bucket.
2
Grant the VM access: IAM → find your VM's service account (PROJECT_NUMBER-compute@developer.gserviceaccount.com) → add role Storage Object Admin. This lets the VM write to the bucket without any static credentials.
3
Set lifecycle rule (optional): Bucket → Lifecycle → Add rule: "Delete object after 30 days." Or use object versioning for soft-delete recovery.

Implementation

A. Database Backups (Every 6 Hours)

The most critical backup — Honcho's PostgreSQL contains all team knowledge. A six-hour cadence means at most half a day of lost conversations:

#!/bin/bash
# /opt/data/scripts/backup-db.sh
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BUCKET="gs://hermes-team-backups-<project>/db/"

docker exec honcho-db pg_dump -U honcho honcho \
  --no-owner --no-acl \
  | gzip > /tmp/honcho-$TIMESTAMP.sql.gz

gsutil cp /tmp/honcho-$TIMESTAMP.sql.gz $BUCKET
gsutil cp /tmp/honcho-$TIMESTAMP.sql.gz $BUCKET"latest.sql.gz"  # overwrite for easy restore

rm /tmp/honcho-$TIMESTAMP.sql.gz

B. Profile & State Backups (Daily)

#!/bin/bash
# /opt/data/scripts/backup-profiles.sh
DATE=$(date +%Y%m%d)
BUCKET="gs://hermes-team-backups-<project>/profiles/"

gsutil -m rsync -r /opt/data/profiles $BUCKET"$DATE/"
gsutil -m rsync -r /opt/data/.hermes $BUCKET"state/$DATE/"
gsutil -m rsync -r /opt/data/plugins $BUCKET"plugins/$DATE/"

C. VM Disk Snapshot (Daily)

gcloud compute snapshots create hermes-vm-daily-$(date +%Y%m%d) \
  --source-disk=hermes-vm \
  --source-disk-zone=us-central1-a \
  --storage-location=us-central1

D. Schedule with Cron Jobs (Agent-Managed)

Each backup script runs via a Hermes cron job. The agent handles the scheduling, the scripts do the actual work. No external cron system needed.

hermes cron create \
  --name "db-backup" \
  --schedule "every 6h" \
  --prompt "Run /opt/data/scripts/backup-db.sh" \
  --profile deployer

hermes cron create \
  --name "profile-backup" \
  --schedule "daily at 2am" \
  --prompt "Run /opt/data/scripts/backup-profiles.sh" \
  --profile deployer

hermes cron create \
  --name "vm-snapshot" \
  --schedule "daily at 3am" \
  --prompt "Run gcloud compute snapshots create hermes-vm-daily-\$(date +%Y%m%d) --source-disk=hermes-vm --source-disk-zone=us-central1-a --storage-location=us-central1" \
  --profile deployer

Recovery Procedures

Scenario 1: Database Corruption or Accidental Delete

Time to recover: ~10 minutes. Tools needed: GCP Console only.

1
Stop the Honcho API container in Portainer (no traffic while restoring).
2
Download the most recent dump: gsutil cp gs://hermes-team-backups-<project>/db/latest.sql.gz /tmp/
3
Restore: gunzip -c /tmp/latest.sql.gz | docker exec -i honcho-db psql -U honcho honcho
4
Start the Honcho API container. Verify memory is intact by asking the bot a known fact.

Scenario 2: Individual User Profile Corruption

Time to recover: ~5 minutes. No other users affected.

1
Download the last good profile backup: gsutil cp -r gs://.../profiles/20260715/alice/ /opt/data/profiles/alice-rollback/
2
Restore: cp -r /opt/data/profiles/alice-rollback/* /opt/data/profiles/alice/
3
Alice's next DM loads the restored profile. No restart needed — multiplex profiles are loaded per-turn.

Scenario 3: Total VM Loss (VM deleted, region disaster)

Time to recover: ~45 minutes. Everything is replaceable because all data is in Cloud Storage.

1
Create a new VM: same machine type, same region. Re-run the startup script (Docker + Portainer).
2
Install gcloud and authenticate: gcloud auth login or use the VM's default service account.
3
Restore profile configs: gsutil -m rsync -r gs://hermes-team-backups-<project>/profiles/LATEST/ /opt/data/profiles/
4
Restore state + plugins: same rsync pattern for state/ and plugins/.
5
Deploy the Docker Compose stack in Portainer. Paste the compose file from git.
6
Restore the database: download the latest dump, restore into the new Postgres container.
7
Re-create the Google Chat bot Pub/Sub subscription (it's tied to the VM's IP or SA). Verify bot responds in DMs.
Total data loss risk at any point: ~6 hours max (the database backup cadence). Profile configs, state, and plugins are snapshotted daily. The VM itself is replaceable — it's just Docker.

Scenario 4: Cryptographic Key Loss

If the Google Chat SA key or Google Drive OAuth client secret is lost, services stop working. These cannot be restored from backup — they must be re-created:

1
Google Chat SA key: GCP Console → IAM & Admin → Service Accounts → hermes-chat-bot → Keys → Add Key → New JSON. Download, copy to VM.
2
Google Drive OAuth client: GCP Console → APIs & Services → Credentials → OAuth 2.0 Client IDs → Download JSON. Redeploy to profiles.
3
AWS Bedrock IAM: No key stored — the VM's IAM role is configured at the GCP instance level. No recovery needed. If credentials need rotation, update the instance's service account.

Automated Verification

Backups are worthless if they can't be restored. A cron job that verifies backup integrity weekly:

#!/bin/bash
# /opt/data/scripts/verify-backup.sh
# Runs weekly. Tests that the latest DB dump is valid.
LATEST=$(gsutil ls gs://hermes-team-backups-<project>/db/ | tail -1)
gsutil cp "$LATEST" /tmp/verify.sql.gz
gunzip -t /tmp/verify.sql.gz && echo "✅ DB dump is valid" || echo "❌ DB dump corrupt"
rm /tmp/verify.sql.gz

# Verify profile configs exist for all 25 users
for name in $(ls /opt/data/profiles/); do
  if [ -f "/opt/data/profiles/$name/config.yaml" ]; then
    echo "✅ $name config found"
  else
    echo "❌ $name config MISSING"
  fi
done
Production-ready backup costs: ~$1/mo — Cloud Storage for data, Compute Engine snapshots for the VM. No third-party backups, no external storage providers. The entire recovery procedure can be followed by any team member with GCP Console access.

Optional Upgrade: Cloud SQL for Honcho

For zero-effort backups with point-in-time recovery, migrate the Honcho database from the container to GCP Cloud SQL. This replaces the pg_dump cron with Google-managed backups:

FeatureContainer PostgreSQLCloud SQL (managed)
BackupsYour cron scriptAutomated, daily + on-demand
Point-in-time recovery✅ 7 days
High availability❌ Single container✅ Cross-zone failover
PatchesYou manageGoogle-managed, zero-downtime
Cost (for Honcho scale)Included in VM~$15-25/mo

When to upgrade: When the team has been using the system for 3+ months and the accumulated knowledge makes even 6 hours of potential data loss unacceptable. Until then, the container + pg_dump cron is sufficient.

Checkpoints & Version Control

Git-Based Agent Error Recovery User Error Recovery

Backups protect against VM loss. Checkpoints and git version control protect against the more common threat: the agent or a user making a mistake that corrupts configs, skills, or state. A bad config change, a typo in a SOUL.md, a plugin that overwrites a shared file — these happen far more often than server failures. This page covers git-tracking configs for instant rollback, pre-operation checkpoints, and patterns for undoing agent-caused damage.

Threat Model: What Actually Goes Wrong

ScenarioCauseFrequencyDetectionRecovery
Agent edits a profile config.yaml and introduces a syntax errorAgent hallucinates a YAML key or indentationCommon (weekly)Hermes gateway fails to load the profile. Error in logs.git revert on config file
Agent overwrites a shared skill with a bad versionAgent misinterprets "improve this skill"Occasional (monthly)Users report the skill stopped workinggit checkout — previous version
User asks agent to "clean up my config" and agent deletes important settingsUser gave vague instructions, agent over-interpretedOccasional (monthly)User's profile stops workingProfile checkpoint restore
Agent bulk-edits Honcho memory and corrupts observationsAgent tool call with bad filter or dataRare (quarterly)Semantic search returns garbageDatabase point-in-time restore (6h max)
User accidentally deletes a plugin from the shared volumeUser had SSH access or used a file managerRareDashboard plugin tab missinggit restore on plugins directory
Agent creates 1000 duplicate files in a profile workspaceAgent stuck in a loop writing filesRare (quarterly)Disk fills up, alert triggersDirectory checkpoint rollback

🛡️ The Principle: Every Change Is Reversible

Before any operation that modifies files — config edits, skill updates, plugin changes, workspace cleanup — the agent creates a timestamped checkpoint. If the operation produces an error, the agent rolls back to the checkpoint automatically. If the error is discovered later, the checkpoint is still there in git history. The only irreversible state is the one you didn't version.

Git Repository Structure

All configuration files live in a git-tracked directory. This is distinct from data files (which go to Cloud Storage backups) — configs change frequently and need per-line attribution and rollback:

/opt/data/
├── config.yaml              # git-tracked
├── Caddyfile                # git-tracked
├── honcho.json              # git-tracked
├── docker-compose.yml       # git-tracked
├── profiles/                # git-tracked (configs only)
│   ├── alice/
│   │   ├── config.yaml      # git-tracked
│   │   ├── .env             # git-tracked (no secrets — those are in GCP Secret Manager or mounted files)
│   │   └── SOUL.md          # git-tracked
│   ├── bob/
│   └── ...
├── shared/                  # git-tracked
│   ├── skills/              # git-tracked
│   └── docs/                # git-tracked
├── plugins/                 # git-tracked (manifests + code)
├── scripts/                 # git-tracked
├── .git/                    # auto-committed via cron
├── checkpoints/             # pre-operation snapshots (not in main git — managed separately)
│   ├── alice-20260715-140231/
│   └── shared-skills-20260715-140500/
└── data/                    # NOT git-tracked — backed up to Cloud Storage
    ├── google_tokens/
    └── session_db/
What's in git vs what's in backups: Configs, skills, plugins, and scripts change by user/agent action and benefit from line-level version history. Data files (tokens, sessions) are machine-generated and go to Cloud Storage as whole-file backups. The separation means git history stays clean and relevant — no binary token files bloating the repo.

Auto-Commit Cron

A cron job on the deployer profile commits all tracked changes every 4 hours. This creates an audit trail of who changed what (the commit author is the profile name):

#!/bin/bash
# /opt/data/scripts/auto-commit.sh
# Runs every 4 hours via Hermes cron. Commits all tracked file changes.
cd /opt/data

# Stage all tracked files
git add -A

# Check if there are changes
if ! git diff --cached --quiet; then
  git commit -m "Auto-commit: $(date +%Y-%m-%dT%H:%M:%S%z) — profile: ${HERMES_PROFILE:-unknown}"
  # Optional: push to a private git remote if you want off-machine redundancy
  # git push origin main
fi
hermes cron create \
  --name "auto-commit" \
  --schedule "every 4h" \
  --prompt "Run /opt/data/scripts/auto-commit.sh" \
  --profile deployer

Recovery Scenarios

Scenario A: Agent Corrupts a Config File

What happened: Alice asked the agent "update my config to use Bedrock Haiku instead of Sonnet." The agent wrote a malformed YAML with wrong indentation. The gateway logs an error and Alice's profile stops loading.

1
Detect: Alice reports "the bot isn't responding." Gateway logs show YAML load error: profiles/alice/config.yaml.
2
Rollback: cd /opt/data && git log --oneline profiles/alice/config.yaml — find the last good commit. git checkout HASH -- profiles/alice/config.yaml.
3
Verify: Alice DMs the bot. Gateway loads the profile cleanly. Alice is back online in under 2 minutes.

Scenario B: Agent Overwrites a Shared Skill with a Bad Version

What happened: Bob told the agent "improve the deploy skill to handle Docker images better." The agent rewrote the skill badly — it no longer works. All 25 profiles are now using the broken skill.

1
Rollback the file: git checkout HEAD~1 -- shared/skills/deploy-skill/ — restores the entire skill directory to before Bob's change.
2
Commit the fix: git commit -m "rollback: deploy-skill broken by agent edit"

Scenario C: Agent Spirals and Creates Thousands of Files

What happened: A cron job or agent task gets stuck in a loop writing files to a profile's workspace. Disk usage spikes.

1
Kill the agent task: The loop is running in a session. Kill it via /cancel or restart the gateway.
2
Restore from checkpoint: If the workspace has checkpoints enabled: rsync -a /opt/data/checkpoints/alice-workspace-20260715-140000/ /opt/data/workspaces/alice/
3
Clean up with git: If the workspace is git-tracked: git checkout -- workspaces/alice/. Otherwise, delete the overflow files manually from the checkpoint diff.

Scenario D: User Accidentally Deletes a Plugin

What happened: A user with file manager access accidentally moved a plugin directory.

1
Restore from git: git checkout HEAD -- plugins/missing-plugin/
2
Rescan: curl -X POST http://dashboard:9119/api/dashboard/plugins/rescan

Pre-Operation Checkpoints (Agent-Side)

The deployer profile's skill template includes an automatic checkpoint before any destructive operation:

# skill: deploy-with-checkpoint (template for all deploy operations)

def pre_checkpoint(target_dir: str, operation_name: str):
    """Copy the target directory to /opt/data/checkpoints/ before making changes."""
    import shutil, datetime
    ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
    safe_name = operation_name.replace(" ", "_")
    dest = f"/opt/data/checkpoints/{safe_name}-{ts}/"
    shutil.copytree(target_dir, dest)
    return dest

def on_error_rollback(checkpoint_dir: str, target_dir: str):
    """Restore the target directory from checkpoint on failure."""
    import shutil
    shutil.rmtree(target_dir)
    shutil.copytree(checkpoint_dir, target_dir)

# Usage in any deploy/update skill:
# checkpoint = pre_checkpoint("/opt/data/profiles/alice", "update config")
# try:
#     # ... make changes ...
# except Exception as e:
#     on_error_rollback(checkpoint, "/opt/data/profiles/alice")
#     return f"Operation failed and was rolled back. Error: {e}"

This means the agent automatically snapshots the state before making changes. If the operation errors — syntax error, bad format, network timeout — the checkpoint is restored. If the operation succeeds but the result is wrong, the checkpoint is still available in the checkpoints directory for manual rollback.

Checkpoint Cleanup

Checkpoints accumulate. A weekly cleanup cron keeps only the last 7 days:

#!/bin/bash
# /opt/data/scripts/cleanup-checkpoints.sh
find /opt/data/checkpoints -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} +
echo "Cleaned up checkpoints older than 7 days"
hermes cron create \
  --name "cleanup-checkpoints" \
  --schedule "weekly on Sunday at 4am" \
  --prompt "Run /opt/data/scripts/cleanup-checkpoints.sh" \
  --profile deployer

The Agent's Own Safeguards

Beyond file and config protection, the Hermes agent itself needs guardrails against its own mistakes:

SafeguardHow It WorksPrevents
Tirith fail_open: falseThe agent's file tool calls are intercepted. Any path outside the profile's allowlist is blocked with a clear error.Agent accidentally modifying another user's files or system configs
SOUL.md destroy directiveEach profile's SOUL.md includes: "You CANNOT delete files. You CANNOT run rm -rf. If a user asks you to clean up files, ask which specific files."Agent interpreting "clean up" as "delete everything"
Checkpoint before editsThe deployer skill template snapshots target dirs before any modification.Agent corrupting a file during a bad edit
Git auto-commit every 4hAll tracked files are committed automatically. Every change has an author, timestamp, and diff.Silent config drift — you can always see who changed what and when
Read-only shared volumesThe shared skills directory is mounted :ro (read-only) for all non-deployer profiles. Only the deployer profile can write to it.Alice's agent accidentally overwriting a team-wide skill
Human-in-the-loop for destructive operationsThe deployer profile requires explicit confirmation: "I'm about to modify [file]. The current version is backed up at [checkpoint path]. Proceed? (yes/no)"Agent making changes without user awareness
End-to-end robustness: The combination of git version control (configs), Cloud Storage backups (data), pre-operation checkpoints (agent actions), and Tirith restrictions (filesystem) means every change is reversible and every mistake has a recovery path. The most dangerous scenario — total VM loss — is the least likely. The most common scenario — a bad config edit — is recoverable in under 2 minutes with git checkout.

Quick Reference: Recovery Commands

ProblemCommandTime
Bad config filegit checkout HASH -- profiles/alice/config.yaml~1 min
Corrupted skillgit checkout HEAD~1 -- shared/skills/skill-name/~1 min
Deleted plugingit checkout HEAD -- plugins/plugin-name/~1 min
Agent wrote garbage filesrm -rf workspaces/alice/bad-dir/ && git checkout -- workspaces/alice/~2 min
Need to see what changed last weekgit log --oneline --since="7 days ago"~30 sec
Full profile rollback to yesterdaygit checkout $(git rev-list -1 --before="24 hours ago" main) -- profiles/alice/~2 min
Checkpoint rollbackrsync -a /opt/data/checkpoints/alice-20260715-140231/ /opt/data/profiles/alice/~1 min

Installation Quick Start

Chronological Build Plan Prioritized Phased Rollout

This is the chronological build plan — from zero to fully operational for 25 team members. Each phase is independent and testable. The system becomes usable after Phase 3. Everything after that is capability expansion.

1 Infrastructure — The VM Day 1 ~45 min Parallelizable: ☐
1.1Create GCP VMe2-standard-4, Ubuntu 24.04, 100GB SSD. gcloud compute instances create.
1.2Install Docker + PortainerStartup script: install docker.io, docker-compose-v2, launch Portainer container on port 9443.
1.3Configure firewallOpen 9443 (Portainer) to your IP. Open 9292 (Hermes API) to office VPN if needed.
1.4Set AWS IAM credentialsCreate IAM role with Bedrock invoke permissions. Set AWS_PROFILE or instance profile on the VM.
1.5Deploy Docker Compose stackPaste docker-compose.yml into Portainer. 4 containers: Hermes, Honcho API, PostgreSQL, Redis.
⏳ Status: Not started — Phases 1-3 take ~3 hours total
🔲 Milestone 1: Portainer dashboard loads at https://<IP>:9443. All 4 containers show green.
2 Google Chat Bot Day 1 ~45 min Parallelizable: ☐
2.1Create GCP project for Chat botEnable Google Chat API + Cloud Pub/Sub API.
2.2Create Service AccountName it hermes-chat-bot. Download JSON key. chmod 600.
2.3Create Pub/Sub topic + pull subscriptionTopic: hermes-chat-events. Subscription: hermes-chat-events-sub, pull, 7-day retention.
2.4IAM bindings (CRITICAL)Topic: add chat-api-push@system.gserviceaccount.com as Publisher. Subscription: add SA as Subscriber + Viewer.
2.5Configure Chat appAPIs → Google Chat API → Configuration. Set name, avatar, Pub/Sub topic. Restrict visibility to your Workspace.
2.6Install bot in a test DMSearch for bot name in Google Chat → New Chat → send first message. Verify ADDED_TO_SPACE event received.
⏳ Status: Not started
🔲 Milestone 2: Bot responds to DMs. "Hermes is thinking…" → real reply appears.
3 Profiles + Multiplex + Routing Day 2 ~60 min This makes the system usable
3.1Enable multiplex modeSet GATEWAY_MULTIPLEX_PROFILES=true in Hermes env. Restart gateway.
3.2Create profile directoriesGenerate 25 profile dirs: profiles/alice/, profiles/bob/, etc. Each with config.yaml, .env, SOUL.md.
3.3Config per profileBedrock provider, Tirith allowlists, Honcho peer name. Use a template and iterate.
3.4Write email → profile mapping hookpre_gateway_dispatch plugin: 25 email entries, stamps event.source.profile.
3.5Test with 2-3 team membersAlice and Bob DM the bot. Verify each gets their own profile, memory isolation, session persistence.
3.6Deploy Tirith allowlistsSet fail_open: false. Define allowed_paths per profile. Test cross-profile file access is blocked.
⏳ Status: Not started — System becomes usable for the team at this point
🔲 Milestone 3: Alice and Bob can DM the bot. Each gets their own memory. Alice cannot read Bob's files.
4 Shared Memory (Honcho) Day 2 ~30 min Adds team knowledge
4.1Verify Honcho is runningCheck docker ps — honcho-api, honcho-db, honcho-redis all healthy.
4.2Mount Honcho configPlace honcho.json at each profile's Hermes home. Set workspace, peer, recall mode.
4.3Create workspace + peersHoncho workspace: hermes-team. User peer: team (shared). AI peers: alice, bob, etc. (private).
4.4Test shared vs private memoryAlice enters a team fact → visible to Bob. Alice enters a private note → invisible to Bob. Confirm with queries.
⏳ Status: Not started
5 Google Drive Integration (Per-User) Day 3 ~60 min ⚠️ GATING ITEM Required for Drive access
⚠️
Requires a ~20 line patch — not a config change. The Hermes Google Workspace skill does not yet support per-user OAuth tokens in multiplex mode (issue #15602). You must patch google_api.py to add a per-profile token resolver. The pattern is already established by PR #59339 (Anthropic OAuth). Without this patch, only the last user to authorize Drive will have working access — everyone else's tokens are silently overwritten.
5.1Create OAuth consent screenGCP → APIs & Services → OAuth consent screen. Configure app name, scopes (drive.file). Add test users.
5.2Create OAuth client IDDesktop app type. Download client_secret.json.
5.3Patch google_api.py for multi-accountAdd per-profile token resolver (20 lines, same pattern as PR #59339). Resolves tokens from google_tokens/<email>.json at call time instead of a global path frozen at import. Without this step, per-user Drive access doesn't work in multiplex mode.
5.4Each user authorizes DriveRun /setup-google-drive in DM. OAuth URL → approve → paste back. Token stored by email.
5.5Set up SA for shared team DriveGCP → Service Accounts → domain-wide delegation. Scope: drive.readonly. Mount SA key into Hermes container.
⚠️ CRITICAL: Step 5.3 (google_api.py patch) is required before any user can use Drive. Without it, multiplex mode overwrites tokens.
🔲 Milestone 4: Alice asks "summarize my Q3 doc" → agent finds and reads it from her Drive. Bob's files are invisible to Alice's agent.
6 Team Rollout Day 4-5 ~2 hours User-facing
6.1Send onboarding emailInstructions: "Open Google Chat → New Chat → Search 'Hermes Team' → Say hello." Include link to Drive auth.
6.2Support first-login issuesMost common: users searching for wrong bot name, permissions not propagated, space ID not yet cached.
6.3Create team space + add botCreate #team-hermes space, add the bot. Users can @mention for shared questions.
6.4Train shared space etiquettePost a message: "Personal questions → DM me. Team questions → @mention here. I can't delete files."
6.5Set up shared skills directoryMount team skills repo as read-only volume. All profiles can use shared skills.
⏳ Status: Not started
🔲 Milestone 5: All 25 team members can DM the bot. Shared space is active. Team etiquette is established.
7 Operations & Hardening Day 5+ Ongoing Continuous
7.1Set up daily Honcho DB backupcron: docker exec honcho-db pg_dump → Cloud Storage bucket or volume backup.
7.2Deploy Cloud SQL (optional)Migrate Honcho DB from container to managed Cloud SQL for HA + point-in-time recovery.
7.3Monitor Bedrock costs per profileUse AWS Cost Explorer with HERMES_PROFILE tag. Set budgets and alerts.
7.4Watch for Hermes updatesv0.19+ may ship per-user Drive token fix (#15602). Test before upgrading production.
7.5Iterate SOUL.md per profileRefine each user's system prompt based on usage patterns. Collect feedback weekly.
7.6Scale if neededBeyond 25 users → resize VM to e2-standard-8 or migrate to GKE for autoscaling.
⏳ Status: Not started
🔲 Final milestone: System is stable, backed up, monitored. Team is self-sufficient. Roadmap items tracked.

Dependency Map

  Phase 1 (VM + Docker) ───────────────────────┐
                                               │
  Phase 2 (Google Chat bot) ───────────────────┤
                                               │
  Phase 3 (Multiplex + Profiles + Routing) ◄───┘
        │
        ├──► Phase 4 (Honcho shared memory)
        │
        ├──► Phase 5 (Google Drive per-user)
        │
        └──► Phase 6 (Team Rollout)
                 │
                 └──► Phase 7 (Operations, monitoring, hardening)
  

Critical path: Phase 1 → Phase 2 → Phase 3. Everything else is parallelizable. The system is usable for the team after Phase 3. Phases 4-7 add capability that can be rolled out gradually without disrupting users.

Estimated Timeline

DayDeliverableWho
Day 1VM running, bot responding to DMs, 2-3 test profiles workingAdmin
Day 225 profiles + multiplex + Honcho shared memory activeAdmin
Day 3Google Drive integration (per-user + team SA)Admin
Day 4Team rollout — all 25 users onboardedAdmin + Users
Day 5+Backups, monitoring, cost tracking, refinementsAdmin