hermes-proton/skills/proton-pass/scripts/tools.py
H.M. Murdock 27592f710b
Phase 3: proton-pass skill — 22 tool handlers wrapping pass-cli (vaults, items, TOTP, SSH, inject)
Hermes skill wrapping the official pass-cli (Rust binary) for agent use.
Follows the architecture from t_d1c7437e (ARCHITECTURE.md section 4).

Tools:
- Auth: proton_pass_login, proton_pass_logout, proton_pass_auth_status, proton_pass_test
- Vaults: proton_pass_vaults, proton_pass_vault_create, proton_pass_vault_delete
- Items: proton_pass_list, proton_pass_get, proton_pass_search, proton_pass_create,
  proton_pass_edit, proton_pass_delete, proton_pass_totp, proton_pass_share_item
- Injection: proton_pass_inject (wraps pass-cli run)
- SSH: proton_pass_ssh_load, proton_pass_ssh_agent_start,
  proton_pass_ssh_daemon_{start,status,stop}
- Utility: proton_pass_generate_password

Signed-off-by: Murdock A-Team
2026-06-08 18:33:09 +02:00

1427 lines
48 KiB
Python

#!/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]} <tool_name> [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()