← Indice documentazione Microprogettazione › pairing

myclaw

pairing — come riconoscere chi parla
Microprogettazione v1.0 — 22 aprile 2026
Primo documento di fase 3.
Reifica il flusso DM pairing di Architettura intro §7.

Pubblico: chi implementa la registrazione identità sui canali multi-utente. Lettura: 15 min.

Indice

  1. Scopo: chi sei prima di cosa vuoi
  2. Il flusso canonico
  3. Generazione del codice
  4. Approvazione admin e assegnazione ruolo
  5. Stato "pending" e SLA di delivery
  6. Firma e revoca
  7. Storage e persistenza
  8. Contratto Python
  9. Alternative considerate
  10. Test di conformità
  11. Riferimenti

1. Scopo: chi sei prima di cosa vuoi

Alcuni canali (Telegram, Signal, Matrix) sono per definizione multi-utente: chiunque conosca l'handle può scrivere. Senza un meccanismo di riconoscimento, myclaw tratterebbe ugualmente Roberto e uno sconosciuto. Il pairing è il rito di "mi presento, aspetto il tuo permesso, poi siamo in affari".

Cosa copre

Cosa non copre

2. Il flusso canonico

Sequenza in 5 passi. Vedi anche Architettura intro §7 fig. 4 per il diagramma narrativo.

  1. Sconosciuto scrive: "ciao, posso parlare?" su Telegram.
  2. Canale normalizza: gateway riceve sender="telegram:@nuovo_tizio". Pairing table non lo conosce.
  3. Gateway genera codice: K7-DELTA-19 (formato umano, 3 sezioni). Stato pending. Risponde allo sconosciuto: "non ti conosco. Codice: K7-DELTA-19. Il tuo admin lo approverà o negherà."
  4. Admin notificato: canale admin (CLI di Roberto, o Telegram @roberto pairato) riceve: "richiesta pairing: K7-DELTA-19 · telegram:@nuovo_tizio · primo contatto 2026-04-22 14:00. Approvi come: [admin | famiglia | ospite | nega]?"
  5. Roberto decide: myclaw pairing approve telegram K7-DELTA-19 --as ospite. Pairing firmato, scritto in DB. Il sender ora può interagire, con autonomy mappata al ruolo (§4).

3. Generazione del codice

Formato: 3 segmenti di 2-6 caratteri alfanumerici separati da trattini, ispirato ai pairing code Bluetooth/Apple. Esempi: K7-DELTA-19, RB-AMBER-42.

def generate_pairing_code() -> str:
    # segment 1: 2 char (letter+digit)
    # segment 2: parola da lista curata 200 parole italiane/inglesi pronunciabili
    # segment 3: 2 digit
    import secrets
    s1 = secrets.choice(string.ascii_uppercase) + secrets.choice(string.digits)
    s2 = secrets.choice(WORD_LIST)
    s3 = f"{secrets.randbelow(100):02d}"
    return f"{s1}-{s2}-{s3}"

Spazio delle chiavi: 26·10 · 200 · 100 ≈ 5.2M. Entropia ~22 bit: non crittografica, ma sufficiente per contesto casalingo dove la collisione non è vettore di attacco serio (l'admin approva manualmente; il codice vive max 1h).

DECISIONE v1: codici human-readable, no hash lungo. La UX batte la sicurezza matematica in questo contesto: Roberto deve poter leggere al telefono "K7-DELTA-19" e approvarlo da CLI senza copiaincollare stringhe esadecimali.

4. Approvazione admin e assegnazione ruolo

I ruoli

RuoloAutonomy defaultChi è tipicamente
adminSupervised (promuovibile a Full via session)Roberto stesso su un nuovo device. Solo uno o due admin.
famigliaSupervised con batching più generosoMoglie, figli maggiorenni. Stesse capability operative, memorie separate.
ospiteReadOnlyAmico che chiede per curiosità, utente occasionale.
revokednessuna — ogni richiesta 403Stato post-revoca. Record conservato per audit.

Comando admin

myclaw pairing approve <channel> <code> --as <role>
myclaw pairing deny    <channel> <code> [--never]
myclaw pairing list
myclaw pairing revoke  <channel> <sender_id> [--reason]

Il comando è disponibile solo da canale cli:roberto o dal sender admin stesso. Non è esposto come comando Telegram globale (troppo facile impersonation).

5. Stato "pending" e SLA di delivery

Una richiesta di pairing attende l'admin. Se l'admin dorme, lo sconosciuto non può stare in limbo all'infinito.

TimingComportamento
T + 0 minSconosciuto riceve "codice generato, admin notificato".
T + 30 minSe admin non ha risposto: sconosciuto riceve update "admin non risponde, riproverò tra 30 min, poi la richiesta scade".
T + 60 minAdmin non risponde: pairing request scade. Sconosciuto riceve "richiesta scaduta, puoi riprovare più tardi". Stato passa da pending a expired.
T + 7 giorniSe lo stesso handle riprova più di 5 volte in 7 giorni senza mai essere approvato: auto-ban per 30 giorni (anti-spam).
DECISIONE v1: timeout 1h, notifica intermedia a 30min. Coerente con pattern di autenticazione "out-of-band" (es. codici SMS bancari), non troppo stretto per lasciare tempo all'admin.

6. Firma e revoca

Firma HMAC

Al momento dell'approvazione, il record di pairing è firmato con una chiave interna di myclaw (in config/secrets.env come MYCLAW_HMAC_KEY). La firma copre: channel + sender_id + role + approved_at.

signature = hmac_sha256(
    key=MYCLAW_HMAC_KEY,
    msg=f"{channel}|{sender_id}|{role}|{approved_at_iso}"
)

A ogni messaggio ingerito, il gateway verifica la firma del pairing record. Se il record è stato manomesso (improbabile ma possibile se qualcuno edita il DB a mano), la firma non torna e il sender rientra in unknown.

Revoca

7. Storage e persistenza

-- workspace/state/pairings.db

CREATE TABLE pairings (
    pairing_id     TEXT PRIMARY KEY,
    channel        TEXT NOT NULL,
    sender_id      TEXT NOT NULL,
    display_name   TEXT,                    -- "@nuovo_tizio" a scopo umano
    role           TEXT NOT NULL,           -- admin | famiglia | ospite | revoked
    approved_by    TEXT NOT NULL,
    approved_at    DATETIME NOT NULL,
    expires_at     DATETIME,                -- NULL = no expiry
    signature      BLOB NOT NULL,
    UNIQUE(channel, sender_id)
);

CREATE TABLE pairing_requests (
    code           TEXT PRIMARY KEY,
    channel        TEXT NOT NULL,
    sender_id      TEXT NOT NULL,
    requested_at   DATETIME NOT NULL,
    expires_at     DATETIME NOT NULL,
    status         TEXT NOT NULL,           -- pending | approved | denied | expired
    resolved_at    DATETIME,
    resolved_by    TEXT
);

CREATE INDEX idx_pairings_sender ON pairings(channel, sender_id);
CREATE INDEX idx_requests_status ON pairing_requests(status, expires_at);

8. Contratto Python

from typing import Protocol, Literal
from dataclasses import dataclass

@dataclass
class Pairing:
    pairing_id: str
    channel: str
    sender_id: str
    display_name: str | None
    role: Literal["admin", "famiglia", "ospite", "revoked"]
    approved_by: str
    approved_at: datetime
    expires_at: datetime | None
    signature: bytes

@dataclass
class PairingRequest:
    code: str
    channel: str
    sender_id: str
    requested_at: datetime
    expires_at: datetime
    status: Literal["pending", "approved", "denied", "expired"]

class PairingService(Protocol):
    async def resolve_sender(
        self, channel: str, sender_id: str,
    ) -> Pairing | None:
        """Dato un sender, ritorna il pairing attivo o None."""
        ...

    async def initiate(
        self, channel: str, sender_id: str, display_name: str,
    ) -> PairingRequest:
        """Sender sconosciuto: genera codice, notifica admin."""
        ...

    async def approve(
        self, code: str, role: str, approved_by: str,
    ) -> Pairing: ...

    async def deny(self, code: str, reason: str) -> None: ...

    async def revoke(
        self, channel: str, sender_id: str, reason: str,
    ) -> None: ...

    async def autonomy_for_role(self, role: str) -> str:
        """Ritorna autonomy level default per ruolo."""
        ...

    async def expire_stale(self) -> int:
        """Job periodico: marca come expired le richieste scadute."""
        ...

9. Alternative considerate

AlternativaPerché scartata
Codice 6-cifre numerico stile SMSMeno resistente a typo quando Roberto legge al volo. Le parole aiutano.
QR code via TelegramNon funziona se l'admin approva da CLI senza fotocamera. Formato testuale è universale.
Self-service pairing con secret condivisoLo sconosciuto dovrebbe avere già accesso a un secret. Perde il senso: chi ha il secret è Roberto.
Pairing automatico per handle in whitelist pre-caricataUtile in futuro per "famiglia", non day-1. Overhead: una whitelist è un'altra cosa da mantenere.
Niente ruoli, tutti SupervisedOspiti non devono avere accesso write. I 4 ruoli sono il minimo funzionante.

10. Test di conformità

InvarianteTest
Sender unknown → pairing requestPrimo messaggio da telegram:@x mai visto → PairingRequest creata, status=pending, codice generato, notifica admin.
Nessuna azione finché non approvatoSender in pending invia "elimina file X" → tool dispatch bloccato, outcome policy_denied con reason="sender not paired".
Codice valido attiva pairingapprove telegram K7-DELTA-19 --as ospite → Pairing inserito, sender ora risolvibile con role=ospite.
Codice inesistente rifiutatoapprove telegram XX-BOGUS-99 → errore, nessun pairing.
Scadenza 1hRequest creata a T, non risolta, verifica a T+61min → status=expired, sender notificato.
Firma verificataEdit manuale della tabella pairings (cambia role) → hmac mismatch → sender torna unknown, warning critical nel log.
Revoca istantanearevoke telegram:@x → messaggio successivo da @x → rifiutato.
Ospite scade dopo 7ggPairing role=ospite creato 7gg+1h fa → resolve_sender ritorna None (scaduto).
Auto-ban anti-spam5 richieste da stesso sender in 7gg senza mai approvazione → 6a richiesta rifiutata automaticamente per 30gg.
Audit log di ogni transizioneOgni approve/deny/revoke/expire → riga in .audit/pairings.jsonl.

11. Riferimenti

RiferimentoCosa abbiamo preso
zeroclaw DM pairingIl pattern generale: codice out-of-band, approvazione esplicita.
Bluetooth pairing UXCodice human-readable a segmenti.
Architettura intro §7 (figura 4)Diagramma sequenza narrativo.
Policy §2 (autonomy level)Il ruolo pairing mappa a autonomy.
Observability §3Audit log dedicato pairings.jsonl.

Continua a leggere

prossimo
constitution
Le 4+1 Leggi, il rito di modifica, il framing linguistico.
microprogettazione · 22 min
policy
Dove il sender pairato viene autorizzato o negato.
indice
Landing
12/15 fatti.

myclaw — pairing v1.0 — 2026-04-22
Primo doc di fase 3. Prossimo: constitution.html.