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,
modificare il bind nella config (Phase 2 prevede TLS self-signed
pin-by-fingerprint, riusando il materiale di pairing).
| Metodo | Path | Ruolo | Scopo |
|---|---|---|---|
| GET | /agent/health | anonymous | Liveness + uptime + version. |
| GET | /.well-known/metnos.json | anonymous | Discovery: name, channels, capabilities, fingerprint admin key. |
| POST | /agent/turn | user/admin | Esecuzione run_turn. SSE se Accept: text/event-stream, altrimenti JSON. |
| GET | /agent/devices/me | user | Info del device chiamante (id, paired_at, autonomy). |
| POST | /agent/register | anonymous | Registra un device (pairing wrapper). |
| GET | /admin | admin | Dashboard root (HTML). |
| GET | /admin/proposals | admin | Tabella proposte introvertiva (filtro kind). |
| POST | /admin/proposals/{sig_key}/{approve|reject|defer} | admin | Azione su singola proposta. |
| GET | /admin/executors | admin | Catalog con lifecycle. |
| GET | /admin/executors/stats | admin | Counts + daily events per uPlot. |
| GET | /admin/runs | admin | Scheduler runs (ultimi N). |
| GET | /admin/safety | admin | Safety signatures (ADR 0071). |
| GET | /admin/turns | admin | Ultimi N turn (jsonl). |
| GET | /admin/users | admin | Tabella utenti (host + guest). |
| POST | /admin/users | admin | Crea user da form (default owner = host). |
| GET | /admin/users/{id} | admin | Dettaglio utente. |
| POST | /admin/users/{id}/delete | admin | Elimina (cascade su user_channels). |
| POST | /admin/users/{id}/autonomy | admin | Cambia autonomy_level. |
| POST | /admin/users/{id}/channels/{channel}/pair | admin | Issue pairing token + istruzioni. |
| POST | /admin/users/{id}/channels/{channel}/remove | admin | Scollega canale. |
Le rotte di collezione (/admin/proposals,
/admin/executors, /admin/runs,
/admin/safety, /admin/turns,
/admin/users) sono negotiation-driven (vedi cap. 4).
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 e ADR 0082).
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./agent/health, /agent/register, /.well-known/*. 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. Smoke test live verificato 4/5/2026: round-trip ETag/304 OK.
/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 + ogni LLM call in corso | {"phase": "planner|intent|vaglio", "step": N} |
progress | Note utente (notify lunghe operazioni) | {"text": "..."} |
tool_call | Phase 2 — non emesso oggi | {"name", "args"} |
tool_result | Phase 2 — non emesso oggi | {"name", "ok", "preview"} |
final | Chiusura stream con la risposta finale | {"final_answer", "n_steps", ...} |
error | Errore catturato dal turno | {"error_code", "message"} |
Il run_turn gira in un thread executor (sync) mentre la
coroutine main pompa gli eventi sul loop tramite
asyncio.run_coroutine_threadsafe.
tool_call e
tool_result non sono emessi: il Progress attuale
non ha ancora questi callback. Phase 2 estende l'interfaccia.
/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 — safety signatures (ADR 0071).turns.html — ultimi N turn dal jsonl, scrub credenziali gia' applicato (ADR 0082).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 (ADR 0083): 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 (ADR 0082): 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.
/agent/turn chiama run_turn con channel="http" ma non registra un Channel nel daemon. Non bloccante in Phase 1.tool_call / tool_result non emessi. Estensione di Progress in Phase 2.web.AppKey invece di string keys. Refactor cosmetico, non bloccante.~/.local/state/metnos/http_server.lock (stesso pattern di agent_server).GET /agent/health → 200 {ok:true, version:"1.1", uptime_s:...}.
GET /.well-known/metnos.json → 200 con fingerprint admin key.
GET /admin (no auth, da loopback) → 403.
GET /admin (Bearer admin key) → 200 HTML dashboard.
If-None-Match round-trip → 304.
Riferimenti codice: /opt/myclaw/runtime/metnos_http_server.py (entrypoint, factory, lock), http_auth.py (admin key + middleware), http_render.py (Accept negotiation + ETag), http_routes_agent.py (SSE, well-known, devices/me), http_routes_admin.py (collezioni read-only + actions), runtime/templates/ (8+ file Jinja2). Test: runtime/tests/test_http_server.py (12 test). ADR di riferimento: 0078 (HTTP API Phase 1), 0082 (credenziali), 0083 (multi-user), 0085 (allineamento doc 4/5/2026).