feat(vault): zero-setup UX — save a login on the page that needs it, managers auto-detected, one "Passwords & Logins" surface
Nobody should have to learn `hermes vault add` or find a toggle before "log into GitHub" works. - browser_vault_save_login: when the agent reaches a sign-in page with no saved login it asks the user on THEIR surface (CLI two-step panel on the sudo modal: identifier shown, password masked; Desktop card with labelled Email/username + Password fields). The answer goes to the encrypted vault bound to the page origin and is filled at once; the model gets back only the handle and identifier. Declining returns save_declined; headless sessions get prompt_unavailable. Never a password in chat. - Vault tools ride with the browser toolset (check_browser_requirements) instead of appearing only once the vault has items — an empty vault is exactly when save_login is needed. browser_vault_list hints at it when empty. - 1Password / Bitwarden are login sources as soon as their CLI is installed; `vault.<name>.enabled` is opt-OUT only. Settings shows Detected/Locked/Unlocked/Off/Not detected with a switch only for installed managers; `hermes vault sources` reports detection, `--disable`/`--enable` flip the opt-out. - Desktop nav/page renamed "Passwords & Logins"; empty state tells the user they do not need to add anything; all five locales updated. Docs rewritten from "how it works" to "say log into X". - New per-thread SaveLoginPrompt callback (agent/vault_backends/unlock.py) installed beside the unlock prompt on every CLI site and the gateway bridge (vault.save_login.request/respond/expire), propagated to worker threads via tools.thread_context. Live: CLI PTY (real model, packaged Chromium, local login server) — panel shown, identifier + masked password typed, server received the correct password, password absent from terminal transcript and from every file under HERMES_HOME outside vault/. Native Electron (headless, isolated HOME/HERMES_HOME, own Vite + CDP port) — card shown, "Save & sign in", server received the password, Settings lists the saved item, password absent from the rendered UI.
This commit is contained in:
@@ -4,6 +4,8 @@ globals at install time (method_ctx.bind_module), so they reference server.py gl
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import contextlib
|
||||
import threading
|
||||
|
||||
@@ -171,11 +173,23 @@ def _wire_callbacks(sid: str):
|
||||
set_secret_capture_callback(secret_cb)
|
||||
# External password-manager unlock: the renderer shows a masked master-password card; the
|
||||
# answer is consumed by the manager CLI on stdin and only a session token stays in memory.
|
||||
from agent.vault_backends.unlock import set_current_session_id, set_unlock_prompt_callback
|
||||
from agent.vault_backends.unlock import (set_current_session_id, set_save_login_prompt_callback,
|
||||
set_unlock_prompt_callback)
|
||||
set_current_session_id(sid) # an unlock made on this turn belongs to this session (released with it)
|
||||
set_unlock_prompt_callback(lambda backend, display_name: _block(
|
||||
"vault.unlock.request", sid, {"backend": backend, "display_name": display_name}, timeout=120))
|
||||
|
||||
def save_login_cb(origin, site):
|
||||
# The renderer shows identifier + masked password; the JSON answer goes straight to the vault store.
|
||||
raw = _block("vault.save_login.request", sid, {"origin": origin, "site": site}, timeout=180)
|
||||
try:
|
||||
data = json.loads(raw) if raw else None
|
||||
except ValueError:
|
||||
return None
|
||||
return data if isinstance(data, dict) and data.get("password") else None
|
||||
|
||||
set_save_login_prompt_callback(save_login_cb)
|
||||
|
||||
|
||||
def _available_personalities(cfg: dict | None = None) -> dict:
|
||||
"""Built-ins + user overrides, via hermes_cli.personality (single owner)."""
|
||||
|
||||
Reference in New Issue
Block a user