From da4321cfcb894be2ff9dc41cdcb4467d30caab57 Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Mon, 11 May 2026 21:53:48 -0400 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20auto-forward=20CAPISCIO=5FSERVER=5FU?= =?UTF-8?q?RL=20=E2=86=92=20CAPISCIO=5FREGISTRY=5FENDPOINT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Go binary reads CAPISCIO_REGISTRY_ENDPOINT for JWKS-based badge verification. Without it, BadgeVerifier is nil and all badge checks fail with ErrBadgeInvalid — even for valid badges. Previously users had to set both CAPISCIO_SERVER_URL (Python SDK) and CAPISCIO_REGISTRY_ENDPOINT (Go binary) to the same value. Now connect.py auto-forwards SERVER_URL into REGISTRY_ENDPOINT before spawning the Go subprocess, following the same pattern used for CAPISCIO_BUNDLE_URL. Users can still override REGISTRY_ENDPOINT explicitly if needed. Closes #28 --- capiscio_mcp/connect.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/capiscio_mcp/connect.py b/capiscio_mcp/connect.py index 8c26fb5..a90f3b7 100644 --- a/capiscio_mcp/connect.py +++ b/capiscio_mcp/connect.py @@ -350,6 +350,12 @@ async def connect( # ------------------------------------------------------------------ os.environ["CAPISCIO_API_KEY"] = effective_api_key + # Forward SERVER_URL so the Go binary can build its JWKS URL for + # badge verification. Without this, BadgeVerifier is nil and all + # badge checks fail with ErrBadgeInvalid. (See issue #28) + if "CAPISCIO_REGISTRY_ENDPOINT" not in os.environ: + os.environ["CAPISCIO_REGISTRY_ENDPOINT"] = server_url + org_id_file = effective_keys_dir / "org_id.txt" cached_org_id: Optional[str] = None if org_id_file.exists(): From 05ab2f86992ff2cc297156cbbcdc37da5e12dafd Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Mon, 11 May 2026 22:32:59 -0400 Subject: [PATCH 2/3] test: add coverage for CAPISCIO_REGISTRY_ENDPOINT auto-forward --- tests/test_connect.py | 62 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/test_connect.py b/tests/test_connect.py index e89350e..7e2c071 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -371,6 +371,68 @@ async def test_connect_no_capture_hint_on_recovery(self, tmp_keys_dir): mock_hint.assert_not_called() + async def test_connect_auto_forwards_registry_endpoint(self, tmp_keys_dir): + """connect() should set CAPISCIO_REGISTRY_ENDPOINT from server_url when absent.""" + fake_keys = { + "did_key": FAKE_DID, + "public_key_pem": FAKE_PUB_KEY_PEM, + "private_key_pem": FAKE_PRIV_KEY_PEM, + "key_id": "key-1", + } + + # Ensure CAPISCIO_REGISTRY_ENDPOINT is NOT in the environment + env = {k: v for k, v in os.environ.items() if k != "CAPISCIO_REGISTRY_ENDPOINT"} + with ( + patch.dict(os.environ, env, clear=True), + patch("capiscio_mcp.connect.generate_server_keypair", new_callable=AsyncMock, return_value=fake_keys), + patch("capiscio_mcp.connect.register_server_identity", new_callable=AsyncMock), + patch("capiscio_mcp.connect._issue_badge", new_callable=AsyncMock, return_value=FAKE_BADGE), + patch("capiscio_mcp.connect.ServerBadgeKeeper") as MockKeeper, + ): + mock_keeper = MagicMock(spec=ServerBadgeKeeper) + mock_keeper.get_current_badge.return_value = FAKE_BADGE + MockKeeper.return_value = mock_keeper + + await MCPServerIdentity.connect( + server_id=SERVER_ID, + api_key=API_KEY, + server_url="http://localhost:8080", + keys_dir=tmp_keys_dir, + ) + + assert os.environ.get("CAPISCIO_REGISTRY_ENDPOINT") == "http://localhost:8080" + + async def test_connect_does_not_overwrite_explicit_registry_endpoint(self, tmp_keys_dir): + """connect() should NOT overwrite an explicitly set CAPISCIO_REGISTRY_ENDPOINT.""" + fake_keys = { + "did_key": FAKE_DID, + "public_key_pem": FAKE_PUB_KEY_PEM, + "private_key_pem": FAKE_PRIV_KEY_PEM, + "key_id": "key-1", + } + + explicit_endpoint = "https://custom-jwks.example.com" + with ( + patch.dict(os.environ, {"CAPISCIO_REGISTRY_ENDPOINT": explicit_endpoint}), + patch("capiscio_mcp.connect.generate_server_keypair", new_callable=AsyncMock, return_value=fake_keys), + patch("capiscio_mcp.connect.register_server_identity", new_callable=AsyncMock), + patch("capiscio_mcp.connect._issue_badge", new_callable=AsyncMock, return_value=FAKE_BADGE), + patch("capiscio_mcp.connect.ServerBadgeKeeper") as MockKeeper, + ): + mock_keeper = MagicMock(spec=ServerBadgeKeeper) + mock_keeper.get_current_badge.return_value = FAKE_BADGE + MockKeeper.return_value = mock_keeper + + await MCPServerIdentity.connect( + server_id=SERVER_ID, + api_key=API_KEY, + server_url="http://localhost:8080", + keys_dir=tmp_keys_dir, + ) + + # Should retain the explicit value, not overwrite with server_url + assert os.environ.get("CAPISCIO_REGISTRY_ENDPOINT") == explicit_endpoint + # --------------------------------------------------------------------------- # MCPServerIdentity.from_env() From c130bf30a640802560bed1daedeb0f6db9335bb7 Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Tue, 12 May 2026 01:45:13 -0400 Subject: [PATCH 3/3] fix: move test assertions inside patch.dict context + add mismatch warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move assertions for registry endpoint tests inside the patch.dict context manager (fixes CI failures — patch.dict restores env on exit) - Add warning log when CAPISCIO_REGISTRY_ENDPOINT differs from server_url (addresses multi-connect race concern from review) --- capiscio_mcp/connect.py | 9 +++++++++ tests/test_connect.py | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/capiscio_mcp/connect.py b/capiscio_mcp/connect.py index 9f1fb23..191ba08 100644 --- a/capiscio_mcp/connect.py +++ b/capiscio_mcp/connect.py @@ -356,6 +356,15 @@ async def connect( # badge checks fail with ErrBadgeInvalid. (See issue #28) if "CAPISCIO_REGISTRY_ENDPOINT" not in os.environ: os.environ["CAPISCIO_REGISTRY_ENDPOINT"] = server_url + elif os.environ["CAPISCIO_REGISTRY_ENDPOINT"] != server_url: + logger.warning( + "CAPISCIO_REGISTRY_ENDPOINT (%s) differs from server_url (%s) " + "— Go core will verify badges against the registry endpoint, " + "not the server URL. Set CAPISCIO_REGISTRY_ENDPOINT explicitly " + "only if you need a separate JWKS source.", + os.environ["CAPISCIO_REGISTRY_ENDPOINT"], + server_url, + ) org_id_file = effective_keys_dir / "org_id.txt" cached_org_id: Optional[str] = None diff --git a/tests/test_connect.py b/tests/test_connect.py index 7e2c071..9a1f278 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -400,7 +400,7 @@ async def test_connect_auto_forwards_registry_endpoint(self, tmp_keys_dir): keys_dir=tmp_keys_dir, ) - assert os.environ.get("CAPISCIO_REGISTRY_ENDPOINT") == "http://localhost:8080" + assert os.environ.get("CAPISCIO_REGISTRY_ENDPOINT") == "http://localhost:8080" async def test_connect_does_not_overwrite_explicit_registry_endpoint(self, tmp_keys_dir): """connect() should NOT overwrite an explicitly set CAPISCIO_REGISTRY_ENDPOINT.""" @@ -430,8 +430,8 @@ async def test_connect_does_not_overwrite_explicit_registry_endpoint(self, tmp_k keys_dir=tmp_keys_dir, ) - # Should retain the explicit value, not overwrite with server_url - assert os.environ.get("CAPISCIO_REGISTRY_ENDPOINT") == explicit_endpoint + # Should retain the explicit value, not overwrite with server_url + assert os.environ.get("CAPISCIO_REGISTRY_ENDPOINT") == explicit_endpoint # ---------------------------------------------------------------------------