Skip to content

Commit 5bfb7a9

Browse files
committed
Meta login reaches the Muse Code subscription: requests go to the address the key mint names (meta.ai only), the connection names the plan; contract 88bb560
Signed-off-by: Maxime Rivest <mrive052@gmail.com>
1 parent fcc367c commit 5bfb7a9

5 files changed

Lines changed: 176 additions & 18 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- **The Meta login reaches the Muse Code subscription, Muse Wonder Full
6+
included.** `auth.login("meta", "device", allow_unverified=True)` was
7+
tested live on 2026-10-12 with a Power Usage plan: login, key, renewal,
8+
then `complete` and `stream` on `meta:muse-wonder-full-1.0`. Requests now
9+
go to the address Meta names when it hands out the key, and only to a
10+
`meta.ai` address; any other address is refused before the key is sent.
11+
The connection's `account_label` names the plan. The method stays
12+
`unverified` because Meta's terms reserve this key for Muse Code
13+
(`docs/managed-login.md` says what that risks). lm15-contract `88bb560`.
14+
315
## 1.2.3 — 2026-10-10
416

517
Two conveniences that make a first program work, and clearer messages

‎CONTRACT_PIN‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
8eedbae4f13dc80666024503256149a7bbf39388
1+
88bb560118243bd6601cf33e6addcb586b568469

‎docs/managed-login.md‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ store.
147147
| ChatGPT / Codex | LM15-owned device login | Separate memory-only login and inference observed **2026-09-23**. Device persistence/renewal were not separately tested; permission and billing remain **unverified**. |
148148
| OpenRouter | Browser approval → API key | Key issuance, $1 limit inspection, model discovery, inference and fresh-process reuse observed **2026-09-23**. No refresh-token flow; broader support review remains pending. |
149149
| GitHub Copilot | Device login → Copilot token | github.com login, catalog, inference, fresh-process persistence and one early renewal observed **2026-09-23**. Uses the public Copilot Chat app registration; permission and billing remain **unverified**. |
150-
| Claude (loopback), Meta, Kimi Code | Other LM15-owned login methods | **unverified**: implemented, no live LM15 receipt; `login(..., allow_unverified=True)` to try |
150+
| Meta (Muse Code subscription) | Device login → the account's Muse Code key | Login, key mint, re-mint and inference on `muse-wonder-full-1.0` observed **2026-10-12** (Power Usage plan). Stays **unverified**: Meta's terms reserve this key for Muse Code (see below). |
151+
| Claude (loopback), Kimi Code | Other LM15-owned login methods | **unverified**: implemented, no live LM15 receipt; `login(..., allow_unverified=True)` to try |
151152
| Radius | — | **unavailable**: its model protocol is not in lm15-python |
152153

153154
An unverified method is never offered by the default picker. Promotion to
@@ -180,6 +181,33 @@ and the wall clock were not falsified. This does not prove an unattended full
180181
expiry cycle, provider permission or included billing. Both methods still require
181182
`allow_unverified=True`, and the existing CLI credential route remains available.
182183

184+
### Meta: the Muse Code subscription (Muse Wonder Full)
185+
186+
```python
187+
auth.login("meta", "device", ui=TerminalUI(), allow_unverified=True)
188+
router = LMRouter(RouterConfig(auth=auth))
189+
router.lm("meta:muse-wonder-full-1.0")
190+
```
191+
192+
You approve a code at `auth.meta.com` with the Meta account that holds the
193+
Muse Code plan. Meta then hands out that account's Muse Code key. It is the
194+
same key every time, so lm15 renewing it daily never logs Muse Code out. The
195+
connection's `account_label` names the plan (for example "Muse Code Power
196+
Usage"), or says "no Muse Code subscription (pay-as-you-go)" when Meta
197+
bills the key per token. `muse-wonder-full-1.0` needs the Power or Maximum
198+
Usage plan; `muse-wonder-1.0` and the Muse Spark models work on every plan.
199+
Under a managed `Auth`, a `META_API_KEY` in the environment is not used, so
200+
requests stay on the plan. Today only the `meta:` route uses the connection.
201+
202+
**Read this before using it.** Meta's Model API terms (updated 2026-10-09,
203+
§ 3.2) say the Muse Code key "may not be used to access the Services
204+
outside the coding harness". lm15 is not Muse Code, so this use is outside
205+
those terms. The account at risk is your Meta account, the one shared with
206+
Facebook, Instagram and WhatsApp. Subscription traffic is also in
207+
Contributor mode by default (§ 3.3): Meta may train on your prompts unless
208+
you turn that off in Muse Code's `/settings`. lm15 identifies itself as
209+
lm15 and does not imitate Muse Code.
210+
183211
### OpenRouter: no manual key handling, but still a key
184212

185213
```python

‎lm15/login/flows/meta.py‎

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
"""
2-
lm15.login.flows.meta — Meta (Muse subscription) device login + key mint.
2+
lm15.login.flows.meta — Meta (Muse Code subscription) device login + key mint.
33
44
RFC 8628 device authorization against ``auth.meta.com`` (JSON replies);
55
the identity token it returns is **not** accepted for inference, so it is
6-
exchanged at the Muse Code key-mint endpoint for a Model API key that
7-
lives about a day. Renewal is a re-mint with the stored identity token
6+
exchanged at the Muse Code key-mint endpoint for the account's Muse Code
7+
Model API key. Renewal is a re-mint with the stored identity token
88
(``renewal="remint"``); the identity token itself is not renewable, so a
9-
401/403 from the mint is a permanent rejection (``needs_login``). Pi
10-
0.87.0 ``oauth/meta.ts`` is the implementation reference.
11-
12-
**Availability: unverified** (no live LM15 receipt).
9+
401/403 from the mint is a permanent rejection (``needs_login``). Muse
10+
Code 1.4.6 is the reference (Pi 0.87.0 ``oauth/meta.ts`` was the first).
11+
12+
Live 2026-10-12 (lm15-contract ``receipts/2026-10-12-muse-code/``): the
13+
mint answers the same key on every call, so a re-mint never revokes the
14+
key Muse Code itself holds; the reply also names the API address
15+
(``base_url``) and the subscription (``is_subs_active``,
16+
``subs_tier_name``). The key serves all three Meta wires, and
17+
``muse-wonder-full-1.0`` (Power and Maximum Usage plans only) answers on
18+
it. The address is used as given when it is an https ``meta.ai`` host and
19+
refused otherwise: the key is never sent anywhere else.
20+
21+
**Availability: unverified.** Meta's Model API terms (2026-10-09, § 3.2)
22+
say the Muse Code key "may not be used to access the Services outside the
23+
coding harness"; lm15 is not that harness. The descriptor says so.
1324
"""
1425

1526
from __future__ import annotations
@@ -26,12 +37,17 @@
2637
DEVICE_AUTHORIZATION_URL = "https://auth.meta.com/oidc/device/authorization/"
2738
DEVICE_TOKEN_URL = "https://auth.meta.com/oidc/device/token/"
2839
KEY_MINT_URL = "https://api.meta.ai/muse-code/key"
29-
KEY_LIFETIME_S = 24 * 60 * 60.0
40+
KEY_LIFETIME_S = 24 * 60 * 60.0 # no expiry in the reply; a daily re-mint is free (same key, 2026-10-12)
41+
TERMS_NOTE = ("Meta's Model API terms (§ 3.2) reserve the Muse Code key for Muse Code; using it here is outside "
42+
"those terms and puts the Meta account at risk. Subscription traffic is Contributor mode by default "
43+
"(Meta may train on it).")
3044

3145
METHOD_DEVICE = LoginMethod(
3246
id="device", label="Sign in with Meta (Muse subscription)", kind="account", flow="device_code",
33-
availability="unverified", reason="no live receipt yet", delivery=("device",), subscription=True,
34-
billing_note="Minted Model API keys are tied to the Muse subscription; verify entitlement on your account.",
47+
availability="unverified", reason="login, mint and inference observed 2026-10-12; " + TERMS_NOTE,
48+
delivery=("device",), subscription=True,
49+
billing_note="Billed to the Muse Code plan when it is active (muse-wonder-full-1.0 needs Power or Maximum "
50+
"Usage); without one, Meta bills the key pay-as-you-go.",
3551
)
3652

3753
DESCRIPTOR = ProviderDescriptor(
@@ -55,7 +71,36 @@ def _positive(value: Any) -> float | None:
5571
return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) and value > 0 else None
5672

5773

58-
def _mint(ctx: LoginContext, identity_token: str) -> Material:
74+
def _api_base(value: Any) -> str | None:
75+
"""The mint's ``base_url`` when the key may go there: https, a
76+
``meta.ai`` host, no credentials in the URL. ``None`` when absent."""
77+
if value is None:
78+
return None
79+
parsed = urllib.parse.urlsplit(value) if isinstance(value, str) else None
80+
host = (parsed.hostname or "") if parsed else ""
81+
if parsed is None or parsed.scheme != "https" or parsed.username or parsed.password or parsed.query \
82+
or not (host == "meta.ai" or host.endswith(".meta.ai")):
83+
raise LoginDenied("Meta named an API address outside meta.ai for the minted key; refusing to send it there")
84+
return value.rstrip("/")
85+
86+
87+
def _plan(body: dict[str, Any]) -> str | None:
88+
"""Display text for the account's plan; ``None`` when the reply is silent."""
89+
active = body.get("is_subs_active")
90+
if active is True:
91+
tier = body.get("subs_tier_name")
92+
return tier if isinstance(tier, str) and tier else "Muse Code subscription"
93+
if active is False:
94+
return "no Muse Code subscription (pay-as-you-go)"
95+
return None
96+
97+
98+
def _result(material: Material, body: dict[str, Any]) -> LoginResult:
99+
return LoginResult(material=material, label="Meta (Muse subscription)", account_label=_plan(body),
100+
renewal="remint")
101+
102+
103+
def _mint(ctx: LoginContext, identity_token: str) -> tuple[Material, dict[str, Any]]:
59104
ctx.notify(ProgressNotice(stage="exchange", message="Enabling Meta Model API access…"))
60105
reply = http_json(ctx, KEY_MINT_URL, {}, headers={
61106
"Authorization": f"Bearer {identity_token}", "x-api-version": "1.0.0",
@@ -68,9 +113,13 @@ def _mint(ctx: LoginContext, identity_token: str) -> Material:
68113
if not isinstance(key, str) or not key:
69114
action = _http_url(reply.body.get("action_url"))
70115
raise LoginDenied("Meta did not issue an API key" + (f"; complete setup at {action}" if action else ""))
116+
base_url = _api_base(reply.body.get("base_url"))
71117
now_ms = int(ctx.wall_clock() * 1000)
72-
return {"type": "oauth", "access": key, "refresh": identity_token, "issued_at": now_ms,
73-
"lifetime_s": KEY_LIFETIME_S, "expires": int(now_ms + KEY_LIFETIME_S * 1000)}
118+
material: Material = {"type": "oauth", "access": key, "refresh": identity_token, "issued_at": now_ms,
119+
"lifetime_s": KEY_LIFETIME_S, "expires": int(now_ms + KEY_LIFETIME_S * 1000)}
120+
if base_url is not None:
121+
material["base_url"] = base_url
122+
return material, reply.body
74123

75124

76125
class MetaFlow(ProviderFlow):
@@ -111,13 +160,15 @@ def poll() -> DeviceStep:
111160
raise LoginDenied(f"Meta device token request failed (HTTP {reply.status})")
112161

113162
identity = run_device_flow(ctx, poll, interval_s=interval, expires_in_s=expires_in)
114-
return LoginResult(material=_mint(ctx, identity), label="Meta (Muse subscription)", renewal="remint")
163+
return _result(*_mint(ctx, identity))
115164

116165
def renew(self, ctx: LoginContext, material: Material, settings: dict[str, str]) -> LoginResult:
117166
identity = material.get("refresh")
118167
if not isinstance(identity, str) or not identity:
119168
raise LoginDenied("Meta credential has no identity token to re-mint with")
120-
return LoginResult(material=_mint(ctx, identity), label="Meta (Muse subscription)", renewal="remint")
169+
return _result(*_mint(ctx, identity))
121170

122171
def request_auth(self, material: Material, settings: dict[str, str]) -> RequestAuth:
123-
return RequestAuth(credential=ApiKey(material["access"]))
172+
base_url = material.get("base_url")
173+
return RequestAuth(credential=ApiKey(material["access"]),
174+
base_url=base_url if isinstance(base_url, str) and base_url else None)

‎tests/test_login.py‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,3 +856,70 @@ def test_external_claude_cli_source_reads_in_place(sandbox: Path, monkeypatch: p
856856
assert auth.request_auth("claude-code").credential.value == ACCESS
857857
auth.logout("claude-code")
858858
assert json.loads(claude.read_text(encoding="utf-8"))["claudeAiOauth"]["accessToken"] == ACCESS # the tool's file is untouched
859+
860+
861+
# ─── Meta (Muse Code subscription): the mint reply decides address and plan ──
862+
863+
864+
class FakeMeta:
865+
"""auth.meta.com device endpoints and the Muse Code key mint, shaped as
866+
observed live 2026-10-12 (lm15-contract receipts/2026-10-12-muse-code/)."""
867+
868+
def __init__(self, mint: dict) -> None:
869+
self.mint = mint
870+
self.calls: list[str] = []
871+
872+
def __call__(self, request: urllib.request.Request, timeout: float):
873+
url = request.full_url
874+
self.calls.append(url)
875+
if url.endswith("/oidc/device/authorization/"):
876+
return _Reply(200, {"device_code": f"{SENTINEL}-device", "user_code": "ABCD-1234",
877+
"verification_uri": "https://auth.meta.com/oauth/device/", "interval": 1,
878+
"expires_in": 600})
879+
if url.endswith("/oidc/device/token/"):
880+
return _Reply(200, {"access_token": f"{SENTINEL}-identity", "token_type": "Bearer"})
881+
if url == "https://api.meta.ai/muse-code/key":
882+
return _Reply(200, self.mint)
883+
raise AssertionError(f"unexpected request {url}")
884+
885+
886+
def test_meta_subscription_key_goes_where_the_mint_says(sandbox: Path, monkeypatch: pytest.MonkeyPatch) -> None:
887+
clock = Clock()
888+
server = FakeMeta({"api_key": ACCESS, "base_url": "https://eu.api.meta.ai/v1/", "is_subs_active": True,
889+
"subs_tier_name": "Muse Code Power Usage"})
890+
auth = make_auth(sandbox, clock, server)
891+
connection = auth.login("meta", "device", ui=ScriptUI(), allow_unverified=True)
892+
assert connection.account_label == "Muse Code Power Usage"
893+
no_secret(connection, auth.status("meta"))
894+
# The environment key is never a fallback under a managed Auth (AUTH-15):
895+
# a subscriber's requests stay on the plan.
896+
transport = FakeTransport([FakeResponse(200, json.dumps({
897+
"id": "r", "object": "response", "status": "completed", "model": "muse-wonder-full-1.0",
898+
"output": [{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "OK"}]}],
899+
"usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2}}).encode())])
900+
router = LMRouter(RouterConfig(auth=auth, env={"META_API_KEY": "payg"}, transport=transport))
901+
lm = router.lm("meta:muse-wonder-full-1.0")
902+
assert "managed connection" in lm.credential_origin()
903+
from lm15 import Request
904+
905+
assert lm.complete(Request(model="muse-wonder-full-1.0", messages=(Message.user("hi"),))).text == "OK"
906+
sent = transport.requests[0]
907+
assert sent.url == "https://eu.api.meta.ai/v1/responses"
908+
assert {k.lower(): v for k, v in sent.headers}["authorization"] == f"Bearer {ACCESS}"
909+
910+
911+
@pytest.mark.parametrize("base_url", ["http://api.meta.ai/v1", "https://api.meta.ai.example.com/v1",
912+
"https://user:pw@api.meta.ai/v1", 42])
913+
def test_meta_mint_address_outside_meta_ai_is_refused(sandbox: Path, base_url: object) -> None:
914+
auth = make_auth(sandbox, Clock(), FakeMeta({"api_key": ACCESS, "base_url": base_url}))
915+
with pytest.raises(AuthOperationError) as info:
916+
auth.login("meta", "device", ui=ScriptUI(), allow_unverified=True)
917+
no_secret(info.value)
918+
assert auth.status("meta").presence == "absent"
919+
920+
921+
def test_meta_without_a_plan_says_pay_as_you_go(sandbox: Path) -> None:
922+
auth = make_auth(sandbox, Clock(), FakeMeta({"api_key": ACCESS, "base_url": "https://api.meta.ai/v1",
923+
"is_subs_active": False}))
924+
connection = auth.login("meta", "device", ui=ScriptUI(), allow_unverified=True)
925+
assert connection.account_label == "no Muse Code subscription (pay-as-you-go)"

0 commit comments

Comments
 (0)