Files
hermes-agent/tui_gateway/methods_free_tier.py
Robin Fernandes 2a94ca80e7 fix(free-tier): review round 2 — route-gate the allowance verdict, keep policy/billing 403s, pool the provision RPC, guard the retry race
Should-fix
- _is_genuine_nous_rate_limit: the structured rate_limited verdict counts only
  on the welcome host; a paid-host 429 keeps main's exhausted-bucket rule.
- _nous_welcome_tier: the route-keyed dark-tier 403 applies only to a 403 that
  matches neither the content-policy nor the billing patterns, so a safety
  refusal or billing wall on the welcome host keeps its own recovery.
- free_tier.provision joins _LONG_HANDLERS (a forced mint + lock waits +
  re-inventory no longer block the RPC reader).
- retry_bootstrap_mint: under the lock, a build that found no identity never
  overwrites a record that has one (the loop racing the user's click).

Simplifications from the review
- _raise_for_anon_status is a (status, error) table; retryable derives from
  ANON_TERMINAL_CODES once (a bare 401 on sign-up now rides the ladder
  instead of dying for the process).
- classify_mint_exception is public and pure; the hand-built failure dict in
  free_tier.provision is gone (the memo is the one source).
- SetupRecord carries the memo payload as one `failure` dict instead of three
  unpacked fields.
- _welcome_surface_kind is a closed table with a "refused" default;
  _welcome_outage_copy excludes the classifier's `unknown` catch-all.
- FREE_TIER_RATE_LIMIT_CHAT is CARD + the sign-in tail, not a slice.
- Copy tests assert the contract (model named, tail present/absent) instead
  of freezing whole sentences.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 20:44:42 +05:30

93 lines
4.5 KiB
Python

"""Nous free-tier JSON-RPC handlers: a renderer reads the profile's local auth state (pull); nothing
is pushed except the boot bootstrap's one ``setup.ready`` event. ``free_tier.status`` answers from the
auth store with zero network and zero side effects; ``free_tier.provision`` is the explicit retry when
the boot bootstrap could not create the identity (desktop-only entry); ``free_tier.ack_notice``
persists the one-time notice flag on the free-tier identity itself, so it dies with that identity.
Bodies are rebound onto server.py's globals (method_ctx.bind_module) and reference them bare.
"""
import logging
from .method_ctx import HandlerRegistry, bind_module
logger = logging.getLogger(__name__)
_registry = HandlerRegistry()
method = _registry.method
_profile_scoped = _registry.profile_scoped
@method("free_tier.status")
@_profile_scoped
def _(rid, params: dict) -> dict:
"""``{has_guest, enabled, available, notice_pending, model, label}`` for the focused profile.
``available`` = an identity exists AND the tier is on: the free tier (connectors, and the model
when nothing else carries inference) is there for this install. Whether inference actually runs
on it is a ROUTE question answered by ``setup.runtime_check.free_tier``, never by this flag.
``notice_pending`` is true until ``free_tier.ack_notice`` ran for this identity.
A pure read. The identity is created by the boot bootstrap (``free_tier_bootstrap``), never as
a side effect of a client polling this method (NS-845 Q1.2)."""
try:
from hermes_cli import anon_auth
has_guest = anon_auth.has_guest()
enabled = anon_auth.guest_enabled()
payload = {
"has_guest": has_guest, "enabled": enabled, "available": has_guest and enabled,
"notice_pending": bool(has_guest and enabled and anon_auth.guest_notice_pending()),
"model": anon_auth.GUEST_MODEL, "label": anon_auth.FREE_TIER_LABEL}
if enabled and not has_guest:
# Why there is no identity, when the last attempt to make one failed:
# ``{error, error_code, retryable, retry_after}`` (the mint memo's verdict).
payload.update(anon_auth.last_mint_failure() or {})
return _ok(rid, payload)
except Exception as e:
return _err(rid, 5090, str(e))
@method("free_tier.provision")
@_profile_scoped
def _(rid, params: dict) -> dict:
"""Explicit retry of the free-tier set-up for the focused profile: adopt the shared store's
identity, else mint one (blocking, short timeout). The boot bootstrap normally did this already;
the desktop calls this when the record says the identity is missing (portal down at boot, gate
turned on later) and the user asks again. The user's click is the one attempt that may run
inside the mint memo's cooldown. ``{has_guest, enabled}``, plus
``{error, error_code, retryable, retry_after}`` when the portal refused."""
try:
from hermes_cli import anon_auth
from hermes_cli import free_tier_bootstrap
enabled = anon_auth.guest_enabled()
if enabled and not anon_auth.has_guest():
if free_tier_bootstrap.current_record() is not None and not params.get("profile"):
# The launch profile: refresh the boot record too, so ``setup.status`` and the
# ``setup.ready`` listeners move with the outcome.
free_tier_bootstrap.retry_bootstrap_mint(force=True)
else:
try:
anon_auth.ensure_portal_identity(explicit=True, force=True)
except Exception as exc: # memoised by the primitive before it re-raised
logger.info("free tier provisioning failed: %s", exc)
has_guest = anon_auth.has_guest()
payload = {"has_guest": has_guest, "enabled": enabled}
if enabled and not has_guest:
payload.update(anon_auth.last_mint_failure() or {})
return _ok(rid, payload)
except Exception as e:
return _err(rid, 5092, str(e))
@method("free_tier.ack_notice")
@_profile_scoped
def _(rid, params: dict) -> dict:
"""Mark the availability notice shown on the free-tier identity. ``acked`` is false when there is
no free-tier identity to mark (nothing to show again either)."""
try:
from hermes_cli import anon_auth
return _ok(rid, {"acked": bool(anon_auth.mark_guest_notice_shown())})
except Exception as e:
return _err(rid, 5091, str(e))
def register(server) -> None:
bind_module(globals(), server, skip=("_",))