mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-12 13:52:15 +00:00
feat(egress): first-class x-api-key providers + hot reload via management API
Both wired against features the iron-proxy author (@mslipper) confirmed on PR #30179 — and both verified present in the pinned v0.39.0 source. Header-auth providers (match_headers): - New _HEADER_AUTH_PROVIDERS: Anthropic native (x-api-key), Azure OpenAI (api-key on *.openai.azure.com / *.cognitiveservices / *.services.ai), Gemini (x-goog-api-key + ?key= query param via match_query). - TokenMapping grows match_headers + alias_env_names; per-provider header sets flow into the secrets rules; mappings.json roundtrips them (legacy files load with the Authorization default). - GEMINI_API_KEY / GOOGLE_API_KEY collapse into ONE mapping (two require-rules on the same host would reject each other); the sandbox gets the token under both names, and the proxy child env mirrors the alias into the canonical name when only the alias is set. - Docker backend injects alias env names alongside canonical ones. - The fail-closed tier is now empty, so fail_on_uncovered_providers and discover_blocked_providers are deleted (dead toggle otherwise); _NON_BEARER_PROVIDERS shrinks to genuinely-unswappable signature auth (AWS SigV4, GCP service-account OAuth) — warn-only, as before. Management API (hot reload): - Generated proxy.yaml enables the v0.39 management listener: loopback only at tunnel_port+2, bearer key from HERMES_IRON_PROXY_MGMT_KEY. - Key minted at setup (management.token, 0600); start_proxy injects it (v0.39 refuses to start when api_key_env is empty). - hermes egress reload -> POST /v1/reload: re-reads proxy.yaml and atomically swaps the pipeline; 422 leaves the running ruleset untouched; actionable errors for not-running / pre-management config / key mismatch. Secrets changes still require restart (daemon env is read at spawn) — the CLI says so. Validation: 218/218 unit+CLI+docker tests; 3/3 gated live E2E against the real v0.39.0 binary (Authorization swap, x-api-key swap, live reload with token rotation on the same pid). Docs updated.
This commit is contained in:
parent
6ff7e78649
commit
86fcb2fe5f
10 changed files with 922 additions and 250 deletions
|
|
@ -198,7 +198,6 @@ def test_cmd_setup_rejects_invalid_tunnel_port_range(hermes_home, monkeypatch, b
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# cmd_start — fail_on_uncovered_providers + Bitwarden rotation wire-up
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
@ -212,21 +211,6 @@ def test_cmd_start_refuses_when_proxy_disabled(hermes_home, monkeypatch):
|
|||
assert rc == 1
|
||||
|
||||
|
||||
def test_cmd_start_refuses_on_uncovered_provider_when_strict(hermes_home, monkeypatch):
|
||||
"""fail_on_uncovered_providers=true + ANTHROPIC_API_KEY in env =
|
||||
refuse to start (real credential would otherwise leak into sandbox)."""
|
||||
|
||||
from hermes_cli.config import load_config, save_config
|
||||
cfg = load_config()
|
||||
cfg.setdefault("proxy", {})["enabled"] = True
|
||||
cfg["proxy"]["fail_on_uncovered_providers"] = True
|
||||
save_config(cfg)
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test")
|
||||
|
||||
rc = proxy_cli.cmd_start(_args())
|
||||
assert rc == 1
|
||||
|
||||
|
||||
def test_cmd_start_honors_auto_install_false(hermes_home, monkeypatch):
|
||||
from hermes_cli.config import load_config, save_config
|
||||
|
||||
|
|
@ -244,7 +228,6 @@ def test_cmd_start_honors_auto_install_false(hermes_home, monkeypatch):
|
|||
|
||||
monkeypatch.setattr(ip, "start_proxy", fake_start_proxy)
|
||||
monkeypatch.setattr(ip, "discover_uncovered_providers", lambda **kw: [])
|
||||
monkeypatch.setattr(ip, "discover_blocked_providers", lambda **kw: [])
|
||||
|
||||
rc = proxy_cli.cmd_start(_args())
|
||||
assert rc == 0
|
||||
|
|
@ -262,7 +245,6 @@ def test_cmd_start_passes_bitwarden_refresh_flag_when_credential_source_is_bitwa
|
|||
cfg = load_config()
|
||||
cfg.setdefault("proxy", {})["enabled"] = True
|
||||
cfg["proxy"]["credential_source"] = "bitwarden"
|
||||
cfg["proxy"]["fail_on_uncovered_providers"] = False
|
||||
cfg.setdefault("secrets", {})["bitwarden"] = {
|
||||
"enabled": True,
|
||||
"project_id": "test-proj-id",
|
||||
|
|
@ -284,7 +266,6 @@ def test_cmd_start_passes_bitwarden_refresh_flag_when_credential_source_is_bitwa
|
|||
return s
|
||||
monkeypatch.setattr(ip, "start_proxy", fake_start_proxy)
|
||||
monkeypatch.setattr(ip, "discover_uncovered_providers", lambda **kw: [])
|
||||
monkeypatch.setattr(ip, "discover_blocked_providers", lambda **kw: [])
|
||||
|
||||
rc = proxy_cli.cmd_start(_args())
|
||||
assert rc == 0
|
||||
|
|
@ -301,7 +282,6 @@ def test_cmd_start_refuses_when_bitwarden_token_missing(hermes_home, monkeypatch
|
|||
cfg = load_config()
|
||||
cfg.setdefault("proxy", {})["enabled"] = True
|
||||
cfg["proxy"]["credential_source"] = "bitwarden"
|
||||
cfg["proxy"]["fail_on_uncovered_providers"] = False
|
||||
cfg.setdefault("secrets", {})["bitwarden"] = {
|
||||
"enabled": True,
|
||||
"project_id": "test-proj-id",
|
||||
|
|
@ -315,7 +295,6 @@ def test_cmd_start_refuses_when_bitwarden_token_missing(hermes_home, monkeypatch
|
|||
pytest.fail("start_proxy should not be invoked when BWS token missing")
|
||||
monkeypatch.setattr(ip, "start_proxy", must_not_call)
|
||||
monkeypatch.setattr(ip, "discover_uncovered_providers", lambda **kw: [])
|
||||
monkeypatch.setattr(ip, "discover_blocked_providers", lambda **kw: [])
|
||||
|
||||
rc = proxy_cli.cmd_start(_args())
|
||||
assert rc == 1
|
||||
|
|
@ -328,7 +307,6 @@ def test_cmd_start_does_not_pass_bitwarden_refresh_when_credential_source_is_env
|
|||
cfg = load_config()
|
||||
cfg.setdefault("proxy", {})["enabled"] = True
|
||||
cfg["proxy"]["credential_source"] = "env"
|
||||
cfg["proxy"]["fail_on_uncovered_providers"] = False
|
||||
save_config(cfg)
|
||||
|
||||
captured: dict = {}
|
||||
|
|
@ -553,7 +531,6 @@ def test_cmd_start_refuses_when_bitwarden_mode_but_disabled(hermes_home, monkeyp
|
|||
cfg = load_config()
|
||||
cfg.setdefault("proxy", {})["enabled"] = True
|
||||
cfg["proxy"]["credential_source"] = "bitwarden"
|
||||
cfg["proxy"]["fail_on_uncovered_providers"] = False
|
||||
cfg.setdefault("secrets", {})["bitwarden"] = {"enabled": False}
|
||||
save_config(cfg)
|
||||
|
||||
|
|
@ -561,7 +538,6 @@ def test_cmd_start_refuses_when_bitwarden_mode_but_disabled(hermes_home, monkeyp
|
|||
pytest.fail("start_proxy must not run when bitwarden mode is broken")
|
||||
monkeypatch.setattr(ip, "start_proxy", must_not_call)
|
||||
monkeypatch.setattr(ip, "discover_uncovered_providers", lambda **kw: [])
|
||||
monkeypatch.setattr(ip, "discover_blocked_providers", lambda **kw: [])
|
||||
|
||||
rc = proxy_cli.cmd_start(_args())
|
||||
assert rc == 1
|
||||
|
|
@ -578,7 +554,6 @@ def test_cmd_start_bitwarden_disabled_proceeds_with_env_fallback(
|
|||
cfg.setdefault("proxy", {})["enabled"] = True
|
||||
cfg["proxy"]["credential_source"] = "bitwarden"
|
||||
cfg["proxy"]["allow_env_fallback"] = True
|
||||
cfg["proxy"]["fail_on_uncovered_providers"] = False
|
||||
cfg.setdefault("secrets", {})["bitwarden"] = {"enabled": False}
|
||||
save_config(cfg)
|
||||
|
||||
|
|
@ -593,7 +568,6 @@ def test_cmd_start_bitwarden_disabled_proceeds_with_env_fallback(
|
|||
|
||||
monkeypatch.setattr(ip, "start_proxy", fake_start_proxy)
|
||||
monkeypatch.setattr(ip, "discover_uncovered_providers", lambda **kw: [])
|
||||
monkeypatch.setattr(ip, "discover_blocked_providers", lambda **kw: [])
|
||||
|
||||
rc = proxy_cli.cmd_start(_args())
|
||||
assert rc == 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue