Files
hermes-agent/tests/tools/test_multiplex_tool_cache_scope.py
teknium1 64ad33e32d refactor: remove Skill Sync (hermes sync) and the org skill mirror from core
Skill Sync only worked against a Nous Portal sync plane that was never made
publicly accessible, so no install has ever used it. We don't want it in core
right now, so this deletes it outright instead of moving it behind a plugin
hook surface nobody needs yet:

- `hermes sync` command, `tools/skills_sync_client*.py` and their tests
- the personal pull ticks (gateway housekeeping, classic CLI startup,
  `hermes serve` maintenance) and the debounced post-write push
- the per-skill `sync` usage flag (`set_sync` / `is_sync_enabled`)
- the `skills/_org/<org_id>/` mirror: index gating on `.active_org`, the
  `[org-shared]` / name-collision labels, the org provenance header and
  `org_provenance` field on skill_view, the org delete guard and the
  auto-propose `org_sharing` note on skill_manage

The code is preserved in the private NousResearch/hermes-skill-sync repo if
the feature comes back.
2026-10-03 23:05:48 -07:00

148 lines
6.4 KiB
Python

"""Multiplexed gateway: module-level caches in tools/ and agent/ must not hand profile A's
config/.env-derived value to profile B. Real temp homes, real config.yaml/.env, real modules;
only HTTP transports are stubbed.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
import hermes_yaml as yaml
from agent.secret_scope import build_profile_secret_scope, reset_secret_scope, set_secret_scope
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
def _make_home(root: Path, cfg: dict, env: str = "") -> Path:
root.mkdir(parents=True, exist_ok=True)
(root / "config.yaml").write_text(yaml.safe_dump(cfg), encoding="utf-8")
(root / ".env").write_text(env, encoding="utf-8")
(root / "cache").mkdir(exist_ok=True)
return root
class _scoped:
def __init__(self, home: Path):
self.home = home
def __enter__(self):
self._t1 = set_hermes_home_override(str(self.home))
self._t2 = set_secret_scope(build_profile_secret_scope(self.home))
def __exit__(self, *_):
reset_secret_scope(self._t2)
reset_hermes_home_override(self._t1)
@pytest.fixture
def two_homes(tmp_path, monkeypatch):
a = _make_home(tmp_path / "A", {}, "CAMOFOX_URL=http://camofox-a:9377\n")
b = _make_home(tmp_path / "A" / "profiles" / "B", {}, "CAMOFOX_URL=http://camofox-b:9377\n")
monkeypatch.setenv("HERMES_HOME", str(a))
monkeypatch.delenv("CAMOFOX_URL", raising=False)
return a, b
def test_camofox_vnc_memo_is_keyed_by_the_profiles_server_url(two_homes, monkeypatch):
"""The one-shot VNC probe must not answer B (own CAMOFOX_URL) with A's server address."""
import tools.browser_camofox as cam
class _Resp:
status_code = 200
def __init__(self, url):
self._port = 6001 if "camofox-a" in url else 6002
def json(self):
return {"ok": True, "vncPort": self._port}
monkeypatch.setattr(cam.requests, "get", lambda url, *a, **k: _Resp(url))
a, b = two_homes
with _scoped(a):
assert cam.check_camofox_available() is True
assert cam.get_vnc_url() == "http://camofox-a:6001"
with _scoped(b):
assert cam.get_vnc_url() == "http://camofox-b:6002"
with _scoped(a):
assert cam.get_vnc_url() == "http://camofox-a:6001"
def test_home_keyed_caches_serve_each_profile_its_own_config(tmp_path, monkeypatch):
"""One mechanism (dict keyed by home / override bypass) across the sites that read per-profile
config or per-home files: aux-vision routing, tirith binary path, learned image cost table, aux
semaphore, MCP lock."""
bin_a, bin_b = tmp_path / "binA" / "tirith", tmp_path / "binB" / "tirith"
for p in (bin_a, bin_b):
p.parent.mkdir()
p.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
p.chmod(0o755)
main = {"model": {"provider": "openai", "model": "gpt-4o"}}
a = _make_home(tmp_path / "A", {**main, "security": {"tirith_path": str(bin_a)},
"auxiliary": {"vision": {"provider": "auto"}, "summary": {"max_concurrency": 2}}})
b = _make_home(tmp_path / "A" / "profiles" / "B", {**main, "security": {"tirith_path": str(bin_b)},
"auxiliary": {"vision": {"provider": "openai", "model": "gpt-4o-mini"},
"summary": {"max_concurrency": 7}}})
monkeypatch.setenv("HERMES_HOME", str(a))
(a / "cache" / "image_token_costs.json").write_text(json.dumps({"m@gw.example": 1000}), encoding="utf-8")
(b / "cache" / "image_token_costs.json").write_text(json.dumps({"m@gw.example": 3000}), encoding="utf-8")
import agent.auxiliary_client as ac
import agent.image_token_cost as itc
import tools.computer_use.tool as cu
import tools.tirith_security as tir
from tools import mcp_tool_loop
ac._reset_aux_semaphores()
cu._AUX_VISION_ROUTE_CACHE.clear()
with _scoped(a):
assert cu._should_route_through_aux_vision() is False # no explicit aux vision: native path
assert tir._resolve_tirith_path(tir._load_security_config()["tirith_path"]) == str(bin_a)
assert itc.learned_image_token_cost("m", "http://gw.example/v1") == 1000
sem_a = ac._acquire_sync_aux_semaphore("summary")
sem_a.acquire()
cookie = mcp_tool_loop._try_acquire_mcp_discovery_lock()
lock_a = cookie._fh.name
cookie.release()
with _scoped(b):
assert cu._should_route_through_aux_vision() is True # B named a dedicated vision model
assert tir._resolve_tirith_path(tir._load_security_config()["tirith_path"]) == str(bin_b)
assert itc.learned_image_token_cost("m", "http://gw.example/v1") == 3000
sem_b = ac._acquire_sync_aux_semaphore("summary")
cookie = mcp_tool_loop._try_acquire_mcp_discovery_lock()
lock_b = cookie._fh.name
cookie.release()
assert Path(lock_a).parent == a and Path(lock_b).parent == b
assert sem_b is not sem_a
with _scoped(a):
# B's differently-sized lookup must not have rebuilt the semaphore A is holding.
assert ac._acquire_sync_aux_semaphore("summary") is sem_a
sem_a.release()
def test_endpoint_model_catalog_memo_is_keyed_by_credential(two_homes, monkeypatch):
"""Two profiles, same base_url, different api_key: a per-key gateway's catalog fetched with A's
key must not be served to B from the in-memory memo (the disk memo already lives per home)."""
from contextlib import contextmanager
import httpx
import agent.model_metadata as mm
a, b = two_homes
mm._endpoint_model_metadata_cache.clear()
mm._endpoint_model_metadata_cache_time.clear()
@contextmanager
def stream(url, headers=None, **kwargs):
who = headers.get("Authorization", "").rsplit("-", 1)[-1]
yield httpx.Response(200, request=httpx.Request("GET", url),
json={"data": [{"id": f"model-for-{who}", "context_length": 1}]})
monkeypatch.setattr(mm.model_metadata_http, "stream", stream)
with _scoped(a):
assert set(mm.fetch_endpoint_model_metadata("http://gw.example/v1", api_key="key-A")) == {"model-for-A"}
with _scoped(b):
assert set(mm.fetch_endpoint_model_metadata("http://gw.example/v1", api_key="key-B")) == {"model-for-B"}
with _scoped(a):
assert set(mm.fetch_endpoint_model_metadata("http://gw.example/v1", api_key="key-A")) == {"model-for-A"}