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
35 KiB
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, pass-cli, rclone protondrive, proton-vpn-cli
Table of Contents
- Project Monorepo Layout
- Auth Architecture — Komodo Plugin
- Hermes Skill: Proton Mail
- Hermes Skill: Proton Pass
- Hermes Skill: Proton Drive
- Hermes Skill: Proton VPN
- MCP Tool Server (Optional)
- Environment & Credential Management
- Auth Flow Detail
- 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)
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
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
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
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:
- Plugin hooks
post_tool_callon every Proton skill call - Before dispatching a tool, checks
AccessTokenexpiry (JWT-like) - If within 5 minutes of expiry, proactively refreshes via
POST /api/core/v4/auth/refresh - On 401 from any call, triggers immediate refresh and retries
- On 422 (invalid grant), transitions to
NO_AUTH, blocks all skills, surfaces re-auth prompt
Storage:
- Encrypted at rest via
gopenpgpor a symmetric AES-GCM key derived from a local master password - Stored at
$XDG_DATA_HOME/hermes-proton/session.encby 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:
@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):
# 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,
--climode), Pythonimaplib/smtplib(stdlib), ornode-imap/nodemailerif 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
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:
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=hydroxideenv 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
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_injectsets 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:
rclonewith 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
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_readshould stream to stdout rather than loading into memory proton_drive_downloadtargets 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:
# 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-appor the CLI-only variant) - Auth: Independent —
protonvpn-cli initstores 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
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:
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/sdkfor MCP server - Wraps Bridge IMAP/SMTP via
node-imapandnodemailer - 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
mcpPyPI 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)
# 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:
# ~/.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
{
"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):
{
"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:
{
"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:
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 |
|---|---|---|---|---|---|
| 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