cde0ad0edd
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
115 lines
3.4 KiB
Python
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
|