Free-tier guests stop getting the browser challenge for connectors: the scoped guest token


You left a scheduled job on a server that reads your calendar, checks a couple of lists and calls a few managed tools on the free tier. It failed again last night. There is no stack trace in the log — just a printed link, and then the job ends quietly. You clicked the link once and everything went back to normal, but next week it can fail at 3am all over again.

That link is the free-tier browser challenge. We covered why it appears and how Hermes turned it into a non-blocking “print the link, poll the state” flow. That only solved half the problem: a guest using nothing but connectors should never have been asked to prove they are human in the first place.

A change merged in the v0.21.7 window closes that half. Connectors and managed tools now use a dedicated token that never enters the challenge flow at all.

A guest actually holds two kinds of permission

Start with why there are two tokens. On the free tier a guest can do two quite different things:

  • Call inference — make a free model generate something. That spends compute.
  • Call connectors and managed tools — read the account, hit the tool gateway, route through connectors. Mostly forwarding, and it does not spend inference quota.

The challenge, including the proof-of-work gate, exists to protect compute from abuse, so it belongs on the inference side only. In the old implementation, a guest touching a connector was still dragged through an inference token exchange — and therefore through a challenge nobody needed.

The server now understands the difference: the anonymous token exchange takes a purpose parameter, and purpose: "connectors" mints a JWT whose only scope is connectors:invoke. The gateway waives the challenge and PoW gates for it, and refuses to use it for inference.

The scope claim picks the slot, not the mint that produced it

This is the design detail worth remembering: which slot a token fills comes from its own JWT scope claim, not from which mint produced it.

  • The connectors token lives in providers.nous.connectors_token (access_token + expires_at), right next to the existing inference slot.
  • The inference mint is unchanged: still lazy, still on the first actual free-model call, still wrapped in the challenge flow.
  • Connector callers now reuse whichever token is already live — an inference token covers connectors too, so nothing extra is minted.

Reading the scope instead of the origin keeps compatibility clean. An older portal ignores purpose and returns an inference token, which therefore lands in the inference slot. And if that older server answers a connectors request with 428, the connectors call falls back to today’s challenge handling instead of failing.

Three background paths that used to mint inference by accident

This is the part that makes debugging confusing — none of these are user-initiated:

  • Auth keepalive. It used to refresh the inference token every tick for any guest, and the messaging gateway and desktop backend start it unconditionally. Under enforcement that meant a 428 per tick for a connectors-only guest, plus a hidden challenge on the desktop. It now skips a guest entirely, matching the guard the start function already had: a guest’s tokens are minted on demand.
  • The Nous model catalog and pricing. _nous_catalog (reached through provider_model_ids("nous")) and _resolve_nous_pricing_credentials ran in the foreground, so listing Nous models as a guest from the CLI could open a browser challenge. Both now run as background callers and fall back to the curated list and public pricing, exactly as they already did on a credential failure.
  • Account reads. These sit on the connectors side, so they take the connectors token.

The combined effect: a challenge now appears only at the moment you actually call a free model.

What did not change

  • Signed-in accounts, the inference path and the persisted guest shape are untouched.
  • A 403 (signin_required / access_denied) on a connectors mint surfaces with the same code and copy as before — no new user-facing copy.
  • Nothing is shared between the two mints, so no per-purpose locks or keyed memos were added: the mint-failure memo covers identity creation (which has no purpose), the 5-second token memo exists only on the connectors resolver, and challenge state (work lock, give-up cooldown) is only reached by a mint that gets a 428 — which on a current server is the inference mint.

Telling which path a call took

What you care about while debugging is who the call actually went to. A few places to look:

# anonymous token exchange and challenge state (guest identity)
hermes logs --follow | grep -i -E "anon|challenge|connectors"

# account-side status
hermes account status

# where the auth slots land (under the host home)
ls -l "$HERMES_HOME/auth.json"

If you see a challenge link while your workload only touches connectors or managed tools, you are on an older build: from v0.21.7 that path no longer goes through a challenge. If you genuinely call a free model, a challenge is expected — that earlier post is about keeping it from blocking unattended runs.

Caveats

  • The dedicated token is not usable for inference — the gateway refuses it. Copying the value from connectors_token into an inference config only buys you an authentication failure.
  • The free pool’s image default did not follow the “subscribers default to Krea” change; resource pools are still decided independently.
  • On a self-hosted or older private deployment the behaviour depends on that server: an old server ignores purpose and everything stays as it was, and a 428 falls back to the old handling rather than crashing.

The takeaway

The value here is not a new API — it is finer-grained permissions. The side that needs you to prove you are human (inference) keeps the challenge; the pure forwarding side (connectors, managed tools) passes with a scope-limited token. For unattended scheduled jobs that removes a previously intermittent, hard-to-reproduce failure — not by working around it, but by removing the reason it existed.