hermes-proton/ARCHITECTURE.md
Hannibal Smith 8fdf219337
architecture: Hermes-Proton multi-layer integration design
Complete ARCHITECTURE.md covering:
1. Komodo plugin spec: shared SRP-6a auth, token lifecycle, encrypted store
2. Hermes skill specs: Mail (Bridge), Pass (pass-cli), Drive (rclone), VPN (vpn-cli)
3. MCP tool server: Python stdio MCP server for non-Hermes agents
4. Auth flow: single Proton login shared across all skills
5. File layout: monorepo with plugin/skills/mcp-server/tests/
6. Environment/credential management: full env table, encrypted storage format

References: go-proton-api, pass-cli, rclone protondrive, proton-vpn-cli, hydroxide
2026-06-08 18:23:35 +02:00

848 lines
35 KiB
Markdown

# 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 <id> --json` |
| `proton_pass_search` | Search items by name/URL | `proton-pass item search <query> --json` |
| `proton_pass_create` | Create new password item | `proton-pass item create --json` |
| `proton_pass_edit` | Update existing item | `proton-pass item edit <id> --json` |
| `proton_pass_delete` | Delete item | `proton-pass item delete <id>` |
| `proton_pass_totp` | Get TOTP code for item | `proton-pass totp <id>` |
| `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 <query>` |
| `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 <country>` 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 <server>` |
| `proton_vpn_killswitch` | Enable/disable kill switch | `protonvpn-cli killswitch <on/off>` |
| `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*