← Indice documentazione Guida all'architettura › channel

Metnos

channel — come Metnos riceve e risponde dal mondo
Guida ai canali conversazionali

I canali traducono browser, API HTTP e Telegram nello stesso contratto di messaggi del runtime. Identità, livello di autonomia, stato del dialogo e destinazione della risposta restano associati all'utente e al canale autenticati: un messaggio non eredita il contesto di un'altra persona.

Chiedi a Metnos con una richiesta come quella di questo esempio: «Invia a Lucia su Telegram il riepilogo dell'incontro di oggi». Metnos risolve il nome attraverso i canali associati all'utente: non occorre conoscere o inserire il chat_id di Telegram. L'invio resta soggetto ai controlli previsti per chi lo richiede e per il destinatario.

Indice

  1. Cosa fa: l'adattatore canale
  2. Il Protocol e i tipi
  3. Il canale Telegram
  4. Long-poll e persistenza dell'offset
  5. Daemon: il ciclo poll → run_turn → send
  6. Pulsanti inline e azioni interattive
  7. Rapporto col pairing
  8. Distribuzione: systemd user unit
  9. Chat web e API HTTP
  10. Vincoli operativi

1. Cosa fa: l'adattatore canale

Un canale collega un'interfaccia conversazionale al runtime. La chat web invoca direttamente l'API HTTP; Telegram usa un adattatore con long polling. In entrambi i casi, il trasporto consegna testo, allegati e metadati autenticati, mentre il runtime gestisce pianificazione, esecuzione, dialoghi e controlli di sicurezza.

chat web / Telegram richiesta / long polling gateway del canale identità / messaggio agent runtime elabora il turno risposta stesso canale
Figura 1 — Web e Telegram convergono sul runtime dopo la risoluzione dell'identità; la risposta torna al canale che ha originato il turno.

Il contratto separa il trasporto dalla logica dell'agente. Il canale Telegram normalizza gli aggiornamenti in ingresso e le risposte in uscita; il server HTTP espone lo stesso runtime con un modello richiesta/risposta. In entrambi i casi l'identità viene risolta prima di elaborare il contenuto.

2. Il Protocol e i tipi

In runtime/channels/__init__.py il canale è un typing.Protocol con due metodi minimi e una proprietà name. Non servono una classe base o l'ereditarietà: qualunque oggetto che esponga la forma giusta è un canale.

@runtime_checkable
class Channel(Protocol):
 name: str
 def send(self, recipient: str, message: OutboundMessage) -> dict:...
 def poll(self) -> list[InboundMessage]:...

I due tipi messaggio sono dataclass(frozen=True):

TipoCampiNote
InboundMessage channel, sender_id, text, message_id, received_at, extra Normalizzato: sender_id è sempre una stringa, anche dove il trasporto usa un intero (il chat_id di Telegram). extra contiene i metadati specifici del canale, per esempio from e update_id.
OutboundMessage text, reply_to, buttons buttons è list[list[dict]] (righe di pulsanti) per i trasporti che supportano azioni interattive.

L'immutabilità (oggetto frozen) evita che il dispatcher modifichi un messaggio in arrivo: chi vuole arricchire crea un nuovo oggetto. È una scelta deliberata: un messaggio arrivato è un fatto storico, non un blocco di appunti.

3. Il canale Telegram

runtime/channels/telegram.py implementa il protocol via Bot API. Niente libreria esterna: solo urllib + json. Coerente col principio self-host (cap. 4 Architettura): il bot interroga in uscita api.telegram.org, niente porte aperte, niente IP pubblico.

Configurazione (in ordine di precedenza):

  1. Parametri di costruzione: TelegramChannel(token=, default_chat_id=, credentials_path=, state_path=).
  2. Variabili d'ambiente: TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID.
  3. Archivio cifrato delle credenziali, domini telegram_bot_token e telegram_chat_id_host.
  4. File precedente ~/.config/metnos/credentials.env (permessi 600), usato solo come ripiego compatibile.

Metodi:

4. Long-poll e persistenza dell'offset

getUpdates è chiamato in modalità long polling: il server Telegram tiene aperta la connessione fino a 25 secondi se non ci sono updates. Niente ciclo a vuoto, niente carico inutile, latenza minima quando un messaggio arriva.

L'offset, cioè l'update_id dell'ultimo aggiornamento confermato, viene mantenuto in memoria e scritto su disco in ~/.local/state/metnos/telegram_offset. La scrittura è atomica. Se il file è illeggibile o danneggiato, il canale riparte senza offset invece di interrompersi.

La persistenza è opzionale: state_path=False la disabilita, principalmente nei test. Il daemon conferma un aggiornamento tramite ack() soltanto dopo averlo gestito. Se la gestione o la scrittura dell'offset fallisce, quell'aggiornamento non viene confermato e può essere riconsegnato al ciclo successivo.

5. Daemon: il ciclo poll → run_turn → send

runtime/channels/daemon.py contiene ChannelDaemon: un processo che esegue in sequenza poll → per ogni messaggio: handle → run_turn → send. Le chiamate a run_turn sono sincrone. Lo stesso processo può avviare, in un thread separato, il listener HTTP destinato agli executor remoti; questo non rende concorrente l'elaborazione dei messaggi Telegram.

ChannelDaemon lavora sul Channel Protocol; le parti specifiche di Telegram, come download degli allegati e risposta ai callback, restano nell'adattatore o nei relativi dispatcher.

Disciplina di errore:

Opzioni del comando ./.venv/bin/python -m runtime.channels.daemon, eseguito dalla radice del repository:

6. Pulsanti inline e azioni interattive

OutboundMessage.buttons è una matrice list[list[dict]]. Ogni dict ha text e data; data finisce nel callback_data del pulsante Telegram.

Il valore di callback_data identifica semanticamente l'azione e non dipende dal testo visualizzato. Il daemon gestisce attualmente:

Le etichette dei pulsanti sono localizzate nella lingua dell'utente. Prima di applicare una decisione, il daemon verifica l'identità associata al canale e la proprietà dello stato pendente; possedere o inoltrare un valore di callback non concede alcuna autorizzazione. Le approvazioni e i dialoghi sono monouso e soggetti a scadenza. Per il contratto dell'interazione vedi approvazioni e controllo umano.

7. Rapporto col pairing

Il daemon non accetta messaggi da chiunque. Prima di elaborare un InboundMessage risolve il legame fra canale, identificativo del mittente e utente Metnos. Consulta il registro di pairing e, per le associazioni multiutente, il registro degli utenti (vedi pairing):

I comandi /pair <codice> e /start <token> sono intercettati prima del controllo di associazione, così possono essere usati da un mittente non ancora riconosciuto. Il primo consuma un codice firmato con un livello di autonomia; il secondo consuma il token monouso e a scadenza emesso dalla gestione utenti. Ogni messaggio di chi è associato aggiorna anche last_seen tramite pairing.touch_last_seen.

8. Distribuzione: systemd user unit

Il daemon viene distribuito come user unit ed è quindi eseguito con l'identità dell'utente che ha installato Metnos, non come root. La sorgente canonica è install/units/metnos-telegram-daemon.service.tmpl; l'installatore la rende in ~/.config/systemd/user/metnos-telegram-daemon.service soltanto quando Telegram è stato configurato.

Per mantenere i servizi utente attivi dopo il logout e fra i riavvii, un amministratore del sistema può abilitare una volta il linger per l'utente del servizio: sudo loginctl enable-linger "$USER". La gestione dell'intero gruppo di servizi è descritta in systemd/README.md.

9. Chat web e API HTTP

La chat web comunica con il server HTTP di Metnos sulla porta 8770. Sul versante agente le rotte principali sono POST /agent/turn (esegue run_turn, in SSE o JSON), GET /agent/devices/me, GET /agent/health e GET /.well-known/metnos.json. Per l'elenco completo vedi http_api.

Il rapporto fra channel e http_api è deliberato:

Multi-user: il comando /start <token> accettato dal daemon Telegram completa il pairing dei guest emesso dal pannello /admin/users dell'HTTP API. Il send_messages supporta to_user="lucia" e via_channel="auto": risolve via users.resolve_recipients, vaglio cross-user applicato per non-host.

10. Vincoli operativi