Los invitados del plan gratuito dejan de recibir el desafío del navegador para los connectors: el token de invitado con scope

Dejaste un trabajo programado en un servidor que lee tu calendario, revisa un par de listas y llama a algunas herramientas gestionadas en el plan gratuito. Anoche volvió a fallar. No hay stack trace en el log: solo un enlace impreso y luego el trabajo termina en silencio. Hiciste clic en el enlace una vez y todo volvió a la normalidad, pero la semana que viene puede volver a fallar a las 3 de la madrugada.
Ese enlace es el desafío del navegador del plan gratuito. Ya cubrimos por qué aparece y cómo Hermes lo convirtió en un flujo no bloqueante de «imprime el enlace y consulta el estado». Eso solo resolvía la mitad del problema: a un invitado que no usa más que connectors nunca debieron pedirle que demostrara que es humano.
Un cambio fusionado en la ventana de v0.21.7 cierra esa otra mitad. Los connectors y las herramientas gestionadas ahora usan un token dedicado que ni siquiera entra en el flujo de desafío.
Un invitado en realidad tiene dos tipos de permiso
Empecemos por por qué hay dos tokens. En el plan gratuito un invitado puede hacer dos cosas bastante distintas:
- Llamar a inferencia: hacer que un modelo gratuito genere algo. Eso consume cómputo.
- Llamar a connectors y herramientas gestionadas: leer la cuenta, pegarle al gateway de herramientas, pasar por connectors. Sobre todo reenvío, y no gasta cuota de inferencia.
El desafío, incluida la compuerta de proof-of-work, existe para proteger el cómputo del abuso, así que pertenece solo al lado de la inferencia. En la implementación antigua, un invitado que tocaba un connector seguía pasando por un intercambio de token de inferencia, y por lo tanto por un desafío que nadie necesitaba.
El servidor ahora entiende la diferencia: el intercambio de token anónimo acepta un parámetro purpose, y purpose: "connectors" acuña un JWT cuyo único scope es connectors:invoke. El gateway exime las compuertas de desafío y PoW para él y se niega a usarlo para inferencia.
El claim de scope elige el slot, no el mint que lo produjo
Este es el detalle de diseño que vale la pena recordar: el slot que ocupa un token sale del propio claim scope de su JWT, no de qué mint lo produjo.
- El token de connectors vive en
providers.nous.connectors_token(access_token+expires_at), justo al lado del slot de inferencia existente. - El mint de inferencia no cambia: sigue siendo perezoso, sigue ocurriendo en la primera llamada real a un modelo gratuito y sigue envuelto en el flujo de desafío.
- Quien llama a connectors ahora reutiliza el token que ya esté vivo: un token de inferencia también cubre connectors, así que no se acuña nada extra.
Leer el scope en lugar del origen mantiene limpia la compatibilidad. Un portal más viejo ignora purpose y devuelve un token de inferencia, que por lo tanto aterriza en el slot de inferencia. Y si ese servidor antiguo responde a una petición de connectors con 428, la llamada de connectors recae en el manejo actual del desafío en vez de fallar.
Tres rutas de segundo plano que acuñaban inferencia por accidente
Esta es la parte que vuelve confuso el debugging: ninguna de estas la inicia el usuario:
- Keepalive de auth. Solía refrescar el token de inferencia en cada tick para cualquier invitado, y el gateway de mensajería y el backend de escritorio lo arrancan incondicionalmente. Bajo la aplicación de la política eso significaba un 428 por tick para un invitado que solo usaba connectors, más un desafío oculto en el escritorio. Ahora omite por completo a un invitado, igual que la guarda que ya tenía la función de arranque: los tokens de un invitado se acuñan bajo demanda.
- El catálogo de modelos y los precios de Nous.
_nous_catalog(al que se llega víaprovider_model_ids("nous")) y_resolve_nous_pricing_credentialscorrían en primer plano, así que listar modelos de Nous como invitado desde la CLI podía abrir un desafío de navegador. Ahora ambos corren como llamadores de segundo plano y recaen en la lista curada y los precios públicos, exactamente como ya hacían ante un fallo de credenciales. - Lecturas de cuenta. Estas viven del lado de connectors, así que toman el token de connectors.
El efecto combinado: un desafío ahora solo aparece en el momento en que realmente llamas a un modelo gratuito.
Qué no cambió
- Las cuentas autenticadas, la ruta de inferencia y la forma persistida del invitado quedan intactas.
- Un 403 (
signin_required/access_denied) en un mint de connectors se muestra con el mismo código y el mismo texto que antes: sin texto nuevo de cara al usuario. - No se comparte nada entre los dos mints, así que no se añadieron locks por purpose ni memos con clave: el memo de fallo de mint cubre la creación de identidad (que no tiene purpose), el memo de token de 5 segundos existe solo en el resolver de connectors, y el estado del desafío (work lock, cooldown de abandono) solo lo alcanza un mint que recibe un 428, que en un servidor actual es el mint de inferencia.
Distinguir qué ruta tomó una llamada
Lo que te importa al depurar es a quién fue realmente la llamada. Algunos sitios donde mirar:
# intercambio de token anónimo y estado del desafío (identidad de invitado)
hermes logs --follow | grep -i -E "anon|challenge|connectors"
# estado del lado de la cuenta
hermes account status
# dónde aterrizan los slots de auth (bajo el home del host)
ls -l "$HERMES_HOME/auth.json"
Si ves un enlace de desafío mientras tu carga de trabajo solo toca connectors o herramientas gestionadas, estás en una build antigua: desde v0.21.7 esa ruta ya no pasa por un desafío. Si realmente llamas a un modelo gratuito, un desafío es lo esperado: ese artículo anterior trata de evitar que bloquee ejecuciones desatendidas.
Advertencias
- El token dedicado no sirve para inferencia: el gateway lo rechaza. Copiar el valor de
connectors_tokena una configuración de inferencia solo te compra un fallo de autenticación. - El valor por defecto de imagen del pool gratuito no siguió el cambio de «los suscriptores usan Krea por defecto»; los pools de recursos se siguen decidiendo de forma independiente.
- En un despliegue privado autoalojado o más antiguo, el comportamiento depende de ese servidor: un servidor viejo ignora
purposey todo queda como estaba, y un 428 recae en el manejo antiguo en lugar de reventar.
La conclusión
El valor aquí no es una API nueva: son permisos más finos. El lado que necesita que demuestres que eres humano (inferencia) conserva el desafío; el lado de puro reenvío (connectors, herramientas gestionadas) pasa con un token de scope limitado. Para trabajos programados desatendidos, eso elimina un fallo antes intermitente y difícil de reproducir, no eludiéndolo, sino quitando la razón por la que existía.