http_api — server amministrativo + canale agent uniforme
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).
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).
| Metodo | Path | Scopo |
|---|---|---|
| GET | / | Chat web (HTML + htmx). |
| GET | /agent/health | Liveness + uptime + version. |
| GET | /.well-known/metnos.json | Discovery: name, channels, capabilities, fingerprint admin key. |
| GET | /static/{name} · /manifest.webmanifest · /sw.js | Asset statici e supporto PWA (manifest + service worker). |
| GET | /oauth/callback | Callback OAuth per le skill (es. Google Workspace). |
| GET | /pair/{token} | Consumo di un token di pairing. |
| Metodo | Path | Scopo |
|---|---|---|
| POST | /agent/turn | Esegue run_turn. SSE se Accept: text/event-stream, altrimenti JSON. |
| POST | /agent/turn/submit | Avvia un turno in modo asincrono (esito via stream/stato). |
| GET | /agent/turns/{id}/stream | Stream 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}/feedback | Feedback dell'utente (✓ / ✗ / ↺). |
| POST | /agent/turns/{id}/retry | Ripete il turno. |
| GET | /agent/turns/recent | Ultimi turni. |
| Metodo | Path | Scopo |
|---|---|---|
| GET | /agent/devices/me | Info 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/events | Stream 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. |
| Metodo | Path | Scopo |
|---|---|---|
| GET | /admin | Home della dashboard. |
| GET/POST | /admin/login · POST /admin/logout | Accesso 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 rollback | Promozioni di executor + revisione. |
| GET | /admin/executors · /admin/executors/stats | Catalogo 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/turns | Esecuzioni 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.
Tre ruoli, in ordine crescente di privilegio:
anonymous < user < admin.
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>.
Il middleware confronta il Bearer con la
public_key_b64 di ogni device pairato (tabella
devices di pairing). Match → ruolo user.
user di default, solo se nessun Bearer e' stato presentato e fallito./ (chat), /agent/health, /.well-known/*, gli asset statici/PWA, /oauth/callback, /pair/{token}. Ogni altra path richiede almeno user./admin/ richiede ruolo admin; altrimenti 403.
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.
/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:
| Evento | Quando | Payload |
|---|---|---|
thinking | Apertura stream + intestazione di ogni fase LLM | {"message": "..."} |
progress | Avanzamento dei passi (note operative) | {"stage": N, "label": "..."} |
tool_call | A ogni passo, prima di invocare un tool. Alimenta il breadcrumb live della chat. | {"tool", "step_num", "path", "predicted_remaining", "args"} |
final | Chiusura dello stream con la risposta finale | {"message": "..."} |
error | Errore 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.
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.
/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):
dashboard.html — sommario: catalog count, ultimi 5 turn, ultimi 5 utenti pairati, conteggio safety signatures, latenza media degli ultimi 20 turni.executors.html — tabella catalog con colonne name, kind (handcrafted/synth), lifecycle, uses_30d, last_used.executor_stats.html — chart uPlot con counts giornalieri di esecuzioni per executor.proposals.html — tabella proposte introvertiva con bottoni approva / rifiuta / posticipa.runs.html — ultimi run dello scheduler (cron + recurring user tasks).safety.html — firme di safety.turns.html — ultimi N turn dal jsonl, scrub credenziali gia' applicato.users.html — tabella utenti host + guest (vedi pairing).user_detail.html, user_pair.html — dettaglio + emissione pairing token.channel (Telegram) e pairingTelegram resta canale primario per il dialogo conversazionale (long-poll, basso costo, coerenza UX su mobile). L'HTTP API e' canale alternativo per:
/admin per amministrazione (utenti, scheduler, proposte introvertiva, safety).POST /agent/turn con SSE.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.
~/.local/state/metnos/http_server.lock impedisce due istanze sullo stesso host.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).