Sari la conținut

Multiplayer — identitate, apartenență și prezență

Control Center funcționează pentru un singur operator, dar modelul de sub el este multi-actor de la bun început. Oamenii și agenții trimit mesaje în aceleași spații, editează aceleași tichete și apar în aceeași listă de participanți. Ca să fie sigur și ușor de înțeles, totul se reduce la trei decizii: cine contează ca actor, ce controlează accesul și care stare este partajată durabil, spre deosebire de cea doar difuzată în direct.

Principalii: utilizatorii și agenții sunt actori cu statut egal

Secțiune intitulată „Principalii: utilizatorii și agenții sunt actori cu statut egal”

Tot ce consemnează „cine a făcut asta” — mesaje, tichete, revizuiri, planuri, jurnale de rulare, înregistrări de audit — se rezolvă printr-un Principal, o uniune sigilată din UserPrincipal și AgentPrincipal, cu o formă compactă de transmisie (user:<id> / agent:<id>). Atribuirea nu colapsează niciodată N oameni (sau N agenți) într-un singur cont santinelă, așa că o revizuire lăsată de un agent și una lăsată de o persoană se disting oriunde apar.

Deoarece principalul este unitatea de identitate, el este și unitatea de izolare:

  • Limitele de rată sunt per principal, nu per sesiune. Protecția împotriva inundării pentru apelurile de instrumente la distanță bugetează pe utilizator, nu pe conexiune: toate dispozitivele unui membru trag dintr-o singură fereastră glisantă partajată (120 de apeluri pe minut, dintre care cel mult 30 pot modifica starea), așa că deschiderea a trei dispozitive nu triplează cota nimănui, iar un membru zgomotos nu îi poate înfometa pe ceilalți.
  • Anularea este per principal. Jurnalul de acțiuni al fiecărui client înregistrează doar mutațiile efectuate de propriul său principal. Poți aplica ⌘Z propriei modificări de tichet; nu poți anula modificarea unui coleg — până când o vezi, calea durabilă a rezolvat-o deja (vezi mai jos).

Apartenența este granița de acces — nu cheia de asociere

Secțiune intitulată „Apartenența este granița de acces — nu cheia de asociere”

Două credențiale separate răspund la două întrebări separate. Cheia de asociere (vezi Control remote și mobil) autentifică un dispozitiv la server. Apartenența autorizează utilizatorul acelui dispozitiv să acționeze în interiorul unui spațiu de lucru. Un dispozitiv asociat al cărui utilizator nu deține nicio apartenență într-un spațiu de lucru nu îl poate nici citi, nici modifica.

Apartenența este o linie pentru fiecare pereche (spațiu de lucru, utilizator), stocată în fișierul de bază de date propriu acelui spațiu de lucru — așa că „cine este în acest spațiu de lucru?” este o citire obișnuită cu domeniu limitat, în timp ce întrebarea inversă, „în ce spații de lucru sunt?”, se întinde inevitabil peste mai multe fișiere și este una dintre puținele difuzări aprobate între spații de lucru. Este ce rulează selectorul de spații de lucru înainte ca vreun spațiu de lucru să fi fost ales.

Fiecare linie poartă un rol dintr-o scară de cinci niveluri, ordonate după rang:

Rol Poate
owner Control complet, inclusiv ștergerea spațiului de lucru; exact unul per spațiu de lucru
admin Gestionează membri, invitații, setări, operațiuni distructive
member Citire-scriere de zi cu zi: mesaje, tichete, rulări de agenți
viewer Doar citire în tot spațiul de lucru; șabloanele glob de excludere a secretelor ascund căile corespunzătoare pe suprafețele cu cod
guest Același prag minim de doar citire ca viewer, la cel mai de jos rang al scării

Fiecare decizie de acces se reduce la „rolul apelantului este cel puțin X?”. Pragul minim este derivat din tipul operațiunii, dacă aceasta nu declară unul în mod explicit: o citire necesită guest, o mutație necesită member, o operațiune distructivă necesită admin. ACL-urile per obiect, dincolo de permisiunile per repo descrise mai jos, sunt în mod deliberat în afara domeniului.

Invitațiile sunt modalitatea prin care se acordă apartenența, din Setări → Spațiu de lucru → Membri. Un admin emite un cod de unică folosință (sub formă de link sau cod QR) care expiră implicit după șapte zile; serverul stochează doar hash-ul SHA-256 al codului, iar codul în sine este afișat o singură dată. Utilizarea lui provisionează utilizatorul pe loc (JIT), înregistrează apartenența la rolul invitației (niciodată owner — proprietatea se transferă explicit, niciodată prin invitație) și predă ștafeta către asocierea dispozitivului. Credențialele de dispozitiv emise după asociere sunt ele însele limitate la 30 de zile, așa că o asociere nu este o cheie permanentă.

Permisiunile per repo se află sub apartenență. Deoarece serverul deține copii de lucru complete, apartenența la spațiul de lucru nu trebuie niciodată să acorde, singură, mai multe privilegii decât forge-ul: fiecare suprafață cu cod — fișiere, diff-uri, revizuirea PR, căutarea de cod — verifică permisiunea membrului pe acel repo. Permisiunile sunt per (spațiu de lucru, utilizator, repo) și ordonate none → read → review → write, unde write înseamnă a dirija rulări de agenți care scriu în repo. Absența unei linii înseamnă none; proprietarii și administratorii dețin implicit write pe fiecare repo conectat și nu au nicio linie.

Control Center împarte starea partajată în două căi cu reguli de durabilitate opuse, pentru că cele două tipuri de stare eșuează în moduri opuse.

Prezența — cine este online, unde se află, ce tastează, ce face un agent chiar acum — nu valorează nimic în momentul în care devine învechită. Dacă ar fi persistată, fiecare reconectare ar trebui să reconcilieze „baza de date spune că Sam editează acest tichet” cu realitatea. Așa că prezența nu este niciodată scrisă în baza de date. Ea trăiește într-un hub de prezență din memoria serverului: clienții publică cu presence.update, partenerii se abonează cu presence.watch, iar intrările expiră automat după trei bătăi de inimă ratate (bătăile de inimă au loc la fiecare 10 secunde; hub-ul face curățenie la fiecare 5). Nu este nimic de reconciliat — doar de lăsat să expire.

Două proprietăți mențin calea demnă de încredere:

  • Identitatea vine din sesiune, niciodată din argumentele clientului. Serverul asociază dispozitivul autentificat al conexiunii cu un id de utilizator; un client nu poate actualiza niciodată decât propria prezență, așa că nimeni nu poate falsifica starea sau locus-ul altui participant. O actualizare explicită offline (deconectare sau comutatorul de invizibilitate) elimină intrarea imediat.
  • Lista de participanți trece prin hub-ul serverului. Partenerii nu fac schimb de prezență direct, așa că fiecare participant vede aceeași listă indiferent de topologie. Agenții nu au client, așa că serverul le sintetizează intrările din starea rulărilor în direct — se gândește, rulează, blocat la o poartă de aprobare, gata — o singură rulare reprezentativă hotărând starea și locus-ul unui agent, pentru ca rulările concurente să nu facă lista să oscileze.

Difuzarea este stratificată pe consumator: desktopul și webul merg pe un nivel complet (cel mult o emisiune a listei la fiecare 100 ms), telefonul pe un nivel de sinteză (la fiecare 500 ms), astfel încât actualizările la cadența cursorului să nu topească PWA-ul.

Locus-ul unui participant — unde se află în acest moment, până la nivel de artefact — este o uniune sigilată: Space, File (repo + cale + linie opțională), Pr, Ticket, PlanNode. Nu orice locus are undeva unde să sari: nu există o rută de vizualizare a fișierelor, așa că un locus File se afișează ca etichetă, nu ca destinație. Calea transportă și revendicări neangajante („Sam editează acest tichet”, „un agent deține acest worktree”): vizibilitate a conflictelor, nu blocaje. Tipurile de locus necunoscute se interpretează ca null, așa că un client vechi pur și simplu nu afișează ce nu înțelege.

Înregistrările care trebuie să supraviețuiască — tichete, documentul Notes partajat al spațiului, mesaje — urmează calea opusă: autoritativă, ordonată și persistată. Modificările concurente se rezolvă prin ultimul scriitor câștigă, în ordinea de primire la server, niciodată după ceasurile clienților. Rezolvarea este în mod deliberat cu granulație fină: modificările de tichet sunt pe coloană, așa că doi oameni care editează câmpuri diferite ale aceluiași tichet ajung amândoi, fără ca vreunul să îl calce pe celălalt (tranzițiile de flux de lucru păstrează în schimb blocarea optimistă); documentul Notes al spațiului este o singură coloană, așa că întregul document urmează LWW, iar revendicările neangajante de pe calea de prezență fac editarea concurentă vizibilă în loc să o blocheze. Nu este în mod deliberat un CRDT — sarcina probei revine CRDT-ului, iar LWW după ordinea de primire plus revendicările vizibile acoperă tiparele reale de colaborare.

„Ultimul scriitor” înseamnă ordinea de primire la server, niciodată un ceas de client — un laptop cu ceasul decalat nu poate câștiga o modificare pe care a pierdut-o. În spatele căii durabile se află fluxul de sincronizare: declanșatoarele SQLite adaugă fiecare mutație într-un jurnal de modificări propriu spațiului de lucru în interiorul tranzacției de scriere, alocând un număr de secvență monoton, propriu spațiului de lucru, în aceeași tranzacție cu mutația însăși. Un serviciu de pe server urmărește acel jurnal și îl transformă în cadre delta versionate, în ordinea secvenței. Clienții evoluează de la instantanee complete la delta; o lacună în secvență face ca acel depozit să revină în modul instantaneu, în loc să aplice greșit cadrele.

Urmărire, direcționare, preluare a controlului, predare înapoi

Secțiune intitulată „Urmărire, direcționare, preluare a controlului, predare înapoi”

Calea de prezență nu este doar o listă de participanți — ea conduce verbele de colaborare:

  • Modul Urmărire. Dă clic pe un participant și clientul tău călărește locus-ul acestuia, navigând pe măsură ce navighează. Urmărirea este „până când acționezi”: în momentul în care navighezi din proprie inițiativă, urmărirea se detașează. Un banner care poate fi închis anunță când un alt participant evidențiază (prezintă) un spațiu în care te afli.
  • Direcționare. Un mesaj adresat unui agent în rulare este pus în coadă ca direcționare; agentul îl recitește înainte de a-și relua următorul tur, așa că poți corecta o rulare în plin zbor fără să o omori.
  • Preluare a controlului. Preluarea controlului asupra unei conversații pune în pauză fiecare rulare activă la următoarea limită curată de tur (o rulare Claude Code, care nu are o limită sigură, este oprită), în spatele unui instantaneu git pentru fiecare tur, astfel încât preluarea este reversibilă. Un marcaj durabil blochează trimiterile noi în spațiu și supraviețuiește unei reporniri a serverului — serverul nu reia niciodată automat peste modificarea neterminată a unui om. Un singur editor activ per worktree este o revendicare neangajantă: o a doua preluare este vizibilă și refuzată, nu fuzionată în tăcere.
  • Predare înapoi. Returnarea controlului publică în spațiu un rezumat structurat al diff-ului worktree-ului, îl pune în coadă ca direcționare pentru rulările în pauză, le reia și șterge marcajul.

Direcționarea și preluarea controlului sunt intervenții ulterioare. Cadranul de autonomie este setarea permanentă: per spațiu, per agent, stabilită din antetul spațiului, el decide ce se întâmplă când un agent ajunge la o acțiune supusă unei porți.

Nivel Valoare transmisă Efect asupra unui apel de instrument supus porții
Doar propune proposeOnly Refuzat direct, cu un motiv pe care agentul este instruit să îl transmită — poate descrie acțiunea, dar nu o poate executa
Acționează cu aprobare actWithApproval Poarta de aprobare cu refuz implicit: o confirmare ajunge la un operator conectat, iar lipsa unui aprobator înseamnă refuz
Acționează liber actFreely Preaprobat, cu excepția cazului în care se aplică o regulă dură deny de guardrail — o regulă de refuz câștigă întotdeauna
(nesetat) null La fel ca acționează-cu-aprobare: valoarea implicită a serverului

Două lucruri rămân în afara cadranului, în ambele direcții. Verbul de ieșire propriu unui mod este fixat și nu poate fi niciodată refuzat, orice ar spune cadranul — modul orchestrate a fost cândva structural incapabil să emită singurul său livrabil, pentru că o presetare refuza clasa de efecte pe care o declară acel verb. Iar un deny dur venit de la guardrail-urile de acțiune nu poate fi anulat de actFreely.

Toate acestea sunt aditive. Fluxul de lucru al unui operator solo rămâne neschimbat: cu un singur om și niciun agent, hub-ul de prezență nu are ce să difuzeze, așa că calea emite instantaneul său inițial și apoi stă degeaba, cu trafic zero; mecanismul LWW și de sincronizare este aceeași cale cu un singur scriitor de totdeauna; iar cheia de asociere rămâne credențialul de dispozitiv care a fost dintotdeauna. Apartenența este deja granița de acces — cu un singur om există o singură linie de proprietar și niciun element de interfață pentru listă.