Salta ai contenuti

Multigiocatore — identità, appartenenza e presenza

Control Center funziona per un singolo operatore, ma il modello che sta sotto è multi-attore fin dall’inizio. Esseri umani e agenti scrivono negli stessi spazi, modificano gli stessi ticket e compaiono nello stesso elenco. Rendere tutto questo sicuro e comprensibile si riduce a tre decisioni: chi conta come attore, cosa controlla l’accesso e quale stato è condiviso in modo durevole rispetto a quello semplicemente trasmesso in tempo reale.

I Principal: utenti e agenti sono attori alla pari

Sezione intitolata “I Principal: utenti e agenti sono attori alla pari”

Tutto ciò che registra «chi ha fatto questo» — messaggi, ticket, revisioni, piani, log di esecuzione, voci di controllo — si risolve attraverso un Principal, un’unione sealed di UserPrincipal e AgentPrincipal con una forma compatta di serializzazione (user:<id> / agent:<id>). L’attribuzione non collassa mai N persone (o N agenti) in un unico account sentinella, quindi una revisione lasciata da un agente e una lasciata da una persona restano distinguibili ovunque emergano.

Poiché il principale è l’unità di identità, è anche l’unità di contenimento:

  • I limiti di velocità sono per principale, non per sessione. La protezione anti-flood sulle chiamate remote agli strumenti calcola il budget per utente, non per connessione: tutti i dispositivi di un membro attingono da un’unica finestra scorrevole condivisa (120 chiamate al minuto, di cui al massimo 30 possono mutare), così aprire tre dispositivi non triplica la quota di nessuno e un membro rumoroso non può affamare gli altri.
  • L’annullamento è per principale. Il diario delle azioni di ogni client registra solo le mutazioni compiute dal suo principale. Puoi fare ⌘Z sulla tua modifica a un ticket; non puoi annullare quella di un collega: nel momento in cui la vedi, la corsia durevole l’ha già risolta (vedi sotto).

L’appartenenza è il confine di accesso, non la chiave di abbinamento

Sezione intitolata “L’appartenenza è il confine di accesso, non la chiave di abbinamento”

Due credenziali distinte rispondono a due domande distinte. La chiave di abbinamento (vedi Controllo remoto e mobile) autentica un dispositivo presso il server. L’appartenenza autorizza l’utente di quel dispositivo ad agire all’interno di uno spazio di lavoro. Un dispositivo abbinato il cui utente non ha alcuna appartenenza a uno spazio di lavoro non può né leggerlo né modificarlo.

L’appartenenza è una riga per coppia (spazio di lavoro, utente), memorizzata nel file di database dello spazio di lavoro stesso — quindi «chi c’è in questo spazio di lavoro?» è una normale lettura con ambito, mentre la domanda inversa, «in quali spazi di lavoro mi trovo?», attraversa necessariamente più file ed è uno dei pochi fan-out tra spazi di lavoro consentiti. È ciò che esegue il selettore dello spazio di lavoro prima che ne sia stato scelto uno.

Ogni riga porta un ruolo da una scala a cinque livelli ordinata per rango:

Ruolo Può
owner Controllo completo, inclusa l’eliminazione dello spazio di lavoro; esattamente uno per spazio di lavoro
admin Gestire membri, inviti, impostazioni, operazioni distruttive
member Lettura-scrittura quotidiana: messaggi, ticket, esecuzioni di agenti
viewer Sola lettura su tutto lo spazio di lavoro; i glob di esclusione dei segreti nascondono i percorsi corrispondenti sulle superfici che espongono codice
guest Stessa soglia di sola lettura dell’osservatore, al rango più basso della scala

Ogni decisione di accesso si riduce a «il ruolo del chiamante è almeno X». La soglia minima è derivata dal tipo di operazione, a meno che questa non ne dichiari una esplicitamente: una lettura richiede guest, una mutazione richiede member, un’operazione distruttiva richiede admin. Le ACL per oggetto oltre alle concessioni per repository descritte sotto sono deliberatamente fuori ambito.

Gli inviti sono il modo in cui si concede l’appartenenza, da Impostazioni → Spazio di lavoro → Membri. Un admin conia un codice monouso (come link o QR) che scade dopo sette giorni per impostazione predefinita; il server memorizza solo l’hash SHA-256 del codice e il codice stesso viene mostrato una sola volta. Il suo riscatto esegue il provisioning JIT dell’utente, registra l’appartenenza con il ruolo dell’invito (mai owner — la proprietà si trasferisce esplicitamente, non si concede mai tramite invito) e passa all’abbinamento del dispositivo. Le credenziali del dispositivo generate dopo l’abbinamento hanno a loro volta una scadenza di 30 giorni, quindi un abbinamento non è una chiave permanente.

Le concessioni per repository stanno sotto l’appartenenza. Poiché il server detiene checkout completi, la sola appartenenza a uno spazio di lavoro non deve mai attribuire più privilegi della forge: ogni superficie che espone codice — file, diff, revisione delle PR, ricerca nel codice — verifica la concessione del membro su quel repository. Le concessioni sono per (spazio di lavoro, utente, repository) e ordinate none → read → review → write, dove write significa guidare esecuzioni di agenti che scrivono nel repository. L’assenza di una riga significa none; proprietari e admin detengono implicitamente write su ogni repository collegato e non hanno alcuna riga.

Control Center divide lo stato condiviso in due corsie con regole di durabilità opposte, perché i due tipi di stato falliscono in modi opposti.

La presenza — chi è online, dove si trova, cosa sta digitando, cosa sta facendo un agente in questo momento — è inutile nel momento stesso in cui diventa obsoleta. Se fosse persistita, ogni riconnessione dovrebbe riconciliare «il database dice che Sam sta modificando questo ticket» con la realtà. Perciò la presenza non viene mai scritta nel database. Vive in un hub della presenza in memoria sul server: i client pubblicano con presence.update, i peer si iscrivono con presence.watch e le voci scadono automaticamente dopo tre heartbeat mancati (gli heartbeat vengono inviati ogni 10 secondi; l’hub esegue una scansione ogni 5). Non c’è nulla da riconciliare: solo da lasciar decadere.

Due proprietà mantengono affidabile la corsia:

  • L’identità proviene dalla sessione, mai dagli argomenti del client. Il server mappa il dispositivo autenticato della connessione su un id utente; un client può sempre e solo aggiornare la propria presenza, quindi nessuno può falsificare lo stato o la posizione di un altro partecipante. Un aggiornamento offline esplicito (disconnessione o l’interruttore dell’invisibilità) rimuove la voce immediatamente.
  • L’elenco è accentrato sul server. I peer non si scambiano mai la presenza direttamente, quindi ogni partecipante vede lo stesso elenco indipendentemente dalla topologia. Gli agenti non hanno un client, quindi il server sintetizza le loro voci dallo stato di esecuzione in tempo reale — sta pensando, in corso, bloccato su un gate di approvazione, completato — con un’unica esecuzione rappresentativa che determina stato e posizione di un agente, così le esecuzioni concorrenti non fanno sfarfallare l’elenco.

Il fan-out è suddiviso in livelli per consumatore: desktop e web viaggiano su un livello completo (al massimo un’emissione dell’elenco ogni 100 ms), il telefono su un livello riassuntivo (ogni 500 ms), così gli aggiornamenti a cadenza di cursore non possono fondere la PWA.

La posizione di un partecipante — dove si trova in questo momento, fino all’artefatto — è un’unione sealed: Space, File (repository + percorso + riga facoltativa), Pr, Ticket, PlanNode. Non ogni posizione ha una destinazione a cui saltare: non esiste una route di visualizzazione dei file, quindi una posizione File viene resa come etichetta anziché come destinazione. La corsia trasporta anche rivendicazioni non bloccanti («Sam sta modificando questo ticket», «un agente detiene questo worktree»): visibilità dei conflitti, non blocchi. I tipi di posizione sconosciuti vengono interpretati come null, quindi un client vecchio semplicemente non rende ciò che non capisce.

I record che devono sopravvivere — ticket, il documento Note condiviso dello spazio, messaggi — seguono il percorso opposto: autorevoli, ordinati e persistiti. Le modifiche concorrenti si risolvono con last-writer-wins nell’ordine di ricezione del server, mai in base agli orologi dei client. La risoluzione è deliberatamente a grana fine: le modifiche ai ticket sono per colonna, quindi due persone che modificano campi diversi dello stesso ticket hanno entrambe successo e nessuna sovrascrive l’altra (le transizioni del flusso di lavoro mantengono invece il locking ottimistico); il documento Note dello spazio è una sola colonna, quindi l’intero documento è LWW e le rivendicazioni non bloccanti sulla corsia della presenza rendono visibile la modifica concorrente invece di bloccarla. Non è deliberatamente un CRDT: l’onere della prova spetta al CRDT, e il LWW nell’ordine di ricezione più le rivendicazioni visibili copre i modelli di collaborazione reali.

«Ultimo scrittore» significa ordine di ricezione del server, mai l’orologio di un client: un portatile con un orologio sfasato non può vincere una modifica che ha perso. Dietro la corsia durevole c’è il feed di sincronizzazione: i trigger SQLite accodano ogni mutazione a un log delle modifiche per spazio di lavoro all’interno della transazione di scrittura, assegnando un numero di sequenza monotono per spazio di lavoro nella stessa transazione della mutazione stessa. Un servizio del server segue quel log e lo trasforma in frame delta versionati in ordine di sequenza. I client evolvono da snapshot completi a delta; una lacuna nella sequenza riporta quello store in modalità snapshot invece di applicare frame in modo errato.

La corsia della presenza non è solo un elenco: guida i verbi della collaborazione:

  • Modalità Segui. Fai clic su un partecipante e il tuo client si aggancia alla sua posizione, navigando mentre lui naviga. Segui vale «finché non agisci»: nel momento in cui navighi di tua iniziativa, l’inseguimento si stacca. Un banner chiudibile segnala quando un altro partecipante sta presentando uno spazio in cui ti trovi.
  • Guida. Un messaggio a un agente in esecuzione viene messo in coda come guida; l’agente lo rilegge prima di riprendere il turno successivo, così puoi indirizzare un’esecuzione in corsa senza interromperla.
  • Prendi il controllo. Assumere il controllo di una conversazione mette in pausa ogni esecuzione attiva al suo prossimo confine di turno pulito (un’esecuzione di Claude Code, che non ha un confine sicuro, viene arrestata) dietro uno snapshot git per turno, così la presa di controllo è reversibile. Un marcatore durevole blocca i nuovi invii nello spazio e sopravvive al riavvio del server: il server non riprende mai automaticamente dentro la modifica a metà di una persona. Un solo editor attivo per worktree è una rivendicazione non bloccante: un secondo tentativo di presa di controllo è visibile e rifiutato, non fuso in silenzio.
  • Restituisci il controllo. Restituire il controllo pubblica nello spazio un riepilogo strutturato del diff del worktree, lo accoda come guida per le esecuzioni in pausa, le riprende e cancella il marcatore.

La guida e la presa di controllo sono interventi successivi ai fatti. Il quadrante dell’autonomia è l’impostazione permanente: per spazio, per agente, impostata dall’intestazione dello spazio, decide cosa succede quando un agente raggiunge un’azione controllata da un gate.

Livello Valore di serializzazione Effetto su una chiamata a uno strumento controllata da gate
Solo proposta proposeOnly Rifiutata del tutto, con una motivazione che l’agente deve riferire: può descrivere l’azione ma non compierla
Agisci con approvazione actWithApproval Il gate di approvazione fail-closed: una conferma compare a un operatore connesso e l’assenza di un approvatore significa rifiuto
Agisci liberamente actFreely Pre-approvata, tranne dove si applica una regola deny rigida dei guardrail: una regola di rifiuto vince sempre
(non impostato) null Come agisci con approvazione: il valore predefinito del server

Due cose restano fuori dal quadrante in entrambe le direzioni. Il verbo di output di una modalità è fissato e non può mai essere negato, qualunque cosa dica il quadrante: la modalità orchestrate una volta era strutturalmente incapace di emettere il suo unico deliverable perché un preset negava la classe di effetto dichiarata da quel verbo. E un deny rigido dai guardrail delle azioni non è aggirabile con actFreely.

Tutto questo è additivo. Il flusso di lavoro di chi opera da solo è invariato: con un solo essere umano e nessun agente, l’hub della presenza non ha nulla da trasmettere, quindi la corsia emette il suo snapshot iniziale e poi resta inattiva a traffico zero; il meccanismo LWW e di sincronizzazione è lo stesso percorso a scrittore singolo di sempre; e la chiave di abbinamento resta la credenziale del dispositivo che è sempre stata. L’appartenenza è già il confine di accesso: con un solo essere umano c’è un’unica riga di proprietario e nessun elemento di interfaccia dell’elenco.