diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..687c423 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,848 @@ +# 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*