Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
*,
Expand Down
19 changes: 19 additions & 0 deletions tests/test_muse_code_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand All @@ -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()


Expand Down
Loading