Files
hermes-agent/tests/tui_gateway/test_vault_methods.py
T
Teknium cde0ad0edd feat: agent signs into sites from an encrypted local vault (CLI, browser fill, Desktop Settings)
Consolidated re-apply of #96988 onto current main. Ported from
Merit-Systems/OpenInstinct (MIT) opaque-handle autofill design: the model
sees vault handles + login metadata, the password is resolved and filled
server-side over the supervised CDP socket, and filled values are scrubbed
from every browser tool result by an unconditional redaction registry.

Rebase adaptations to the Sep-2026 facade/sibling layout:
- toolsets: one _HERMES_CORE_TOOLS entry (the browser toolset derives from it)
- hermes_cli/main.py: vault parser registered via the subcommand owner table
- file_safety: vault/ joins the _READ_DENIED_DIRS credential-dir table
- redact: registry scrub runs before the redact_secrets early-return
- browser_vault_tool: _run_browser_command now lives in browser_tool_session
2026-09-10 10:35:07 -07:00

115 lines
3.4 KiB
Python

"""Tests: vault.* JSON-RPC handlers (tui_gateway/methods_vault.py).
The Desktop's Settings → Credential Vault panel. Contracts:
- vault.list returns metadata only — secret values must never appear in
any response envelope;
- vault.add validates via VaultStore.add_item and surfaces clean,
secret-free error messages;
- vault.remove reports {removed: bool} idempotently.
"""
from __future__ import annotations
import json
import pytest
import tui_gateway.server as srv
@pytest.fixture
def home(tmp_path, monkeypatch):
h = tmp_path / ".hermes"
h.mkdir()
monkeypatch.setenv("HERMES_HOME", str(h))
return h
def _result(envelope):
assert "error" not in envelope, envelope
return envelope["result"]
def _error(envelope):
assert "error" in envelope, envelope
return envelope["error"]
_LOGIN_PARAMS = {
"kind": "login",
"label": "Example login",
"origin": "https://example.com",
"secret": {
"identifier_type": "email",
"identifier": "user@example.com",
"password": "s3cret-pw-9000",
},
}
def test_add_then_list_is_password_free(home):
out = _result(srv._methods["vault.add"](1, dict(_LOGIN_PARAMS)))
assert out["id"].startswith("vault_")
# add's own envelope must not echo the secret back
assert "s3cret-pw-9000" not in json.dumps(out)
listed = _result(srv._methods["vault.list"](2, {}))
assert len(listed["items"]) == 1
item = listed["items"][0]
assert item["id"] == out["id"]
assert item["kind"] == "login"
assert item["label"] == "Example login"
assert item["origin"] == "https://example.com"
assert item["created_at"]
# Identifier is agent-visible metadata (design: only the password is secret).
assert item["identifier"] == "user@example.com"
assert item["identifier_type"] == "email"
dumped = json.dumps(listed)
assert "s3cret-pw-9000" not in dumped
assert "password" not in dumped
def test_add_validation_errors_are_clean(home):
err = _error(
srv._methods["vault.add"](
1,
{
"kind": "login",
"label": "no origin",
"secret": {
"identifier_type": "email",
"identifier": "user@example.com",
"password": "s3cret-pw-9000",
},
},
)
)
assert err["code"] == 5095
assert "origin is required" in err["message"]
assert "s3cret-pw-9000" not in json.dumps(err)
err = _error(srv._methods["vault.add"](2, {"kind": "login", "label": "x"}))
assert err["code"] == 5095
assert "secret payload is required" in err["message"]
err = _error(
srv._methods["vault.add"](
3, {"kind": "wat", "label": "x", "secret": {"password": "s3cret-pw-9000"}}
)
)
assert err["code"] == 5095
assert "unknown vault kind" in err["message"]
assert "s3cret-pw-9000" not in json.dumps(err)
def test_remove_is_idempotent(home):
item_id = _result(srv._methods["vault.add"](1, dict(_LOGIN_PARAMS)))["id"]
assert _result(srv._methods["vault.remove"](2, {"id": item_id}))["removed"] is True
assert _result(srv._methods["vault.remove"](3, {"id": item_id}))["removed"] is False
assert _result(srv._methods["vault.list"](4, {}))["items"] == []
def test_remove_requires_id(home):
err = _error(srv._methods["vault.remove"](1, {}))
assert err["code"] == 5095