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.
What This Architecture Delivers
Stack Overview
| Layer | Technology | Role |
|---|---|---|
| Identity & Interface | Google Chat (Google Workspace) | Users DM the bot or interact in shared spaces. Google identity is the auth layer. |
| Agent Runtime | Hermes Agent v0.18+ (multiplex mode) | Single gateway process, 25 isolated profiles, Tirith filesystem authorization. |
| LLM Inference | AWS Bedrock (Claude, Nova, etc.) | IAM-authenticated, no API keys. Models selected per-profile or globally. |
| Memory | Honcho (self-hosted) | Shared workspace for team context + per-peer observations for private memory. |
| Database | PostgreSQL + pgvector | Honcho backing store. Vector embeddings for semantic memory search. |
| File Storage | Google Drive | Team documents stored in Drive. Per-user OAuth for personal files. |
| Deployment | GCP Compute Engine + Portainer | Single 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)
spaces/AAA1111, users/123456789, alice@co.com.hermes-chat-events. No public endpoint needed — the subscription is pulled.source.profile via the email → profile mapping hook.agent:alice:google_chat:... — isolated from every other user.Key Design Decisions
Hermes Multiplex Gateway
Core Runtime v0.18+ Single Process 25 ProfilesThe 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.
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 PullGoogle 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
| Step | Action | GCP Console |
|---|---|---|
| 1 | Create GCP project | console.cloud.google.com |
| 2 | Enable Google Chat API + Cloud Pub/Sub API | APIs & Services → Library |
| 3 | Create Service Account for the bot | IAM & Admin → Service Accounts |
| 4 | Create Pub/Sub topic + pull subscription | Pub/Sub → Topics |
| 5 | Grant Chat API publisher role on topic | Topic IAM: chat-api-push@system.gserviceaccount.com |
| 6 | Grant SA subscriber role on subscription | Subscription IAM: your SA + Pub/Sub Subscriber |
| 7 | Configure Chat app (point to Pub/Sub topic) | APIs → Google Chat API → Configuration |
| 8 | Restrict visibility to your Workspace | Chat API Configuration → Visibility |
| 9 | Install bot via DM or space | Google Chat → New Chat → Search "Hermes Team" |
GOOGLE_CHAT_SERVICE_ACCOUNT_JSON.Message Behavior
| Scenario | Behavior |
|---|---|
| User DMs the bot | Routed to user's profile via email mapper. Private session. |
| User mentions bot in shared space | Routed to user's profile via email mapper. Public reply in thread. |
| Bot needs to share with whole space | Agent can post to any space it has access to, regardless of profile. |
| User replies in a thread | Hermes detects thread.name and replies in same thread. Separate session per thread. |
| Unauthorized user messages bot | Restricted by GOOGLE_CHAT_ALLOWED_USERS — message ignored. |
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
What Users Experience
| Action | User Sees | What Happens Backstage |
|---|---|---|
| First DM to bot | "Hermes is thinking…" → reply | Gateway receives ADDED_TO_SPACE event. Email mapper resolves alice@co.com → profile "alice". New session created. |
| Second DM (next day) | Bot remembers context from yesterday | Session key agent:alice:google_chat:dm:spaces/AAA... loads previous conversation via Honcho. |
| @mention in shared space | Reply in thread, same space | Email 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.
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-HostedHoncho 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:
| Capability | Honcho | Hermes MEMORY.md | Obsidian Vault | Plain 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
| Content | Stored In | Visible To |
|---|---|---|
| Team deadlines, project status | Workspace (user peer: "team") | All 25 profiles |
| Shared space conversations | Workspace (user peer: "team") | All 25 profiles |
| Alice's personal preferences | AI peer: "alice" (observations) | Alice only |
| Alice's project notes | AI peer: "alice" (observations) | Alice only |
| Alice's Drive file index | AI peer: "alice" (observations) | Alice only |
| Bob's preferences | AI peer: "bob" (observations) | Bob only |
What This Means For Your Team
| Scenario | Without Honcho | With Honcho |
|---|---|---|
| Bob learns the Q3 deadline is Dec 15 | Bob'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 script | Carol'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 strategy | Nothing 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-year | Alice'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 revisit | Someone 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}
.env in the Honcho container.AWS Bedrock LLM Provider
Inference IAM Auth Converse APIAll 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
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY).boto3 resolves credentials from environment variables, ~/.aws/credentials, or instance profile. No Bedrock-specific API key needed.AWS_PROFILE env var per profile or globally.Configuration
# Hermes config.yaml (global or per-profile)
model:
provider: bedrock
default: us.anthropic.claude-sonnet-4-6
# Available Bedrock models:
# us.anthropic.claude-sonnet-4-6 — Heavy reasoning
# us.anthropic.claude-3-5-haiku — Fast/cheap
# amazon.nova-pro-v1:0 — Amazon's own
# us.meta.llama4-405b-instruct-v1:0 — Open weight
# Per-profile model override:
# profiles/alice/config.yaml
model:
provider: bedrock
default: us.anthropic.claude-sonnet-4-6 # Alice gets Sonnet
auxiliary:
background_review:
model: us.anthropic.claude-3-5-haiku # cheap for background tasks
# Environment (per-profile .env or global) AWS_DEFAULT_REGION=us-east-1 AWS_PROFILE=hermes-team # Or use named profile in ~/.aws/credentials # [hermes-team] # aws_access_key_id = AKIA... # aws_secret_access_key = ...
Cost Estimates for 25 Users
| Usage Pattern | Monthly Cost |
|---|---|
| Light (5 conversations/user/day, Claude Sonnet) | ~$100-200 |
| Moderate (15 conversations/user/day, Claude Sonnet) | ~$300-500 |
| Heavy (50 conversations/user/day, Claude Sonnet) | ~$800-1500 |
| Mixed (Sonnet + Haiku for background tasks) | ~30-50% of above |
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 AccountTeam 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
https://www.googleapis.com/auth/drive.readonly)./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.$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.
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
| Scope | Read | Create | Edit | Delete | Best For |
|---|---|---|---|---|---|
drive.readonly | ✅ | ❌ | ❌ | ❌ | Team 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
drive.file + tool stripping is sufficient.GCP + Portainer Deployment
Infrastructure Single VM Portainer StackThe 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
| Resource | Recommended | Minimum |
|---|---|---|
| Machine type | e2-standard-4 (4 vCPU, 16GB) | e2-standard-2 (2 vCPU, 8GB) |
| Disk | 100GB persistent SSD | 50GB 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
| Data | Method | Frequency |
|---|---|---|
| Honcho PostgreSQL | docker exec honcho-db pg_dump | Daily |
| Profile configs | Volume backup (or git-tracked) | On change |
| Google Chat SA key | Stored separately (GCP Secret Manager) | N/A |
| Portainer data | Volume backup | Weekly |
Security Model
Isolation Layers IAM Auth TirithThe 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
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
| Threat | Prevented By | Confidence |
|---|---|---|
| Alice reads Bob's project files | Tirith path allowlist | High (tool-level gate) |
| Alice reads Bob's private memory | Honcho peer isolation | High (API-level gate) |
| Alice steals Bob's AWS credentials | IAM role (not per-user keys) | High (no keys to steal) |
| Alice reads Bob's Drive files | Per-user OAuth scoping | High (Google auth) |
| Alice executes os.open() to bypass Tirith | Not prevented in execute_code | ⚠️ Medium gap |
| External attacker messages the bot | Google Chat allowed_users | High (Google auth) |
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.User Onboarding
Setup Flow Admin Tasks User TasksAdmin Setup (One Time, ~1 Hour)
Phase 1: GCP Infrastructure
Phase 2: Google Chat Bot
hermes.env. Mount the SA key. Set GOOGLE_CHAT_ALLOWED_USERS.Phase 3: AWS Bedrock
AWS_PROFILE or IAM instance profile on the GCP VM. Ensure Bedrock access is granted for the desired models.Phase 4: Profiles
profiles/alice/, profiles/bob/, etc. Each with config.yaml, .env, SOUL.md, Tirith allowlist.pre_gateway_dispatch hook that maps alice@co.com → alice profile.User Setup (~5 Minutes Each)
/setup-google-drive in the DM to authorize Drive access. Click the OAuth link, approve, paste back the URL./setup-profile to set preferences — model selection (Sonnet vs Haiku), notification preferences, timezone.Onboarding Checklist
| Item | Owner | Done |
|---|---|---|
| Create GCP VM + install Docker/Portainer | Admin | ☐ |
| Configure firewall rules | Admin | ☐ |
| Deploy docker-compose stack in Portainer | Admin | ☐ |
| Create Google Cloud project for Chat bot | Admin | ☐ |
| Enable Chat API + Pub/Sub API | Admin | ☐ |
| Create Service Account + download JSON key | Admin | ☐ |
| Create Pub/Sub topic + subscription | Admin | ☐ |
| Grant IAM roles (Chat publisher, SA subscriber) | Admin | ☐ |
| Configure Chat app in GCP Console | Admin | ☐ |
| Restrict bot visibility to workspace | Admin | ☐ |
| Configure Google Chat adapter in Hermes env | Admin | ☐ |
| Install bot in test DM | Admin | ☐ |
| Set up AWS Bedrock + IAM role | Admin | ☐ |
| Create 25 profile directories with configs | Admin | ☐ |
| Write email → profile mapping hook | Admin | ☐ |
| Create Honcho workspace + peers | Admin | ☐ |
| Test with Alice's Google account | Admin | ☐ |
| Send onboarding instructions to team | Admin | ☐ |
| Each user DMs the bot (5 min) | Users | ☐ |
| Optional: Each user authorizes Google Drive | Users | ☐ |
Cost Summary
| Item | Monthly 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
Order of Operations
Implementation Plan Prioritized Phased RolloutThis 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.1 | Create GCP VM | e2-standard-4, Ubuntu 24.04, 100GB SSD. gcloud compute instances create. |
| 1.2 | Install Docker + Portainer | Startup script: install docker.io, docker-compose-v2, launch Portainer container on port 9443. |
| 1.3 | Configure firewall | Open 9443 (Portainer) to your IP. Open 9292 (Hermes API) to office VPN if needed. |
| 1.4 | Set AWS IAM credentials | Create IAM role with Bedrock invoke permissions. Set AWS_PROFILE or instance profile on the VM. |
| 1.5 | Deploy Docker Compose stack | Paste docker-compose.yml into Portainer. 4 containers: Hermes, Honcho API, PostgreSQL, Redis. |
https://<IP>:9443. All 4 containers show green.
| 2.1 | Create GCP project for Chat bot | Enable Google Chat API + Cloud Pub/Sub API. |
| 2.2 | Create Service Account | Name it hermes-chat-bot. Download JSON key. chmod 600. |
| 2.3 | Create Pub/Sub topic + pull subscription | Topic: hermes-chat-events. Subscription: hermes-chat-events-sub, pull, 7-day retention. |
| 2.4 | IAM bindings (CRITICAL) | Topic: add chat-api-push@system.gserviceaccount.com as Publisher. Subscription: add SA as Subscriber + Viewer. |
| 2.5 | Configure Chat app | APIs → Google Chat API → Configuration. Set name, avatar, Pub/Sub topic. Restrict visibility to your Workspace. |
| 2.6 | Install bot in a test DM | Search for bot name in Google Chat → New Chat → send first message. Verify ADDED_TO_SPACE event received. |
| 3.1 | Enable multiplex mode | Set GATEWAY_MULTIPLEX_PROFILES=true in Hermes env. Restart gateway. |
| 3.2 | Create profile directories | Generate 25 profile dirs: profiles/alice/, profiles/bob/, etc. Each with config.yaml, .env, SOUL.md. |
| 3.3 | Config per profile | Bedrock provider, Tirith allowlists, Honcho peer name. Use a template and iterate. |
| 3.4 | Write email → profile mapping hook | pre_gateway_dispatch plugin: 25 email entries, stamps event.source.profile. |
| 3.5 | Test with 2-3 team members | Alice and Bob DM the bot. Verify each gets their own profile, memory isolation, session persistence. |
| 3.6 | Deploy Tirith allowlists | Set fail_open: false. Define allowed_paths per profile. Test cross-profile file access is blocked. |
| 4.1 | Verify Honcho is running | Check docker ps — honcho-api, honcho-db, honcho-redis all healthy. |
| 4.2 | Mount Honcho config | Place honcho.json at each profile's Hermes home. Set workspace, peer, recall mode. |
| 4.3 | Create workspace + peers | Honcho workspace: hermes-team. User peer: team (shared). AI peers: alice, bob, etc. (private). |
| 4.4 | Test shared vs private memory | Alice enters a team fact → visible to Bob. Alice enters a private note → invisible to Bob. Confirm with queries. |
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.1 | Create OAuth consent screen | GCP → APIs & Services → OAuth consent screen. Configure app name, scopes (drive.file). Add test users. |
| 5.2 | Create OAuth client ID | Desktop app type. Download client_secret.json. |
| 5.3 | Patch google_api.py for multi-account | Add 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.4 | Each user authorizes Drive | Run /setup-google-drive in DM. OAuth URL → approve → paste back. Token stored by email. |
| 5.5 | Set up SA for shared team Drive | GCP → Service Accounts → domain-wide delegation. Scope: drive.readonly. Mount SA key into Hermes container. |
| 6.1 | Send onboarding email | Instructions: "Open Google Chat → New Chat → Search 'Hermes Team' → Say hello." Include link to Drive auth. |
| 6.2 | Support first-login issues | Most common: users searching for wrong bot name, permissions not propagated, space ID not yet cached. |
| 6.3 | Create team space + add bot | Create #team-hermes space, add the bot. Users can @mention for shared questions. |
| 6.4 | Train shared space etiquette | Post a message: "Personal questions → DM me. Team questions → @mention here. I can't delete files." |
| 6.5 | Set up shared skills directory | Mount team skills repo as read-only volume. All profiles can use shared skills. |
| 7.1 | Set up daily Honcho DB backup | cron: docker exec honcho-db pg_dump → Cloud Storage bucket or volume backup. |
| 7.2 | Deploy Cloud SQL (optional) | Migrate Honcho DB from container to managed Cloud SQL for HA + point-in-time recovery. |
| 7.3 | Monitor Bedrock costs per profile | Use AWS Cost Explorer with HERMES_PROFILE tag. Set budgets and alerts. |
| 7.4 | Watch for Hermes updates | v0.19+ may ship per-user Drive token fix (#15602). Test before upgrading production. |
| 7.5 | Iterate SOUL.md per profile | Refine each user's system prompt based on usage patterns. Collect feedback weekly. |
| 7.6 | Scale if needed | Beyond 25 users → resize VM to e2-standard-8 or migrate to GKE for autoscaling. |
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
| Day | Deliverable | Who |
|---|---|---|
| Day 1 | VM running, bot responding to DMs, 2-3 test profiles working | Admin |
| Day 2 | 25 profiles + multiplex + Honcho shared memory active | Admin |
| Day 3 | Google Drive integration (per-user + team SA) | Admin |
| Day 4 | Team rollout — all 25 users onboarded | Admin + Users |
| Day 5+ | Backups, monitoring, cost tracking, refinements | Admin |