From 712bdfb949d22e3bb4c8db501e9846d7d459013d Mon Sep 17 00:00:00 2001 From: Shannon Sands Date: Fri, 27 Mar 2026 09:42:49 +1000 Subject: [PATCH] fix(gateway): revoke deleted keystore-backed env vars on refresh Force-refresh now also clears env vars that were previously injected by the keystore but no longer exist in the current injectable secret set. This lets credential deletion/revocation propagate in long-lived gateway processes without restart, while still preserving external env precedence. Adds a regression test covering deletion of a keystore-backed OPENAI_API_KEY followed by gateway refresh. --- keystore/client.py | 12 +++++++++++ tests/gateway/test_keystore_injection.py | 26 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/keystore/client.py b/keystore/client.py index 5df3b842f4..0f599800ef 100644 --- a/keystore/client.py +++ b/keystore/client.py @@ -180,6 +180,18 @@ class KeystoreClient: previous = dict(self._injected) owned = _owned_env_names() injected = {} + current_names = set(secrets.keys()) + + # Force-refresh also acts as revocation for previously keystore-owned + # env vars that have been deleted from the keystore or are no longer + # injectable. External env vars are never in `owned`, so they are not + # touched here. + if force: + removed = owned - current_names + for name in removed: + os.environ.pop(name, None) + owned -= removed + for name, value in secrets.items(): should_write = False if name not in os.environ: diff --git a/tests/gateway/test_keystore_injection.py b/tests/gateway/test_keystore_injection.py index 0608f70229..6bfbd8bf48 100644 --- a/tests/gateway/test_keystore_injection.py +++ b/tests/gateway/test_keystore_injection.py @@ -102,3 +102,29 @@ def test_gateway_refresh_does_not_clobber_external_env(monkeypatch, tmp_path): ks.set_secret("OPENAI_API_KEY", "rotated-keystore-value") gateway_run._inject_keystore_env(force=True) assert os.environ.get("OPENAI_API_KEY") == "env-wins" + + +def test_gateway_refresh_revokes_deleted_keystore_secret(monkeypatch, tmp_path): + home = tmp_path / ".hermes" + home.mkdir(parents=True) + (home / ".env").write_text("") + (home / "config.yaml").write_text("toolsets:\n- hermes-cli\n") + + monkeypatch.setenv("HERMES_HOME", str(home)) + from keystore.client import KeystoreClient, reset_keystore + reset_keystore() + ks = KeystoreClient(home / "keystore" / "secrets.db") + ks.initialize("passphrase") + ks.set_secret("OPENAI_API_KEY", "sk-old") + monkeypatch.setenv("HERMES_KEYSTORE_PASSPHRASE", "passphrase") + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + + gateway_run = _reload_gateway_run(monkeypatch, home) + assert os.environ.get("OPENAI_API_KEY") == "sk-old" + + # Delete from keystore; force refresh should revoke the previously + # injected env var from the long-lived process. + ks.delete_secret("OPENAI_API_KEY") + gateway_run._inject_keystore_env(force=True) + assert os.environ.get("OPENAI_API_KEY") is None