diff --git a/CHANGELOG.md b/CHANGELOG.md index a20b7ad..fd224b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- `MuseCodeSubscriptionProfile.runtime_base_url()` now returns the loopback adapter URL. + Hermes resolves the runtime inference base URL through that hook, so without the override + `profile.base_url` was never read: every subscription request went straight to + `api.meta.ai` and Meta answered 403 `client_attestation_denied`. + ## [0.6.0] - 2026-10-30 ### Added diff --git a/__init__.py b/__init__.py index 9a057c9..b78bcd3 100644 --- a/__init__.py +++ b/__init__.py @@ -232,6 +232,19 @@ class MuseCodeSubscriptionProfile(ProviderProfile): # plugin update; only a brand-new non-chat family needs an entry here. _NON_CHAT_PREFIXES = ("sam-", "muse-image-", "muse-voice-") + def runtime_base_url( + self, *, api_key: str, default: str, env_url: str + ) -> str | None: + """Point runtime inference at the loopback wire adapter. + + Runtime credential resolution only reads this hook, so without the + override ``base_url`` is never consulted: requests go straight to + ``api.meta.ai`` and Meta answers 403. ``register()`` keeps + ``self.base_url`` on the adapter, or on the direct endpoint when the + adapter could not bind. + """ + return self.base_url or None + def fetch_models( self, *, diff --git a/tests/test_muse_code_provider.py b/tests/test_muse_code_provider.py index 5f6a2f2..d9e86c0 100644 --- a/tests/test_muse_code_provider.py +++ b/tests/test_muse_code_provider.py @@ -206,6 +206,22 @@ def test_base_url_uses_the_loopback_adapter(tmp_path, monkeypatch): assert "attacker.test" not in base_url +def test_runtime_base_url_returns_the_adapter(tmp_path, monkeypatch): + """Runtime credential resolution must be routed through the adapter. + + ``ProviderProfile.runtime_base_url`` is the only hook Hermes reads when it + builds the inference base URL; without the override every request bypasses + the adapter and Meta answers 403. + """ + monkeypatch.setenv("MUSE_CODE_SUB_CREDENTIALS", str(tmp_path / "c.json")) + _, registered = _load_plugin() + profile = registered["muse-code"] + assert profile.runtime_base_url( + api_key="LLM|k", default="https://api.meta.ai/v1", env_url="" + ) == profile.base_url + assert re.fullmatch(r"http://127\.0\.0\.1:\d+/v1", profile.base_url) + + def test_adapter_bind_failure_falls_back_to_the_direct_endpoint( tmp_path, monkeypatch, caplog ): @@ -229,6 +245,9 @@ def unavailable(**kwargs): else: sys.modules[submodule] = real assert registered["muse-code"].base_url == "https://api.meta.ai/v1" + assert registered["muse-code"].runtime_base_url( + api_key="LLM|k", default="https://api.meta.ai/v1", env_url="" + ) == "https://api.meta.ai/v1" assert "adapter" in caplog.text.lower()