# Hermes-Proton Architecture > **Multi-layer integration design** — Hermes Agent runtime + Proton product suite > (Mail, Pass, Drive, VPN, Calendar) > > Target: Hermes Agent 0.x (komodo plugin system, Hermes skills, MCP tools) > References: README.md (research report), [go-proton-api](https://github.com/ProtonMail/go-proton-api), > [pass-cli](https://protonpass.github.io/pass-cli/), [rclone protondrive](https://rclone.org/protondrive/), > [proton-vpn-cli](https://github.com/ProtonVPN/linux-cli) --- ## Table of Contents 1. [Project Monorepo Layout](#1-project-monorepo-layout) 2. [Auth Architecture — Komodo Plugin](#2-auth-architecture--komodo-plugin) 3. [Hermes Skill: Proton Mail](#3-hermes-skill-proton-mail) 4. [Hermes Skill: Proton Pass](#4-hermes-skill-proton-pass) 5. [Hermes Skill: Proton Drive](#5-hermes-skill-proton-drive) 6. [Hermes Skill: Proton VPN](#6-hermes-skill-proton-vpn) 7. [MCP Tool Server (Optional)](#7-mcp-tool-server-optional) 8. [Environment & Credential Management](#8-environment--credential-management) 9. [Auth Flow Detail](#9-auth-flow-detail) 10. [Integration Layer Matrix](#10-integration-layer-matrix) --- ## 1. Project Monorepo Layout ``` hermes-proton/ ├── README.md # Research report (already written) ├── ARCHITECTURE.md # This file ├── plugin/ # Komodo plugin — shared auth layer │ ├── plugin.yaml # Manifest │ ├── __init__.py # Registration: tools + hooks │ ├── schemas.py # Auth tool schemas │ ├── tools.py # Auth tool handlers │ ├── auth.py # SRP-6a client, session state machine │ ├── store.py # Encrypted credential store │ ├── token.py # Token lifecycle (refresh, re-auth) │ └── config.py # Plugin-scoped config │ ├── skills/ │ ├── proton-mail/ # Hermes skill (IMAP/SMTP via Bridge) │ │ └── SKILL.md │ ├── proton-pass/ # Hermes skill (pass-cli subprocess) │ │ └── SKILL.md │ ├── proton-drive/ # Hermes skill (rclone subprocess) │ │ └── SKILL.md │ └── proton-vpn/ # Hermes skill (vpn-cli subprocess) │ └── SKILL.md │ ├── mcp-server/ # Optional MCP server │ ├── package.json # Node.js / Python? │ └── src/ │ ├── index.ts # MCP server entry │ ├── proton-mail.ts # Mail MCP tools │ ├── proton-pass.ts # Pass MCP tools │ ├── proton-drive.ts # Drive MCP tools │ └── proton-vpn.ts # VPN MCP tools │ ├── scripts/ │ ├── install-plugin.sh # one-command plugin install │ └── install-skills.sh # one-command skill install │ ├── tests/ │ ├── test_auth.py # Auth flow tests │ ├── test_mail.py # Mail skill tests │ ├── test_pass.py # Pass skill tests │ ├── test_drive.py # Drive skill tests │ └── test_vpn.py # VPN skill tests │ ├── .env.example # Stub env file (no secrets) ├── .gitignore ├── LICENSE └── hermes-proton.code-workspace # Optional VS Code workspace ``` ### Layout Rationale | Directory | Purpose | Why separate? | |-----------|---------|---------------| | `plugin/` | Auth layer — SRP-6a session, encrypted store | Single plugin owns shared auth; skills never touch Proton API directly | | `skills/` | Product-specific Hermes skills | Each skill maps to one Proton product; clean isolation | | `mcp-server/` | MCP compatibility layer | Optional; only needed for non-Hermes MCP clients | | `scripts/` | Install/setup helpers | One-command deployment for Hermes users | | `tests/` | Integration & unit tests | Auth tests mock the Proton API; skill tests mock subprocess | --- ## 2. Auth Architecture — Komodo Plugin ### 2.1 Role of the Plugin The `hermes-proton` komodo plugin is the **single authority for Proton authentication**. No skill talks directly to the Proton REST API. The plugin: - Handles the SRP-6a handshake once at login - Encrypts and stores session tokens (UID, AccessToken, RefreshToken) - Manages token refresh lifecycle (server-delivered + proactive refresh) - Provides auth context to skills via plugin-internal API - Detects session expiry and triggers re-authentication ### 2.2 Plugin Manifest (`plugin/plugin.yaml`) ```yaml name: hermes-proton version: 1.0.0 description: Shared authentication layer for Proton services — SRP-6a session management, encrypted token storage, token refresh provides_tools: - proton_auth_login - proton_auth_status - proton_auth_logout provides_hooks: - post_tool_call requires_env: - PROTON_USERNAME - PROTON_PASSWORD # Only for initial login; cached after - PROTON_CREDENTIAL_STORE # Optional: path to encrypted store - PROTON_API_URL # Optional: default https://api.protonmail.ch ``` ### 2.3 Tool Schemas (`plugin/schemas.py`) **`proton_auth_login`** ```python PROTON_AUTH_LOGIN = { "name": "proton_auth_login", "description": ( "Authenticate with Proton using SRP-6a. Uses PROTON_USERNAME from env. " "Password is prompted interactively or read from encrypted store. " "Returns session status. Subsequent calls use cached tokens." ), "parameters": { "type": "object", "properties": { "password": { "type": "string", "description": "Proton password (optional if stored)" }, "force_reauth": { "type": "boolean", "description": "Force re-authentication even if tokens exist" } } } } ``` **`proton_auth_status`** ```python PROTON_AUTH_STATUS = { "name": "proton_auth_status", "description": ( "Check Proton session status. Returns whether authenticated, " "token expiry, and which products are available." ), "parameters": {} } ``` **`proton_auth_logout`** ```python PROTON_AUTH_LOGOUT = { "name": "proton_auth_logout", "description": ( "Log out of Proton. Clears cached tokens and encrypted store. " "Revokes refresh token with Proton API." ), "parameters": { "type": "object", "properties": { "revoke_all": { "type": "boolean", "description": "Revoke all sessions (remote too)" } } } } ``` ### 2.4 Token Lifecycle (`plugin/token.py`) ``` State machine: ┌──────────┐ first-login ┌────────────┐ │ NO_AUTH ├─────────────────►│ AUTHED │ └──────────┘ └─────┬──────┘ ▲ │ │ refresh fails │ token expires │ ┌──────────────────┐ │ │ │ REFRESHING │◄────┘ │ └────────┬─────────┘ │ │ refresh succeeds │ ▼ │ ┌────────────┐ └────┤ AUTHED │ (new tokens) └────────────┘ ``` **Refresh mechanism:** 1. Plugin hooks `post_tool_call` on every Proton skill call 2. Before dispatching a tool, checks `AccessToken` expiry (JWT-like) 3. If within 5 minutes of expiry, proactively refreshes via `POST /api/core/v4/auth/refresh` 4. On 401 from any call, triggers immediate refresh and retries 5. On 422 (invalid grant), transitions to `NO_AUTH`, blocks all skills, surfaces re-auth prompt **Storage:** - Encrypted at rest via `gopenpgp` or a symmetric AES-GCM key derived from a local master password - Stored at `$XDG_DATA_HOME/hermes-proton/session.enc` by default - Plaintext fields: `UID`, `AccessToken`, `RefreshToken`, `ServerProof`, `KeySalt`, `PasswordSalt`, token expiry timestamps ### 2.5 Credential Store (`plugin/store.py`) The credential store wraps an encrypted JSON file: ```python @dataclass class ProtonCredentialStore: path: Path # e.g. ~/.local/share/hermes-proton/session.enc master_key: bytes # Derived from PROTON_PASSWORD env or keyring def save(self, session: ProtonSession) -> None def load(self) -> ProtonSession | None def clear(self) -> None def has_valid_session(self) -> bool ``` **Encryption scheme:** ``` master_key = argon2id( password=PROTON_PASSWORD or keyring_password, salt=random_salt, time_cost=3, mem_cost=65536, parallelism=4 ) nonce = random(12) ciphertext = aes256_gcm_encrypt(plaintext=json(session), key=master_key, nonce=nonce) store = base64(nonce + ciphertext + tag) ``` ### 2.6 Plugin API for Skills The plugin exposes an in-process API for skills (not exposed as tools): ```python # Plugin internal API (consumed by skills via plugin context) class ProtonAuthAPI: def get_session(self) -> ProtonSession | None def get_authenticated_client(self) -> proton.Client | None def is_authenticated(self) -> bool def require_auth(self) -> ProtonSession # raises if not authenticated ``` Skills call `require_auth()` at the top of every handler. If the plugin returns a session, the skill proceeds; if not, the skill returns an error prompting the user to run `proton_auth_login` first. --- ## 3. Hermes Skill: Proton Mail ### 3.1 Strategy **Path:** Proton Bridge → local IMAP/SMTP Proton Bridge is the recommended integration vehicle for Mail. It runs as a local daemon exposing: - IMAP on `127.0.0.1:1143` (read) - SMTP on `127.0.0.1:1025` (send) - gRPC API on `127.0.0.1:1042` (management) - OpenPGP crypto is handled transparently by the Bridge — no key management in the skill ### 3.2 Skill Metadata - **Name:** `proton-mail` - **Category:** `email` - **Dependencies:** Proton Bridge (headless, `--cli` mode), Python `imaplib` / `smtplib` (stdlib), or `node-imap` / `nodemailer` if the Bridge gRPC interface is preferred - **Auth:** Relies on plugin for session; Bridge has its own local auth (TLS cert) ### 3.3 Tool Definitions | Tool | Description | Implementation | |------|-------------|----------------| | `proton_mail_list` | List inbox messages (folder, limit, page) | IMAP SEARCH / FETCH | | `proton_mail_read` | Read full message by ID | IMAP FETCH (RFC822) | | `proton_mail_search` | Search messages by subject/from/body/date | IMAP SEARCH (CHARSET UTF-8) | | `proton_mail_send` | Send email (to, subject, body, attachments) | SMTP via localhost:1025 | | `proton_mail_reply` | Reply to message by ID (thread support) | SMTP + In-Reply-To/References | | `proton_mail_draft` | Save draft | IMAP APPEND to Drafts | | `proton_mail_folders` | List mailbox folders | IMAP LIST | | `proton_mail_move` | Move message between folders | IMAP MOVE / COPY + STORE \Deleted | ### 3.4 Implementation Pattern ```python def proton_mail_read(args: dict, **kwargs) -> str: # 1. Verify Bridge is running (systemctl check or process check) # 2. Connect to IMAP localhost:1143 (TLS) # 3. FETCH the message by UID # 4. Parse MIME (body text/HTML, attachments) # 5. Return structured JSON ``` ### 3.5 Bridge Management The skill should also expose a management tool confirming Bridge is running: ```python PROTON_MAIL_BRIDGE_STATUS = { "name": "proton_mail_bridge_status", "description": "Check Proton Bridge status — running, authenticated, connected to Proton servers." } ``` ### 3.6 Fallback: hydroxide If the official Bridge is unavailable (e.g. macOS or headless server without GUI), the skill should support **hydroxide** (`emersion/hydroxide`) as an alternative: - Implements SRP-6a natively (no GUI required) - Exposes same IMAP/SMTP surface - Configurable via `PROTON_MAIL_BRIDGE_TYPE=hydroxide` env var --- ## 4. Hermes Skill: Proton Pass ### 4.1 Strategy **Path:** Official `pass-cli` → subprocess with `--json` output `pass-cli` v2.1+ supports `--json` output on all commands, making it trivially machine-readable. No auth management needed — pass-cli handles its own token lifecycle via its own offline-first encrypted vault. ### 4.2 Skill Metadata - **Name:** `proton-pass` - **Category:** `productivity` - **Dependencies:** `proton-pass-cli` (Rust binary, installed separately) - **Auth:** Independent — pass-cli manages its own session/session-key via `proton-pass auth login` ### 4.3 Tool Definitions | Tool | Description | CLI command | |------|-------------|-------------| | `proton_pass_list` | List vaults and items | `proton-pass item list --json` | | `proton_pass_get` | Get secret by item ID | `proton-pass item get --json` | | `proton_pass_search` | Search items by name/URL | `proton-pass item search --json` | | `proton_pass_create` | Create new password item | `proton-pass item create --json` | | `proton_pass_edit` | Update existing item | `proton-pass item edit --json` | | `proton_pass_delete` | Delete item | `proton-pass item delete ` | | `proton_pass_totp` | Get TOTP code for item | `proton-pass totp ` | | `proton_pass_inject` | Inject secret as env var (SSH agent support) | Reads + sets env | | `proton_pass_vaults` | List vaults | `proton-pass vault list --json` | ### 4.4 Implementation Pattern ```python def proton_pass_get(args: dict, **kwargs) -> str: item_id = args["item_id"] result = subprocess.run( ["proton-pass", "item", "get", item_id, "--json"], capture_output=True, text=True, timeout=10 ) if result.returncode != 0: return json.dumps({"error": result.stderr.strip()}) return result.stdout # already JSON ``` ### 4.5 Security Considerations - Passwords returned by pass-cli enter the agent's context. This is expected (the agent needs them to use them), but users should be warned. - `proton_pass_inject` sets temporary env vars for subprocesses and scrubs them after — prevents secret leakage across turns. - SSH agent support via `pass-cli`'s built-in SSH key injection. --- ## 5. Hermes Skill: Proton Drive ### 5.1 Strategy **Primary Path:** rclone protondrive backend → subprocess **Alternative Path:** Drive SDK (TypeScript, preview) rclone's protondrive backend is the most battle-tested option. It handles Proton's non-standard API, encryption/decryption, and chunked uploads transparently. The skill shells out to `rclone` with protondrive remote configured. ### 5.2 Skill Metadata - **Name:** `proton-drive` - **Category:** `productivity` - **Dependencies:** `rclone` with protondrive backend configured (`rclone config create protondrive ...`) - **Auth:** Relies on rclone's token storage (rclone handles its own refresh) ### 5.3 Tool Definitions | Tool | Description | CLI command | |------|-------------|-------------| | `proton_drive_list` | List files/folders in path | `rclone ls protondrive:path/` or `rclone lsf --json` | | `proton_drive_read` | Read file content (text/small binary) | `rclone cat protondrive:path/file` | | `proton_drive_download` | Download file to local path | `rclone copy protondrive:path/file local/` | | `proton_drive_upload` | Upload local file to Drive | `rclone copy local/file protondrive:path/` | | `proton_drive_search` | Search files by name | `rclone lsf -R --files-only protondrive: \| grep -i ` | | `proton_drive_mkdir` | Create folder | `rclone mkdir protondrive:path/` | | `proton_drive_delete` | Delete file | `rclone delete protondrive:path/file` | | `proton_drive_stat` | Get file/bucket metadata | `rclone lsl protondrive:path/file` | | `proton_drive_sync` | Two-way sync (with confirmation) | `rclone sync ...` | ### 5.4 Implementation Pattern ```python def proton_drive_list(args: dict, **kwargs) -> str: path = args.get("path", "/") result = subprocess.run( ["rclone", "lsf", "--json", f"protondrive:{path}"], capture_output=True, text=True, timeout=30 ) if result.returncode != 0: return json.dumps({"error": result.stderr.strip()}) items = [json.loads(line) for line in result.stdout.strip().split("\n") if line] return json.dumps({"items": items}) ``` ### 5.5 Large File Considerations - For files over 10MB, `proton_drive_read` should stream to stdout rather than loading into memory - `proton_drive_download` targets a temp path and returns the local path - rclone handles chunking and retry automatically ### 5.6 Drive SDK Alternative If rclone proves unreliable for specific operations (notably incremental sync or finer-grained metadata), fall back to the TypeScript Drive SDK: ```python # Concept: Drive SDK wrapper via node subprocess # Only if rclone protondrive is insufficient PROTON_DRIVE_USE_SDK = os.getenv("PROTON_DRIVE_USE_SDK", "false") ``` --- ## 6. Hermes Skill: Proton VPN ### 6.1 Strategy **Path:** Official `proton-vpn-cli` → subprocess `protonvpn-cli` (Python) is the official Linux CLI. It manages its own auth (Proton API credentials stored in `~/.cache/protonvpn-cli/`). The skill wraps connect/disconnect/status as tools. ### 6.2 Skill Metadata - **Name:** `proton-vpn` - **Category:** `infra` - **Dependencies:** `protonvpn-cli` (PyPI: `pip install proton-vpn-gtk-app` or the CLI-only variant) - **Auth:** Independent — `protonvpn-cli init` stores credentials ### 6.3 Tool Definitions | Tool | Description | CLI command | |------|-------------|-------------| | `proton_vpn_connect` | Connect to fastest/selected server | `protonvpn-cli connect ` or `--fastest` | | `proton_vpn_disconnect` | Disconnect VPN | `protonvpn-cli disconnect` | | `proton_vpn_status` | Get connection status | `protonvpn-cli status --json` | | `proton_vpn_servers` | List available servers/countries | `protonvpn-cli list --json` | | `proton_vpn_connect_to` | Connect to specific server | `protonvpn-cli connect ` | | `proton_vpn_killswitch` | Enable/disable kill switch | `protonvpn-cli killswitch ` | | `proton_vpn_config` | Show current config | `protonvpn-cli config --json` | ### 6.4 Implementation Pattern ```python def proton_vpn_connect(args: dict, **kwargs) -> str: server = args.get("server", "--fastest") result = subprocess.run( ["protonvpn-cli", "connect", server], capture_output=True, text=True, timeout=30 ) if result.returncode != 0: return json.dumps({"error": result.stderr.strip()}) return json.dumps({"status": "connected", "server": server}) ``` ### 6.5 Privilege Management `protonvpn-cli` typically requires root or a `protonvpn` group membership for WireGuard interface creation. The skill should check at init: ```python def _check_vpn_privileges(): user = os.getenv("USER") groups = subprocess.run(["groups"], capture_output=True, text=True).stdout if "protonvpn" not in groups and os.geteuid() != 0: return json.dumps({"error": "User not in protonvpn group or not root. Run: sudo usermod -aG protonvpn $USER"}) return None ``` --- ## 7. MCP Tool Server (Optional) ### 7.1 Purpose An MCP (Model Context Protocol) server exposes Proton services to **any MCP-compatible agent**, not just Hermes. This includes Claude Code, Cursor, VS Code ACP, and custom MCP clients. ### 7.2 Architecture ``` ┌──────────────────────────────┐ │ MCP Client (any agent) │ │ Claude Code / Cursor / etc. │ └──────────┬───────────────────┘ │ MCP protocol (stdio or HTTP) ┌──────────┴───────────────────┐ │ MCP Server: hermes-proton │ │ (Node.js or Python) │ │ │ │ proton-mail-read │ │ proton-drive-sync │ │ proton-vpn-connect │ │ proton-pass-get │ └──────────┬───────────────────┘ │ ┌──────────┴───────────────────┐ │ Backend (same as skills) │ │ Bridge / pass-cli / rclone │ │ / vpn-cli / plugin auth │ └──────────────────────────────┘ ``` ### 7.3 Tool Exports The MCP server exposes a curated subset of skills as MCP tools: | MCP Tool | Maps To | Notes | |----------|---------|-------| | `proton_mail_list` | Mail skill → IMAP | Inbox list — most useful for agents | | `proton_mail_read` | Mail skill → IMAP | Full message content | | `proton_mail_send` | Mail skill → SMTP | Send via Bridge | | `proton_mail_search` | Mail skill → IMAP | Search mailbox | | `proton_pass_get` | Pass skill → pass-cli | Get secret (**caution: context exposure**) | | `proton_pass_list` | Pass skill → pass-cli | List without secret values | | `proton_drive_read` | Drive skill → rclone | Read file content | | `proton_drive_list` | Drive skill → rclone | List directory | | `proton_drive_search` | Drive skill → rclone | Find files | | `proton_vpn_connect` | VPN skill → vpn-cli | Connect VPN | | `proton_vpn_disconnect` | VPN skill → vpn-cli | Disconnect | | `proton_vpn_status` | VPN skill → vpn-cli | Status check | ### 7.4 Implementation Options **Option A: Node.js (TypeScript)** - Uses `@modelcontextprotocol/sdk` for MCP server - Wraps Bridge IMAP/SMTP via `node-imap` and `nodemailer` - Shells out to pass-cli and rclone via `child_process` - Pros: MCP SDK is first-class in Node.js ecosystem - Cons: Another runtime dependency **Option B: Python** - Uses `mcp` PyPI package (`pip install mcp`) - Reuses Python implementations from Hermes skills directly - Pros: No runtime switch; skills and MCP share code - Cons: Python MCP ecosystem less mature than Node.js **Recommendation: Python first.** The skills are written in Python. The MCP server should reuse the same code paths. If Python MCP proves problematic, fall back to Node.js. ### 7.5 MCP Server Design (Python) ```python # mcp-server/src/server.py from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent class ProtonMCP(Server): def __init__(self): super().__init__("hermes-proton") self.auth = ProtonAuthAPI() self.mail = MailSkill(auth=self.auth) self.drive = DriveSkill(auth=self.auth) self.pass_ = PassSkill(auth=self.auth) self.vpn = VPNService() app = ProtonMCP() @app.list_tools() async def list_tools() -> list[Tool]: return [ Tool(name="proton_mail_list", ...), Tool(name="proton_mail_read", ...), # ... etc ] @app.call_tool() async def call_tool(name: str, arguments: dict): if name == "proton_mail_list": result = app.mail.list_mail(**arguments) return [TextContent(content=json.dumps(result))] # ... etc if __name__ == "__main__": import anyio anyio.run(stdio_server, app) ``` ### 7.6 MCP Config for Hermes When the MCP server is installed, Hermes can connect via its built-in MCP client: ```yaml # ~/.hermes/config.yaml mcp_servers: proton: command: python args: ["/path/to/hermes-proton/mcp-server/src/server.py"] ``` Then all MCP tools appear alongside Hermes tools automatically. --- ## 8. Environment & Credential Management ### 8.1 Environment Variables | Variable | Required | Purpose | Default | |----------|----------|---------|---------| | `PROTON_USERNAME` | **Yes** | Proton account email/username | — | | `PROTON_PASSWORD` | Optional | Password (prompted if absent) | — | | `PROTON_CREDENTIAL_STORE` | No | Path to encrypted session store | `~/.local/share/hermes-proton/session.enc` | | `PROTON_API_URL` | No | Proton API base URL | `https://api.protonmail.ch` | | `PROTON_MAIL_BRIDGE_TYPE` | No | `bridge` (official) or `hydroxide` | `bridge` | | `PROTON_DRIVE_USE_SDK` | No | Use Drive SDK instead of rclone | `false` | | `PROTON_BRIDGE_PASSFILE` | No | Bridge password file path | Auto-generated | | `PROTON_BRIDGE_PORT_IMAP` | No | Bridge IMAP port | `1143` | | `PROTON_BRIDGE_PORT_SMTP` | No | Bridge SMTP port | `1025` | | `PROTON_VPN_CLI_PATH` | No | Path to protonvpn-cli binary | Auto-detected | | `PROTON_PASS_CLI_PATH` | No | Path to pass-cli binary | Auto-detected | | `PROTON_RCLONE_REMOTE` | No | rclone remote name for protondrive | `protondrive` | ### 8.2 Credential Store Design ``` Storage chain: 1. PROTON_USERNAME (from env) ← minimal env config 2. Plugin prompts for password ← first login 3. Plugin encrypts tokens via AES-GCM 4. Writes to ~/.local/share/hermes-proton/session.enc 5. Subsequent sessions auto-load encrypted tokens 6. Re-auth only needed on: expiry, password change, revocation ``` ### 8.3 Encrypted Storage Format ```json { "version": 1, "created_at": "2026-06-08T...", "username": "user@proton.me", "encrypted_data": "base64...", // AES-256-GCM encrypted payload "iv": "base64...", "salt": "base64...", "auth_tag": "base64..." } ``` The inner plaintext (before encryption): ```json { "uid": "proton-uid", "access_token": "...", "refresh_token": "...", "server_proof": "...", "key_salt": "...", "password_salt": "...", "expires_at": 1780936666, "product_access": ["mail", "pass", "drive", "vpn"] } ``` ### 8.4 Multi-Account Support (Future) The credential store supports multiple accounts via a key rotation scheme: ```json { "version": 1, "active_account": "user@proton.me", "accounts": { "user@proton.me": { "encrypted_data": "...", ... }, "other@pm.me": { "encrypted_data": "...", ... } } } ``` Skill tools accept an optional `account` parameter to switch contexts. --- ## 9. Auth Flow Detail ### 9.1 Full Login Sequence ``` Agent Plugin Proton API │ │ │ │ proton_auth_login() │ │ │───────────────────────►│ │ │ │ GET /api/core/v4/auth/info │ │ │──────────────────────────────►│ │ │◄──────────────────────────────│ │ │ {Modulus, ModulusID, ServerEphemeral, │ │ Salt, Version} │ │ │ │ │ │ SRP client proof: │ │ │ clientProof = SRP( │ │ │ password, salt, modulus │ │ │ ) │ │ │ │ │ │ POST /api/core/v4/auth │ │ │ {username, clientEphemeral, │ │ │ clientProof, SRPSession} │ │ │──────────────────────────────►│ │ │◄──────────────────────────────│ │ │ {UID, AccessToken, │ │ │ RefreshToken, ServerProof} │ │ │ │ │ │ Encrypt tokens, store on disk│ │ │ Set plugin state = AUTHED │ │ │ │ │ {status: "authenticated", products: [...], expiry} │ │◄───────────────────────│ │ ``` ### 9.2 Token Refresh Sequence ``` Skill call Plugin Proton API │ │ │ │ skill handler calls require_auth│ │ │─────────────────────────────────►│ │ │ │ Check token expiry │ │ │ ├─ fresh → return │ │ │ └─ stale → refresh │ │ │ │ │ │ POST /auth/refresh │ │ │──────────────────────►│ │ │◄──────────────────────│ │ │ {AccessToken, │ │ │ RefreshToken, expiry}│ │ │ │ │ │ Re-encrypt, store │ │ │ │ │ session (refreshed) │ │ │◄─────────────────────────────────│ │ ``` ### 9.3 Session-Expired Sequence ``` Skill call → require_auth() → plugin checks tokens → valid → API call returns 401 → plugin attempts refresh → 422 (invalid grant) → plugin transitions to NO_AUTH → returns error: "Session expired. Run proton_auth_login to re-authenticate." ``` ### 9.4 Auth Lifecycle Hooks The plugin registers a `post_tool_call` hook to detect 401s on any skill's Proton API calls: ```python def _on_post_tool_call(tool_name, args, result, task_id, **kwargs): if result and "401" in result or "token_expired" in result: auth_state = _get_plugin_auth_state() if auth_state == "AUTHED": _schedule_background_refresh() elif result and "422" in result or "invalid_grant" in result: _set_auth_state("NO_AUTH") ``` --- ## 10. Integration Layer Matrix ### 10.1 How Each Product Connects | Product | Primary Path | Auth Owner | Protocol | Maturity | Risks | |---------|-------------|------------|----------|----------|-------| | **Mail** | Bridge (IMAP/SMTP) | Bridge app password | TCP + TLS | **Proven** — OpenClaw uses this | Bridge requires display (GUI) for initial login; `--cli` mode may be flaky | | **Mail (fallback)** | hydroxide (IMAP/SMTP) | hydroxide SRP | TCP + TLS | **Stable** — 2.1k stars | No official support; slower development | | **Pass** | pass-cli (Rust) | pass-cli self | stdio | **Mature** — v2.1.x | Secrets in agent context; SSH agent integration needs care | | **Drive** | rclone protondrive | rclone OAuth-like token | subprocess | **Beta** — most-used 3rd party | rclone sync semantics may not match Drive UX | | **Drive (alt)** | Drive SDK (TS) | go-proton-api session | HTTP | **Preview** — breaking crypto changes | Unstable API; TypeScript dependency | | **VPN** | protonvpn-cli | protonvpn self | subprocess | **Mature** | Requires `protonvpn` group / sudo | | **Calendar** | go-proton-api (direct) | Plugin session | HTTP | **Exploratory** | Calendar endpoints exist but underdocumented | | **Wallet** | None | N/A | N/A | **None** | No public API | ### 10.2 Auth Responsibility Summary | Component | Auth Type | Owns Tokens? | Can Re-auth? | |-----------|-----------|--------------|--------------| | **Plugin** | SRP-6a | Yes — master session tokens | Yes — full SRP login flow | | **Bridge** | Bridge app password | Yes — separate Bridge cred | No — must be re-authed via Proton login | | **pass-cli** | pass-cli session key | Yes — encrypted local vault | Yes — `proton-pass auth login` | | **rclone** | protondrive token | Yes — rclone config file | Yes — `rclone config reconnect` | | **vpn-cli** | Proton API credentials | Yes — `~/.cache/protonvpn-cli/` | Yes — `protonvpn-cli init` | ### 10.3 Which Extension Mechanism When | Scenario | Use | |----------|-----| | Agent needs to read/send mail | `proton-mail` Hermes skill | | Agent needs to retrieve secrets | `proton-pass` Hermes skill | | Agent needs to access stored files | `proton-drive` Hermes skill | | Agent needs VPN connect/disconnect | `proton-vpn` Hermes skill | | Multiple skills need auth | `hermes-proton` komodo plugin | | Non-Hermes MCP agent needs Proton | `mcp-server` with Python stdio | | VS Code / Cursor agent needs Proton | MCP server (same as above) | --- ## Appendix A: Risk Register | Risk | Impact | Mitigation | Owner | |------|--------|------------|-------| | Bridge requires display for initial login | High — headless servers can't auth Bridge | Use `protonmail-bridge --cli` with passfile; fallback to hydroxide | Phase 2 | | Proton API changes (breaking) | High — all products break | Wrap each API call in version negotiation; log API version at login | Ongoing | | pass-cli secrets in agent context | Medium — leakage through prompt logging | Warn user; never log raw secret output; offer `inject` mode | Phase 3 | | rclone protondrive backend removed | Medium — Drive breaks | Abstract rclone behind an interface; implement SDK fallback | Phase 4 | | Token refresh race condition | Medium — two skills refresh simultaneously | Plugin uses a `asyncio.Lock` or `threading.Lock` around refresh | Phase 1 | | VPN requires sudo | Low — user friction | Document `protonvpn` group setup in skill; check at tool dispatch | Phase 5 | | MCP server version mismatch | Low — tools drift from skills | MCP server imports skills directly; always in lockstep | Phase 6 | --- ## Appendix B: Implementation Order ``` Phase 1 (Foundation) ───► Project scaffold + ARCHITECTURE.md Phase 2 (Mail) ───► Bridge install → proton-mail skill → IMAP/SMTP tools Phase 3 (Pass) ───► pass-cli install → proton-pass skill → vault tools Phase 4 (Drive) ───► rclone config → proton-drive skill → file tools Phase 5 (VPN) ───► vpn-cli install → proton-vpn skill → network tools Phase 6 (Auth Plugin) ───► Komodo plugin → SRP-6a login → encrypted store → shared session Phase 7 (MCP Server) ───► Python MCP server → expose tools to non-Hermes agents Phase 8 (Calendar) ───► go-proton-api Calendar endpoints (exploratory) ``` Auth plugin is Phase 6 (after all skills exist independently) because early phases can prototype skill-level auth independently. Plugin consolidation happens once patterns are proven. --- *Last updated: 2026-06-08* *Author: Colonel Hannibal — A-Team Architecture*