Il secondo modo di parlare a Metnos: HTTP, browser, curl. Un server aiohttp sulla porta 8770 che espone l'agente (chat web + API JSON/SSE) e la dashboard di amministrazione. Implementazione: runtime/metnos_http_server.py + http_auth.py + http_render.py + http_routes_agent.py + http_routes_admin.py + i template Jinja2.
Affianca, non sostituisce, channel (Telegram resta un canale primario per il dialogo). Vedi anche pairing (multi-utente).
← Indice documentazione Microprogettazione › http_api

Metnos

http_api — server amministrativo + canale agent uniforme
Microprogettazione — la HTTP API sulla porta 8770.
Pubblico: chi vuole capire come si parla a Metnos via HTTP/browser/curl.

Lettura: 12 minuti.

Indice

  1. Cosa fa: secondo server HTTP
  2. Gli endpoint
  3. Autenticazione e ruoli
  4. Content negotiation + ETag
  5. SSE su /agent/turn
  6. Dashboard /admin (htmx + uPlot)
  7. Rapporto con channel (Telegram) e pairing
  8. Limiti noti

1. Cosa fa: secondo server HTTP

La HTTP API è il secondo server di Metnos: in ascolto su porta 8770, separata dal server di pairing dei device remoti (porta 8765). Espone tre cose: una dashboard amministrativa in HTML per il host, un canale agent uniforme (POST /agent/turn) per chi preferisce HTTP/SSE al posto di Telegram, una discovery del nodo (/.well-known/metnos.json) per i client.

La scelta di un secondo server (anziché estendere il pairing server o passare tutto da Telegram) e' motivata da tre vincoli: (a) l'admin UI deve essere a portata di browser senza che l'host debba aprire Telegram per gestire utenti, scheduler, proposte introvertiva; (b) il canale agent HTTP serve come fondamento per il client Rust e per integrazioni a venire; (c) il pairing server e' minimale per design (porta 8765, solo register-device) e non va appesantito.

Bind di default: 127.0.0.1:8770. Per esporre alla LAN si modifica il bind nella config; per andare oltre la LAN va aggiunto un terminatore TLS davanti al server (vedi cap. 8).

Ciclo di una richiesta — auth → ruolo → rotta → risposta Richiesta HTTP porta 8770 auth_middleware Bearer · cookie · LAN ruolo anon < user < admin rotta + negotiation Accept + ETag risposta HTML · JSON SSE stream Il middleware nega con 403 prima del dispatch se il ruolo non basta; le whitelist anonime saltano il gate. Su /agent/turn con Accept: text/event-stream la risposta diventa uno stream SSE (cap. 5).

2. Gli endpoint

Le rotte sono dichiarate come liste di tuple in http_routes_agent.py (lato agente) e http_routes_admin.py (lato amministrazione), montate insieme dal server. In totale sono diverse decine; qui sono raggruppate per area. Ogni path richiede almeno il ruolo indicato (vedi cap. 3).

Pubbliche (anonymous)

MetodoPathScopo
GET/Chat web (HTML + htmx).
GET/agent/healthLiveness + uptime + version.
GET/.well-known/metnos.jsonDiscovery: name, channels, capabilities, fingerprint admin key.
GET/static/{name} · /manifest.webmanifest · /sw.jsAsset statici e supporto PWA (manifest + service worker).
GET/oauth/callbackCallback OAuth per le skill (es. Google Workspace).
GET/pair/{token}Consumo di un token di pairing.

Agente: turni (user/admin)

MetodoPathScopo
POST/agent/turnEsegue run_turn. SSE se Accept: text/event-stream, altrimenti JSON.
POST/agent/turn/submitAvvia un turno in modo asincrono (esito via stream/stato).
GET/agent/turns/{id}/streamStream SSE di un turno (riallacciabile dopo un refresh).
GET/agent/turns/{id}Stato/esito di un turno (resumable: la navigazione via non è un errore).
POST/agent/turns/{id}/feedbackFeedback dell'utente (✓ / ✗ / ↺).
POST/agent/turns/{id}/retryRipete il turno.
GET/agent/turns/recentUltimi turni.

Agente: sessione, dialoghi, immagini (user)

MetodoPathScopo
GET/agent/devices/meInfo del device chiamante (id, paired_at, autonomy).
POST/agent/session/{register,takeover,ping,revoke}Ciclo di vita della sessione device (sessione attiva unica + takeover).
GET/agent/session/eventsStream SSE eventi sessione (es. session_revoked).
GET/POST/agent/dialog/{id}/{form,submit,cancel,preview,context}Dialoghi interattivi (richiesta input, scelta con anteprima/contesto).
GET/agent/photos/web · /agent/photos/{turn}/{idx} · /agent/gallery/{turn}Servizio immagini e galleria di un turno.

Amministrazione (admin)

MetodoPathScopo
GET/adminHome della dashboard.
GET/POST/admin/login · POST /admin/logoutAccesso amministrativo.
GET/admin/changes + POST /{id}/{accept|reject|stage|rollback|retry}Ciclo di vita unificato delle modifiche (change_intent).
GET/admin/proposals (+ /introvertiva, /telos + azioni)Triage delle proposte (introspettiva e telos).
GET/admin/promotions · /promotions/review + POST rollbackPromozioni di executor + revisione.
GET/admin/executors · /admin/executors/statsCatalogo con lifecycle + conteggi/eventi per i grafici uPlot.
GET/admin/timers + POST /{name}/{enable|disable|fire}Tutti i timer di sistema: vedi, abilita, disabilita, esegui ora.
GET/admin/runs · /admin/builds · /admin/safety · /admin/turnsEsecuzioni scheduler, build, firme di safety, ultimi turni.
GET/admin/users (+POST) · /{id} (+ delete/update/autonomy/channels)Gestione utenti (host + ospiti) e canali abbinati.

Le rotte di collezione (/admin/executors, /admin/runs, /admin/safety, /admin/turns, /admin/users, …) sono negotiation-driven: stessa URL, HTML per il browser o JSON per curl (vedi cap. 4). Non esiste un /agent/register: la registrazione del device avviene via /agent/session/register oppure tramite il flusso di pairing.

3. Autenticazione e ruoli

Tre ruoli, in ordine crescente di privilegio: anonymous < user < admin.

Admin key

File ~/.config/metnos/admin.key (mode 0600), 256-bit hex generato con secrets.token_hex(32) al primo boot del server. Nei log compare solo il fingerprint sha256 (primi 16 hex). La admin key e' anche la master della cifratura credenziali (vedi cap. 7).

Login web: POST /admin/login con la admin key in chiaro; risposta set-cookie con TTL 7 giorni (HttpOnly, SameSite=Strict). Da CLI/curl: header Authorization: Bearer <admin.key>.

Bearer token device

Il middleware confronta il Bearer con la public_key_b64 di ogni device pairato (tabella devices di pairing). Match → ruolo user.

LAN trusted + whitelist anonymous

La distinzione «LAN trusted» e' figlia del modello self-hosted: sulla rete di casa il rischio e' contenuto, e l'host vuole curl/htmx senza header. La policy si irrigidisce appena il bind si apre alla WAN (dove va aggiunto il TLS).

4. Content negotiation + ETag

Le rotte di collezione esaminano Accept: text/html → fragment Jinja2 (htmx-friendly); altrimenti JSON. In entrambi i casi viene calcolato un ETag (sha256 del payload, primi 16 hex). Se il chiamante ripresenta If-None-Match con lo stesso valore, la risposta e' 304 Not Modified senza body.

5. SSE su /agent/turn

Quando il client manda Accept: text/event-stream, POST /agent/turn apre uno stream e installa un _SSEProgress (implementa l'interfaccia runtime.progress.Progress) che il run_turn invoca a ogni step. Eventi emessi:

EventoQuandoPayload
thinkingApertura stream + intestazione di ogni fase LLM{"message": "..."}
progressAvanzamento dei passi (note operative){"stage": N, "label": "..."}
tool_callA ogni passo, prima di invocare un tool. Alimenta il breadcrumb live della chat.{"tool", "step_num", "path", "predicted_remaining", "args"}
finalChiusura dello stream con la risposta finale{"message": "..."}
errorErrore catturato dal turno{"message": "..."}

Il run_turn gira in un thread executor (sincrono) mentre la coroutine principale pompa gli eventi sullo stream tramite asyncio.run_coroutine_threadsafe.

Niente evento tool_result separato. L'esito di ogni passo non viaggia come evento a sé: è già riflesso nel path del tool_call successivo (i passi completati diventano badge pieni nel breadcrumb) e, alla fine, nel messaggio final. Così la chat disegna l'avanzamento in tempo reale senza un evento dedicato.

6. Dashboard /admin (htmx + uPlot)

Stack frontend deliberatamente minimo: htmx + Jinja2 + uPlot via CDN, niente build step, niente framework CSS. Template compatti (<50 righe ciascuno) sotto runtime/templates/. Stile system-ui pulito, palette allineata al resto della doc canonica (navy / sage / bronze).

Pagine principali (rendering Jinja2 + frammenti htmx):

7. Rapporto con channel (Telegram) e pairing

Telegram resta canale primario per il dialogo conversazionale (long-poll, basso costo, coerenza UX su mobile). L'HTTP API e' canale alternativo per:

  1. Host browser: dashboard /admin per amministrazione (utenti, scheduler, proposte introvertiva, safety).
  2. Client Rust: turn remoto da PC Windows/Linux pairato, via POST /agent/turn con SSE.
  3. curl/script: automazioni casalinghe veloci (curl -H "Authorization: Bearer..." http://localhost:8770/agent/turn).

Il run_turn invocato via HTTP riceve channel="http"; non viene registrato un Channel formale nel channels.daemon (modello poll-based incompatibile con request/response). Per il vaglio e per policy l'effetto e' equivalente: gli step passano dagli stessi gate, l'audit conserva channel="http" nel turn log.

Multi-user: l'HTTP API e' il punto di amministrazione della tabella users.db. Pairing dei guest via POST /admin/users/{id}/channels/telegram/pair: l'admin riceve un token short-lived, lo passa al guest, il guest manda /start <token> al bot. Il flusso e' descritto in pairing cap. 6.

Credenziali: la admin key del server HTTP e' anche la master della cifratura simmetrica per ~/.config/metnos/credentials/<domain>.json.age. Chi controlla ~/.config/metnos/admin.key (mode 0600) puo' leggere le credenziali. Backup offline raccomandato.

8. Limiti noti

Verifica veloce dello stato. GET /agent/health → 200 {ok:true, version:"...", uptime_s:...}. GET /.well-known/metnos.json → 200 con il fingerprint della admin key. GET /admin senza credenziali → 403; con Bearer admin key → 200 (dashboard HTML). Un round-trip If-None-Match su una collezione → 304.

Riferimenti codice: runtime/metnos_http_server.py (entrypoint, factory, lock), http_auth.py (admin key + middleware), http_render.py (negoziazione Accept + ETag), http_routes_agent.py (rotte agente, SSE, well-known), http_routes_admin.py (collezioni + azioni admin), runtime/templates/ (template Jinja2).