#!/usr/bin/env python3 """ Proton Pass Hermes skill — tool implementations. All tools shell out to the official `pass-cli` Rust binary and use `--output json` (or `--output=json`) for machine-readable output. Requires: pass-cli >= 2.1.0 (official Proton Pass CLI) See SKILL.md for installation and setup instructions. """ import json import os import shlex import subprocess from typing import Any # ── helpers ──────────────────────────────────────────────────────────── def _get_pass_binary() -> str: """Return the pass-cli binary path, respecting PROTON_PASS_CLI_PATH.""" return os.environ.get("PROTON_PASS_CLI_PATH", "pass-cli") def _run_pass(args: list[str], timeout: int = 15) -> dict: """ Run a `pass-cli` command and return a structured result dict. Uses --output=json where applicable. Returns parsed JSON output when the CLI emits JSON, or a structured wrapper for non-JSON output. """ binary = _get_pass_binary() cmd = [binary] + args try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=timeout ) out = result.stdout.strip() err = result.stderr.strip() if result.returncode != 0: # pass-cli often prints errors to stdout too error_msg = err or out or f"Exit code {result.returncode}" return { "success": False, "exit_code": result.returncode, "error": error_msg, "command": " ".join(shlex.quote(s) for s in cmd), } # Try to parse JSON output if out: try: parsed = json.loads(out) return { "success": True, "data": parsed, "command": " ".join(shlex.quote(s) for s in cmd), } except json.JSONDecodeError: pass return { "success": True, "output": out, "command": " ".join(shlex.quote(s) for s in cmd), } except subprocess.TimeoutExpired: return { "success": False, "error": f"Command timed out after {timeout}s: {' '.join(shlex.quote(s) for s in cmd)}", } except FileNotFoundError: return { "success": False, "error": ( f"{binary} not found. Install via:\n" " curl -fsSL https://proton.me/download/pass-cli/install.sh | bash\n" "Or set PROTON_PASS_CLI_PATH env var to the binary path." ), } except OSError as exc: return { "success": False, "error": str(exc), } def _ensure_json_args(args: list[str]) -> list[str]: """Append --output=json if not already present and no --output is there.""" if any(a.startswith("--output") for a in args): return args return args + ["--output", "json"] # ── auth handlers ───────────────────────────────────────────────────── def proton_pass_login(args: dict) -> dict: """ Authenticate with Proton Pass. Supports: interactive login (browser), interactive with credentials, and personal access token login. """ mode = args.get("mode", "interactive") username = args.get("username", "") if mode == "pat": token = args.get("token", "") if not token: return {"success": False, "error": "token required for PAT login mode"} cmd = _run_pass(["login", "--personal-access-token", token], timeout=30) elif mode == "interactive": cmd_args = ["login", "--interactive"] if username: cmd_args.append(username) cmd = _run_pass(cmd_args, timeout=60) elif mode == "web": cmd = _run_pass(["login"], timeout=60) else: return {"success": False, "error": f"Unknown login mode: {mode}"} if not cmd["success"]: return cmd return { "success": True, "message": "Authentication successful. Session established.", "output": cmd.get("output", cmd.get("data", "")), "note": ( "Interactive and web login modes open a browser for authentication. " "PAT mode is recommended for automation (CI/CD, headless)." ), } def proton_pass_logout(args: dict) -> dict: """Log out from Proton Pass, ending the current session.""" cmd = _run_pass(["logout"]) if not cmd["success"]: return cmd return { "success": True, "message": "Logged out. Session cleared.", } def proton_pass_auth_status(args: dict) -> dict: """Check current Proton Pass authentication status.""" cmd = _run_pass(["info"]) if not cmd["success"]: # Not logged in is a common case — return structured, not error return { "success": True, "authenticated": False, "message": "Not authenticated. Run proton_pass_login first.", } output = cmd.get("output", "") or json.dumps(cmd.get("data", {})) # Parse the info output to extract user details return { "success": True, "authenticated": True, "info": cmd.get("data") or {"output": output}, "raw_output": output, } def proton_pass_test(args: dict) -> dict: """Test the pass-cli connection and authentication.""" cmd = _run_pass(["test"]) if not cmd["success"]: return { "success": True, "passed": False, "message": cmd.get("error", "Connection test failed"), } return { "success": True, "passed": True, "message": cmd.get("output", cmd.get("data", "Connection OK")), } # ── vault handlers ──────────────────────────────────────────────────── def proton_pass_vaults(args: dict) -> dict: """List all accessible Proton Pass vaults.""" cmd = _run_pass(_ensure_json_args(["vault", "list"]), timeout=15) if not cmd["success"]: return cmd vaults = cmd.get("data", cmd.get("output", [])) if isinstance(vaults, str): # Non-JSON output fallback return {"success": True, "vaults": [{"name": vaults}]} return { "success": True, "vaults": vaults if isinstance(vaults, list) else [vaults], "total": len(vaults) if isinstance(vaults, list) else 1, } def proton_pass_vault_create(args: dict) -> dict: """Create a new Proton Pass vault.""" name = args.get("name", "") if not name: return {"success": False, "error": "name is required"} cmd = _run_pass(["vault", "create", "--name", name], timeout=15) if not cmd["success"]: return cmd return { "success": True, "message": f"Vault '{name}' created.", "output": cmd.get("output", ""), } def proton_pass_vault_delete(args: dict) -> dict: """Delete a vault and all its contents.""" share_id = args.get("share_id", "") vault_name = args.get("vault_name", "") if not share_id and not vault_name: return {"success": False, "error": "share_id or vault_name required"} cmd_args = ["vault", "delete"] if share_id: cmd_args.extend(["--share-id", share_id]) else: cmd_args.extend(["--vault-name", vault_name]) cmd = _run_pass(cmd_args, timeout=15) if not cmd["success"]: return cmd return { "success": True, "message": f"Vault deleted (permanent).", "output": cmd.get("output", ""), } # ── item handlers ───────────────────────────────────────────────────── def proton_pass_list(args: dict) -> dict: """ List items in a vault. Optionally filter by vault name, share ID, or output format. """ vault_name = args.get("vault_name", "") share_id = args.get("share_id", "") cmd_args = ["item", "list", "--output", "json"] if vault_name and not share_id: cmd_args.append(vault_name) if share_id: cmd_args.extend(["--share-id", share_id]) cmd = _run_pass(cmd_args, timeout=15) if not cmd["success"]: return cmd items = cmd.get("data", cmd.get("output", [])) if isinstance(items, str): return {"success": True, "items": [{"name": items}]} return { "success": True, "items": items if isinstance(items, list) else [items], "total": len(items) if isinstance(items, list) else 1, } def proton_pass_get(args: dict) -> dict: """ Get a specific secret/item from a vault. Requires either (share_id + item_id) or (vault_name + item_title), or a pass:// URI. """ share_id = args.get("share_id", "") vault_name = args.get("vault_name", "") item_id = args.get("item_id", "") item_title = args.get("item_title", "") uri = args.get("uri", "") field = args.get("field", "") cmd_args = ["item", "view", "--output", "json"] if uri: cmd_args.append(uri) else: if not ((share_id or vault_name) and (item_id or item_title)): return { "success": False, "error": ( "Provide (share_id & item_id), (vault_name & item_title), " "or a pass:// URI" ), } if share_id: cmd_args.extend(["--share-id", share_id]) if vault_name: cmd_args.extend(["--vault-name", vault_name]) if item_id: cmd_args.extend(["--item-id", item_id]) if item_title: cmd_args.extend(["--item-title", item_title]) if field: cmd_args.extend(["--field", field]) cmd = _run_pass(cmd_args, timeout=15) if not cmd["success"]: return cmd result = { "success": True, "item": cmd.get("data") or {"output": cmd.get("output", "")}, } if field: result["field"] = field return result def proton_pass_search(args: dict) -> dict: """ Search items across vaults by title or field value. Lists items from specified vault or all vaults, then filters by the search query client-side. """ query = args.get("query", "") vault_name = args.get("vault_name", "") share_id = args.get("share_id", "") if not query: return {"success": False, "error": "query is required"} # Get items from vault (or all vaults) if vault_name or share_id: list_args = {"vault_name": vault_name, "share_id": share_id} list_result = proton_pass_list(list_args) else: # List all vaults first, then collect items from each vaults_result = proton_pass_vaults({}) if not vaults_result.get("success"): return vaults_result vaults = vaults_result.get("vaults", []) all_items = [] for vault in vaults: sid = vault.get("share_id", "") or vault.get("id", "") if sid: vr = proton_pass_list({"share_id": sid}) if vr.get("success"): items = vr.get("items", []) for item in items: item["_vault"] = vault.get("name", sid) all_items.extend(items) list_result = {"success": True, "items": all_items} if not list_result.get("success"): return list_result items = list_result.get("items", []) query_lower = query.lower() # Client-side filtering by title, name, or any string field matches = [] for item in items: if isinstance(item, dict): for key in ("title", "name", "item_name", "id", "item_id"): val = item.get(key, "") if isinstance(val, str) and query_lower in val.lower(): matches.append(item) break elif isinstance(item, str) and query_lower in item.lower(): matches.append(item) return { "success": True, "query": query, "items": matches, "total": len(matches), } def proton_pass_create(args: dict) -> dict: """ Create a new item in a vault. Supports 'login' and 'note' item types by default. """ item_type = args.get("type", "login") vault_name = args.get("vault_name", "") share_id = args.get("share_id", "") title = args.get("title", "") username = args.get("username", "") password = args.get("password", "") generate_password = args.get("generate_password", False) url = args.get("url", "") note = args.get("note", "") if not title: return {"success": False, "error": "title is required"} cmd_args = ["item", "create"] if item_type == "login": cmd_args.append("login") cmd_args.extend(["--title", title]) if username: cmd_args.extend(["--username", username]) if password: cmd_args.extend(["--password", password]) if generate_password: gen_settings = args.get("password_settings", "20,uppercase,symbols") cmd_args.append(f"--generate-password={gen_settings}") if url: cmd_args.extend(["--url", url]) if note: cmd_args.extend(["--field", f"note={note}"]) elif item_type == "note": cmd_args.append("note") cmd_args.extend(["--title", title]) if note: cmd_args.extend(["--note", note]) else: return {"success": False, "error": f"Unsupported item type: {item_type}"} # Target vault if share_id: cmd_args.extend(["--share-id", share_id]) if vault_name: cmd_args.extend(["--vault-name", vault_name]) cmd = _run_pass(cmd_args, timeout=15) if not cmd["success"]: return cmd return { "success": True, "message": f"Item '{title}' created.", "output": cmd.get("output", "") or cmd.get("data", ""), } def proton_pass_edit(args: dict) -> dict: """ Update fields on an existing item. Requires identifiers (share_id/vault_name + item_id/item_title) and one or more fields to update. """ share_id = args.get("share_id", "") vault_name = args.get("vault_name", "") item_id = args.get("item_id", "") item_title = args.get("item_title", "") fields = args.get("fields", {}) if not ((share_id or vault_name) and (item_id or item_title)): return { "success": False, "error": "Provide (share_id & item_id) or (vault_name & item_title)", } if not fields: return {"success": False, "error": "fields dict is required"} cmd_args = ["item", "update"] if share_id: cmd_args.extend(["--share-id", share_id]) if vault_name: cmd_args.extend(["--vault-name", vault_name]) if item_id: cmd_args.extend(["--item-id", item_id]) if item_title: cmd_args.extend(["--item-title", item_title]) for key, value in fields.items(): cmd_args.extend(["--field", f"{key}={value}"]) cmd = _run_pass(cmd_args, timeout=15) if not cmd["success"]: return cmd return { "success": True, "message": f"Item updated ({len(fields)} fields).", "updated_fields": list(fields.keys()), "output": cmd.get("output", ""), } def proton_pass_delete(args: dict) -> dict: """Delete an item from a vault.""" share_id = args.get("share_id", "") vault_name = args.get("vault_name", "") item_id = args.get("item_id", "") item_title = args.get("item_title", "") if not ((share_id or vault_name) and (item_id or item_title)): return { "success": False, "error": "Provide (share_id & item_id) or (vault_name & item_title)", } cmd_args = ["item", "delete"] if share_id: cmd_args.extend(["--share-id", share_id]) if vault_name: cmd_args.extend(["--vault-name", vault_name]) if item_id: cmd_args.extend(["--item-id", item_id]) if item_title: cmd_args.extend(["--item-title", item_title]) cmd = _run_pass(cmd_args, timeout=15) if not cmd["success"]: return cmd return { "success": True, "message": "Item deleted.", "output": cmd.get("output", ""), } # ── secret injection handlers ───────────────────────────────────────── def proton_pass_totp(args: dict) -> dict: """Get the current TOTP code for an item.""" share_id = args.get("share_id", "") vault_name = args.get("vault_name", "") item_id = args.get("item_id", "") item_title = args.get("item_title", "") uri = args.get("uri", "") cmd_args = ["item", "view", "--field", "totp", "--output", "json"] if uri: cmd_args.append(uri) else: if not ((share_id or vault_name) and (item_id or item_title)): return { "success": False, "error": ( "Provide (share_id & item_id), (vault_name & item_title), " "or pass:// URI" ), } if share_id: cmd_args.extend(["--share-id", share_id]) if vault_name: cmd_args.extend(["--vault-name", vault_name]) if item_id: cmd_args.extend(["--item-id", item_id]) if item_title: cmd_args.extend(["--item-title", item_title]) cmd = _run_pass(cmd_args, timeout=10) if not cmd["success"]: return cmd code = cmd.get("output", "") or (cmd.get("data", {}).get("totp", "") if isinstance(cmd.get("data"), dict) else "") return { "success": True, "totp": code.strip(), "item": item_title or item_id or uri, } def proton_pass_inject(args: dict) -> dict: """ Run a command with secrets injected into environment variables. Wraps `pass-cli run` which resolves pass:// URIs in env vars before executing the command. Secrets are masked in stdout/stderr by default. """ command = args.get("command", "") env_files = args.get("env_files", []) no_masking = args.get("no_masking", False) if not command: return {"success": False, "error": "command is required"} cmd_args = ["run"] for env_file in (env_files or []): cmd_args.extend(["--env-file", env_file]) if no_masking: cmd_args.append("--no-masking") cmd_args.append("--") # Split command string into args cmd_args.extend(shlex.split(command)) cmd = _run_pass(cmd_args, timeout=60) if not cmd["success"]: return cmd return { "success": True, "command": command, "output": cmd.get("output", ""), "note": ( "Secrets were injected as environment variables. " "Values are masked in stdout/stderr by default." ), } def proton_pass_run(args: dict) -> dict: """Alias for inject — runs a command with secret-injected env vars.""" return proton_pass_inject(args) # ── SSH agent handlers ──────────────────────────────────────────────── def proton_pass_ssh_load(args: dict) -> dict: """ Load SSH keys from Proton Pass into the system's SSH agent. Requires SSH_AUTH_SOCK to be set. Optionally restrict to a vault. """ share_id = args.get("share_id", "") vault_name = args.get("vault_name", "") if not os.environ.get("SSH_AUTH_SOCK"): return { "success": False, "error": ( "SSH_AUTH_SOCK not set. No SSH agent running. " "Start one with: eval $(ssh-agent)" ), } cmd_args = ["ssh-agent", "load"] if share_id: cmd_args.extend(["--share-id", share_id]) if vault_name: cmd_args.extend(["--vault-name", vault_name]) cmd = _run_pass(cmd_args, timeout=15) if not cmd["success"]: return cmd return { "success": True, "message": cmd.get("output", "SSH keys loaded."), } def proton_pass_ssh_agent_start(args: dict) -> dict: """ Start Proton Pass as the SSH agent (foreground). Sets SSH_AUTH_SOCK to the agent's socket path. Keys load from vaults. """ share_id = args.get("share_id", "") vault_name = args.get("vault_name", "") cmd_args = ["ssh-agent", "start"] if share_id: cmd_args.extend(["--share-id", share_id]) if vault_name: cmd_args.extend(["--vault-name", vault_name]) cmd = _run_pass(cmd_args, timeout=15) if not cmd["success"]: return cmd return { "success": True, "message": cmd.get("output", "SSH agent started."), "note": ( "Set SSH_AUTH_SOCK to the socket path shown above " "to connect other terminals to this agent." ), } def proton_pass_ssh_daemon_start(args: dict) -> dict: """ Start Proton Pass SSH agent as a background daemon. """ share_id = args.get("share_id", "") vault_name = args.get("vault_name", "") log_file = args.get("log_file", "") cmd_args = ["ssh-agent", "daemon", "start"] if share_id: cmd_args.extend(["--share-id", share_id]) if vault_name: cmd_args.extend(["--vault-name", vault_name]) if log_file: cmd_args.extend(["--log-file", log_file]) cmd = _run_pass(cmd_args, timeout=15) if not cmd["success"]: return cmd return { "success": True, "message": cmd.get("output", "SSH agent daemon started."), } def proton_pass_ssh_daemon_status(args: dict) -> dict: """Check the status of the Proton Pass SSH daemon.""" cmd = _run_pass(["ssh-agent", "daemon", "status"], timeout=10) return cmd if not cmd["success"] else { "success": True, "status": cmd.get("output", ""), "data": cmd.get("data"), } def proton_pass_ssh_daemon_stop(args: dict) -> dict: """Stop the Proton Pass SSH agent daemon.""" cmd = _run_pass(["ssh-agent", "daemon", "stop"], timeout=10) if not cmd["success"]: return cmd return { "success": True, "message": cmd.get("output", "SSH agent daemon stopped."), } # ── generic item operations ─────────────────────────────────────────── def proton_pass_share_item(args: dict) -> dict: """Share an item with another user.""" share_id = args.get("share_id", "") vault_name = args.get("vault_name", "") item_id = args.get("item_id", "") item_title = args.get("item_title", "") email = args.get("email", "") role = args.get("role", "viewer") if not email: return {"success": False, "error": "email is required"} if not ((share_id or vault_name) and (item_id or item_title)): return { "success": False, "error": "Provide item identifiers (share_id/vault_name + item_id/item_title)", } cmd_args = ["item", "share"] if share_id: cmd_args.extend(["--share-id", share_id]) if vault_name: cmd_args.extend(["--vault-name", vault_name]) if item_id: cmd_args.extend(["--item-id", item_id]) if item_title: cmd_args.extend(["--item-title", item_title]) cmd_args.append(email) if role: cmd_args.extend(["--role", role]) cmd = _run_pass(cmd_args, timeout=15) if not cmd["success"]: return cmd return { "success": True, "message": f"Item shared with {email} (role: {role}).", } def proton_pass_generate_password(args: dict) -> dict: """Generate a random password or passphrase and optionally save to a vault.""" length = args.get("length", 20) include_uppercase = args.get("include_uppercase", True) include_symbols = args.get("include_symbols", True) passphrase = args.get("passphrase", False) word_count = args.get("word_count", 4) save = args.get("save", False) vault_name = args.get("vault_name", "") share_id = args.get("share_id", "") title = args.get("title", "") if passphrase: # Generate a passphrase cmd_args = ["item", "create", "login"] gen_flag = f"--generate-passphrase={word_count}" if word_count else "--generate-passphrase" cmd_args.append(gen_flag) if title: cmd_args.extend(["--title", title]) else: cmd_args.extend(["--title", f"Generated Passphrase ({word_count} words)"]) else: # Generate a password settings_parts = [str(length)] if include_uppercase: settings_parts.append("uppercase") if include_symbols: settings_parts.append("symbols") gen_settings = ",".join(settings_parts) cmd_args = ["item", "create", "login"] cmd_args.append(f"--generate-password={gen_settings}") if title: cmd_args.extend(["--title", title]) else: cmd_args.extend(["--title", f"Generated Password ({length} chars)"]) if save: if vault_name: cmd_args.extend(["--vault-name", vault_name]) if share_id: cmd_args.extend(["--share-id", share_id]) cmd = _run_pass(cmd_args, timeout=15) if not cmd["success"]: return cmd return { "success": True, "message": f"Password generated and saved to vault.", "output": cmd.get("output", ""), } # For non-save mode, use --get-template to capture output # Actually just generate and discard; or generate on CLI side return { "success": True, "message": ( f"Password generation requires saving to a vault (pass --save). " f"Generated items are securely stored in Proton Pass." ), "settings": { "type": "passphrase" if passphrase else "password", "length": word_count if passphrase else length, }, } # ── tool registry ───────────────────────────────────────────────────── TOOLS: dict[str, dict[str, Any]] = { "proton_pass_login": { "handler": proton_pass_login, "schema": { "name": "proton_pass_login", "description": "Authenticate with Proton Pass. Supports interactive (browser), PAT (personal access token), and web login modes.", "parameters": { "type": "object", "properties": { "mode": { "type": "string", "enum": ["interactive", "pat", "web"], "description": "Login mode. 'interactive' (default): prompts for credentials. 'pat': personal access token. 'web': browser-based OAuth.", }, "username": { "type": "string", "description": "Proton account email/username (interactive mode).", }, "token": { "type": "string", "description": "Personal access token (PAT mode only).", }, }, }, }, }, "proton_pass_logout": { "handler": proton_pass_logout, "schema": { "name": "proton_pass_logout", "description": "End the current Proton Pass session and clear cached credentials.", "parameters": {"type": "object", "properties": {}}, }, }, "proton_pass_auth_status": { "handler": proton_pass_auth_status, "schema": { "name": "proton_pass_auth_status", "description": "Check Proton Pass authentication status — whether logged in, account info, session validity.", "parameters": {"type": "object", "properties": {}}, }, }, "proton_pass_test": { "handler": proton_pass_test, "schema": { "name": "proton_pass_test", "description": "Test the pass-cli connection and authentication validity.", "parameters": {"type": "object", "properties": {}}, }, }, "proton_pass_vaults": { "handler": proton_pass_vaults, "schema": { "name": "proton_pass_vaults", "description": "List all accessible Proton Pass vaults with share IDs and metadata.", "parameters": {"type": "object", "properties": {}}, }, }, "proton_pass_vault_create": { "handler": proton_pass_vault_create, "schema": { "name": "proton_pass_vault_create", "description": "Create a new Proton Pass vault.", "parameters": { "type": "object", "properties": { "name": { "type": "string", "description": "Name for the new vault.", }, }, "required": ["name"], }, }, }, "proton_pass_vault_delete": { "handler": proton_pass_vault_delete, "schema": { "name": "proton_pass_vault_delete", "description": "Permanently delete a Proton Pass vault and all its contents. Cannot be undone.", "parameters": { "type": "object", "properties": { "share_id": { "type": "string", "description": "Vault share ID (mutually exclusive with vault_name).", }, "vault_name": { "type": "string", "description": "Vault name (mutually exclusive with share_id).", }, }, }, }, }, "proton_pass_list": { "handler": proton_pass_list, "schema": { "name": "proton_pass_list", "description": "List all items in a vault. Optionally filter by vault name or share ID.", "parameters": { "type": "object", "properties": { "vault_name": { "type": "string", "description": "Vault name (mutually exclusive with share_id).", }, "share_id": { "type": "string", "description": "Vault share ID (mutually exclusive with vault_name).", }, }, }, }, }, "proton_pass_get": { "handler": proton_pass_get, "schema": { "name": "proton_pass_get", "description": "Retrieve a specific secret/item from a Proton Pass vault. Returns full item details including password, username, URLs, notes, and custom fields. WARNING: Secrets enter the agent context.", "parameters": { "type": "object", "properties": { "share_id": { "type": "string", "description": "Vault share ID.", }, "vault_name": { "type": "string", "description": "Vault name.", }, "item_id": { "type": "string", "description": "Item ID within the vault.", }, "item_title": { "type": "string", "description": "Item title (name) within the vault.", }, "uri": { "type": "string", "description": "pass:// URI shortcut: pass://vault/item/field or pass://share_id/item_id.", }, "field": { "type": "string", "description": "Specific field to retrieve (e.g., 'password', 'username', 'email', 'url', 'note', or custom field name).", }, }, }, }, }, "proton_pass_search": { "handler": proton_pass_search, "schema": { "name": "proton_pass_search", "description": "Search items across Proton Pass vaults by title or name. Lists items and filters client-side for matching names.", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "Search query string.", }, "vault_name": { "type": "string", "description": "Optional: restrict search to a specific vault by name.", }, "share_id": { "type": "string", "description": "Optional: restrict search to a specific vault by share ID.", }, }, "required": ["query"], }, }, }, "proton_pass_create": { "handler": proton_pass_create, "schema": { "name": "proton_pass_create", "description": "Create a new item (login or note) in a Proton Pass vault with specified fields.", "parameters": { "type": "object", "properties": { "type": { "type": "string", "enum": ["login", "note"], "description": "Item type. Default: 'login'.", }, "vault_name": { "type": "string", "description": "Target vault name.", }, "share_id": { "type": "string", "description": "Target vault share ID.", }, "title": { "type": "string", "description": "Item title (required).", }, "username": { "type": "string", "description": "Username for login items.", }, "password": { "type": "string", "description": "Password value. Exclusive with generate_password.", }, "generate_password": { "type": "boolean", "description": "Auto-generate a password instead of providing one.", }, "password_settings": { "type": "string", "description": "Generation settings: 'length,uppercase,symbols' or 'length' (default: '20,uppercase,symbols').", }, "url": { "type": "string", "description": "URL for the item.", }, "note": { "type": "string", "description": "Notes or content for note-type items.", }, }, "required": ["title"], }, }, }, "proton_pass_edit": { "handler": proton_pass_edit, "schema": { "name": "proton_pass_edit", "description": "Update fields on an existing Proton Pass item. Supports standard and custom fields.", "parameters": { "type": "object", "properties": { "share_id": { "type": "string", "description": "Vault share ID.", }, "vault_name": { "type": "string", "description": "Vault name.", }, "item_id": { "type": "string", "description": "Item ID to update.", }, "item_title": { "type": "string", "description": "Item title to update.", }, "fields": { "type": "object", "description": "Key-value map of fields to update (e.g., {'password': 'newpass', 'title': 'New Name'}).", }, }, }, }, }, "proton_pass_delete": { "handler": proton_pass_delete, "schema": { "name": "proton_pass_delete", "description": "Permanently delete an item from a Proton Pass vault.", "parameters": { "type": "object", "properties": { "share_id": { "type": "string", "description": "Vault share ID.", }, "vault_name": { "type": "string", "description": "Vault name.", }, "item_id": { "type": "string", "description": "Item ID to delete.", }, "item_title": { "type": "string", "description": "Item title to delete.", }, }, }, }, }, "proton_pass_totp": { "handler": proton_pass_totp, "schema": { "name": "proton_pass_totp", "description": "Get the current TOTP (time-based one-time password) code for an item that has TOTP configured.", "parameters": { "type": "object", "properties": { "share_id": { "type": "string", "description": "Vault share ID.", }, "vault_name": { "type": "string", "description": "Vault name.", }, "item_id": { "type": "string", "description": "Item ID.", }, "item_title": { "type": "string", "description": "Item title.", }, "uri": { "type": "string", "description": "pass:// URI shortcut.", }, }, }, }, }, "proton_pass_inject": { "handler": proton_pass_inject, "schema": { "name": "proton_pass_inject", "description": "Run a shell command with secrets injected into environment variables from Proton Pass. Uses pass-cli run to resolve pass:// URIs in env vars. Secrets are masked in stdout/stderr by default.", "parameters": { "type": "object", "properties": { "command": { "type": "string", "description": "Command to execute with secret-injected environment variables.", }, "env_files": { "type": "array", "items": {"type": "string"}, "description": ".env files to load (later files override earlier).", }, "no_masking": { "type": "boolean", "description": "Disable automatic secret masking in output. Use with caution.", }, }, "required": ["command"], }, }, }, "proton_pass_ssh_load": { "handler": proton_pass_ssh_load, "schema": { "name": "proton_pass_ssh_load", "description": "Load SSH keys stored in Proton Pass into the system's SSH agent. Requires SSH_AUTH_SOCK to be set.", "parameters": { "type": "object", "properties": { "share_id": { "type": "string", "description": "Restrict to vault by share ID.", }, "vault_name": { "type": "string", "description": "Restrict to vault by name.", }, }, }, }, }, "proton_pass_ssh_agent_start": { "handler": proton_pass_ssh_agent_start, "schema": { "name": "proton_pass_ssh_agent_start", "description": "Start Proton Pass CLI as the SSH agent (foreground). Sets SSH_AUTH_SOCK to the agent's Unix socket.", "parameters": { "type": "object", "properties": { "share_id": { "type": "string", "description": "Restrict to vault by share ID.", }, "vault_name": { "type": "string", "description": "Restrict to vault by name.", }, }, }, }, }, "proton_pass_ssh_daemon_start": { "handler": proton_pass_ssh_daemon_start, "schema": { "name": "proton_pass_ssh_daemon_start", "description": "Start Proton Pass SSH agent as a background daemon process.", "parameters": { "type": "object", "properties": { "share_id": { "type": "string", "description": "Restrict to vault by share ID.", }, "vault_name": { "type": "string", "description": "Restrict to vault by name.", }, "log_file": { "type": "string", "description": "Path to log file for daemon output.", }, }, }, }, }, "proton_pass_ssh_daemon_status": { "handler": proton_pass_ssh_daemon_status, "schema": { "name": "proton_pass_ssh_daemon_status", "description": "Check the status of the Proton Pass SSH agent daemon (running, degraded, or stopped).", "parameters": {"type": "object", "properties": {}}, }, }, "proton_pass_ssh_daemon_stop": { "handler": proton_pass_ssh_daemon_stop, "schema": { "name": "proton_pass_ssh_daemon_stop", "description": "Stop the Proton Pass SSH agent background daemon.", "parameters": {"type": "object", "properties": {}}, }, }, "proton_pass_share_item": { "handler": proton_pass_share_item, "schema": { "name": "proton_pass_share_item", "description": "Share a Proton Pass item with another user by email.", "parameters": { "type": "object", "properties": { "share_id": { "type": "string", "description": "Vault share ID.", }, "vault_name": { "type": "string", "description": "Vault name.", }, "item_id": { "type": "string", "description": "Item ID to share.", }, "item_title": { "type": "string", "description": "Item title to share.", }, "email": { "type": "string", "description": "Recipient email address.", }, "role": { "type": "string", "enum": ["viewer", "editor", "manager"], "description": "Access role. Default: viewer.", }, }, "required": ["email"], }, }, }, "proton_pass_generate_password": { "handler": proton_pass_generate_password, "schema": { "name": "proton_pass_generate_password", "description": "Generate a random password or passphrase and optionally save it to a vault.", "parameters": { "type": "object", "properties": { "length": { "type": "integer", "description": "Password character length (default: 20).", }, "include_uppercase": { "type": "boolean", "description": "Include uppercase letters (default: true).", }, "include_symbols": { "type": "boolean", "description": "Include symbols (default: true).", }, "passphrase": { "type": "boolean", "description": "Generate a passphrase instead of a random password.", }, "word_count": { "type": "integer", "description": "Number of words for passphrase (default: 4).", }, "save": { "type": "boolean", "description": "Save the generated item to a vault.", }, "vault_name": { "type": "string", "description": "Vault to save into (required if save=true).", }, "share_id": { "type": "string", "description": "Vault share ID to save into.", }, "title": { "type": "string", "description": "Title for the saved item.", }, }, }, }, }, } def dispatch(tool_name: str, args: dict | None = None) -> str: """ Dispatch a tool by name with the given arguments. Returns a JSON string (Hermes tools return strings, not dicts). """ if args is None: args = {} tool = TOOLS.get(tool_name) if tool is None: return json.dumps({ "success": False, "error": f"Unknown tool: {tool_name}. Available: {', '.join(sorted(TOOLS.keys()))}", }) try: result = tool["handler"](args) return json.dumps(result, indent=2, default=str) except Exception as exc: return json.dumps({ "success": False, "error": f"{tool_name} raised: {exc}", }, indent=2) # ── CLI entry point (for testing) ────────────────────────────────────── def main(): """CLI entry point for testing tools from the command line.""" import sys if len(sys.argv) < 2: print(f"Usage: {sys.argv[0]} [json_args]") print(f"Tools: {', '.join(sorted(TOOLS.keys()))}") sys.exit(1) tool_name = sys.argv[1] args = {} if len(sys.argv) > 2: try: args = json.loads(sys.argv[2]) except json.JSONDecodeError as exc: print(json.dumps({"error": f"Invalid JSON args: {exc}"})) sys.exit(1) result = dispatch(tool_name, args) print(result) if __name__ == "__main__": main()