"""Write-guard tests — managed keys can't be set/removed by the user.""" import pytest @pytest.fixture def homes(tmp_path, monkeypatch): home = tmp_path / "home" home.mkdir() managed = tmp_path / "managed" managed.mkdir() monkeypatch.setenv("HERMES_HOME", str(home)) monkeypatch.setenv("HERMES_MANAGED_DIR", str(managed)) import hermes_cli.config as cfg from hermes_cli import managed_scope cfg._LOAD_CONFIG_CACHE.clear() cfg._RAW_CONFIG_CACHE.clear() managed_scope.invalidate_managed_cache() (managed / "config.yaml").write_text( "model:\n default: managed/model\n", encoding="utf-8" ) managed_scope.invalidate_managed_cache() return home, managed def test_config_set_managed_key_rejected(homes, capsys): from hermes_cli.config import set_config_value with pytest.raises(SystemExit) as exc: set_config_value("model.default", "user/override") assert exc.value.code != 0 captured = capsys.readouterr() assert "managed" in (captured.out + captured.err).lower() # ── env write guards ───────────────────────────────────────────────────────── @pytest.fixture def env_homes(tmp_path, monkeypatch): home = tmp_path / "home" home.mkdir() managed = tmp_path / "managed" managed.mkdir() monkeypatch.setenv("HERMES_HOME", str(home)) monkeypatch.setenv("HERMES_MANAGED_DIR", str(managed)) (managed / ".env").write_text( "OPENAI_API_BASE=https://org.example/v1\n", encoding="utf-8" ) from hermes_cli import managed_scope managed_scope.invalidate_managed_cache() return home, managed def test_save_env_value_managed_key_rejected(env_homes, capsys): from hermes_cli.config import save_env_value, get_env_path save_env_value("OPENAI_API_BASE", "https://user.example/v1") assert "managed" in capsys.readouterr().err.lower() env_path = get_env_path() body = env_path.read_text() if env_path.exists() else "" assert "user.example" not in body # ── refused .env writes must fail, not half-apply ──────────────────────────── # Runtime-constructed fake credentials (never literal key-shaped strings). ADMIN_KEY = "sk-ds-" + "a" * 24 OLD_KEY = "sk-ds-" + "b" * 24 NEW_KEY = "sk-ds-" + "c" * 24 @pytest.fixture def pinned_env(tmp_path, monkeypatch): """The administrator pins DEEPSEEK_API_KEY and TELEGRAM_HOME_CHANNEL; the user's own stores still hold an older key in .env, a config.yaml mirror of it, its env-seeded pool entry, and a stale top-level config.yaml copy of the channel.""" import json import hermes_cli.config as cfg from hermes_cli import managed_scope home, managed = tmp_path / "home", tmp_path / "managed" home.mkdir() managed.mkdir() monkeypatch.setenv("HERMES_HOME", str(home)) monkeypatch.setenv("HERMES_MANAGED_DIR", str(managed)) (managed / ".env").write_text( f"DEEPSEEK_API_KEY={ADMIN_KEY}\nTELEGRAM_HOME_CHANNEL=111\n", encoding="utf-8") (home / ".env").write_text( f"DEEPSEEK_API_KEY={OLD_KEY}\nTELEGRAM_HOME_CHANNEL=222\n", encoding="utf-8") (home / "config.yaml").write_text( f"model:\n provider: deepseek\n default: deepseek-chat\n api_key: {OLD_KEY}\n" "TELEGRAM_HOME_CHANNEL: '333'\n", encoding="utf-8") (home / "auth.json").write_text(json.dumps({"credential_pool": {"deepseek": [{ "id": "e1", "label": "env", "auth_type": "api_key", "priority": 0, "source": "env:DEEPSEEK_API_KEY", "access_token": OLD_KEY}]}}), encoding="utf-8") cfg._LOAD_CONFIG_CACHE.clear() cfg._RAW_CONFIG_CACHE.clear() cfg.invalidate_env_cache() managed_scope.invalidate_managed_cache() return home, _user_stores(home) def _user_stores(home): return {name: (home / name).read_text(encoding="utf-8") for name in (".env", "config.yaml", "auth.json")} def _assert_user_stores_untouched(home, before): assert _user_stores(home) == before @pytest.mark.parametrize("method", ["PUT", "DELETE"]) @pytest.mark.parametrize("lock, refusal", [ pytest.param("scope", "managed by your administrator", id="managed-scope"), pytest.param("install", "managed by nixos", id="package-managed"), ]) def test_api_env_write_of_locked_key_is_refused_and_writes_nothing(pinned_env, monkeypatch, method, lock, refusal): from fastapi.testclient import TestClient from hermes_cli.web_server import _SESSION_TOKEN, app if lock == "install": monkeypatch.setenv("HERMES_MANAGED", "nixos") home, before = pinned_env body = {"key": "DEEPSEEK_API_KEY", **({"value": NEW_KEY} if method == "PUT" else {})} resp = TestClient(app).request(method, "/api/env", json=body, headers={"X-Hermes-Session-Token": _SESSION_TOKEN}) assert resp.status_code == 400 assert refusal in resp.json()["detail"] _assert_user_stores_untouched(home, before) _ANTHROPIC_ENV_VARS = ("ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN") @pytest.fixture def anthropic_homes(tmp_path, monkeypatch): """A user home whose auth.json holds a manually added anthropic key, and an empty managed dir.""" import json import hermes_cli.config as cfg from hermes_cli import managed_scope home, managed = tmp_path / "home", tmp_path / "managed" home.mkdir() managed.mkdir() monkeypatch.setenv("HERMES_HOME", str(home)) monkeypatch.setenv("HERMES_MANAGED_DIR", str(managed)) for name in _ANTHROPIC_ENV_VARS: monkeypatch.delenv(name, raising=False) (home / "auth.json").write_text(json.dumps({"credential_pool": {"anthropic": [{ "id": "m1", "label": "manual", "auth_type": "api_key", "priority": 0, "source": "manual", "access_token": OLD_KEY}]}}), encoding="utf-8") def reload(): cfg.invalidate_env_cache() managed_scope.invalidate_managed_cache() reload() return home, managed, reload def _disconnect(slug): from tui_gateway import server return server._methods["model.disconnect"](1, {"slug": slug}) def test_disconnect_on_a_package_managed_install_still_clears_the_auth_store(anthropic_homes, monkeypatch): """The .env lock refuses every key on such an install, but a key that holds nothing is nothing to refuse.""" import json home, _managed, _reload = anthropic_homes monkeypatch.setenv("HERMES_MANAGED", "nixos") resp = _disconnect("anthropic") assert resp.get("result", {}).get("disconnected") is True, resp assert "anthropic" not in json.loads((home / "auth.json").read_text(encoding="utf-8"))["credential_pool"] # ── bulk save strips managed leaves ──────────────────────────────────────────