Ga naar inhoud

Multiplayer — identiteit, lidmaatschap en aanwezigheid

Control Center werkt voor één enkele operator, maar het model eronder is vanaf het begin multi-actor. Mensen en agenten berichten in dezelfde ruimtes, bewerken dezelfde tickets en staan op dezelfde deelnemerslijst. Dat veilig en begrijpelijk maken komt neer op drie beslissingen: wie als actor telt, wat de toegang begrenst en welke toestand duurzaam wordt gedeeld en welke alleen live wordt uitgezonden.

Principalen: gebruikers en agenten zijn gelijkwaardige actoren

Sectie getiteld “Principalen: gebruikers en agenten zijn gelijkwaardige actoren”

Alles wat vastlegt “wie dit heeft gedaan” — berichten, tickets, reviews, plannen, runlogboeken, auditregels — loopt via een Principal, een verzegelde union van UserPrincipal en AgentPrincipal met een compacte draadvorm (user:<id> / agent:<id>). Toeschrijving vouwt nooit N mensen (of N agenten) samen in één sentinelaccount, dus een review die een agent achterliet en een review die een persoon achterliet zijn overal waar ze opduiken te onderscheiden.

Omdat de principal de eenheid van identiteit is, is het ook de eenheid van inkapseling:

  • Snelheidslimieten gelden per principal, niet per sessie. De overbelastingsbeveiliging op externe toolaanroepen budgetteert per gebruiker, niet per verbinding: alle apparaten van een lid putten uit één gedeeld schuivend venster (120 aanroepen per minuut, waarvan er hoogstens 30 mogen muteren), dus drie apparaten openzetten verdrievoudigt niemands tegoed en één luidruchtig lid kan de anderen niet uithongeren.
  • Ongedaan maken geldt per principal. Het actiejournaal van elke client registreert alleen de mutaties die zijn principal heeft uitgevoerd. Je kunt je eigen ticketbewerking met ⌘Z terugdraaien; die van een teamgenoot niet — tegen de tijd dat je hem ziet, heeft het duurzame spoor hem al opgelost (zie hieronder).

Lidmaatschap is de toegangsgrens — niet de koppelingssleutel

Sectie getiteld “Lidmaatschap is de toegangsgrens — niet de koppelingssleutel”

Twee afzonderlijke inloggegevens beantwoorden twee afzonderlijke vragen. De koppelingssleutel (zie Afstandsbediening en mobiel) authenticeert een apparaat bij de server. Lidmaatschap machtigt de gebruiker van dat apparaat om binnen een werkruimte te handelen. Een gekoppeld apparaat waarvan de gebruiker geen lidmaatschap in een werkruimte heeft, kan die werkruimte niet lezen en er niets in wijzigen.

Lidmaatschap is één rij per (werkruimte, gebruiker), opgeslagen in het eigen databasebestand van die werkruimte — dus “wie zit er in deze werkruimte?” is een gewone leesopdracht binnen één scope, terwijl de omgekeerde vraag, “in welke werkruimtes zit ik?”, noodzakelijk bestanden overspant en een van de weinige toegestane fan-outs over werkruimtes heen is. Het is wat de werkruimtekiezer uitvoert voordat er een werkruimte is gekozen.

Elke rij draagt een rol uit een vijfledige, op rang geordende ladder:

Rol Mag
owner Volledige controle, inclusief het verwijderen van de werkruimte; precies één per werkruimte
admin Leden, uitnodigingen, instellingen en destructieve bewerkingen beheren
member Dagelijks lezen en schrijven: berichten, tickets, agentruns
viewer Alleen-lezen in de hele werkruimte; globs voor het uitsluiten van secrets verbergen overeenkomende paden op oppervlakken met code
guest Dezelfde alleen-lezen-ondergrens als viewer, op de laagste rang van de ladder

Elke toegangsbeslissing komt neer op “is de rol van de aanvrager minstens X”. De ondergrens wordt afgeleid uit de soort bewerking, tenzij die er expliciet een aangeeft: een lezing vereist guest, een mutatie vereist member, een destructieve bewerking vereist admin. ACL’s per object, naast de repo-toekenningen hieronder, vallen bewust buiten scope.

Uitnodigingen zijn hoe lidmaatschap wordt verleend, vanuit Instellingen → Werkruimte → Leden. Een admin maakt een eenmalige code aan (als link of QR) die standaard na zeven dagen verloopt; de server bewaart alleen de SHA-256-hash van de code en de code zelf wordt precies één keer getoond. Het inwisselen voorziet de gebruiker JIT van een account, legt het lidmaatschap vast in de rol van de uitnodiging (nooit owner — eigenaarschap wordt expliciet overgedragen, nooit per uitnodiging verleend) en draagt over aan de apparaatkoppeling. Apparaatreferenties die na het koppelen worden uitgegeven, zijn zelf beperkt tot 30 dagen, dus een koppeling is geen permanente sleutel.

Repo-toekenningen liggen onder het lidmaatschap. Omdat de server volledige checkouts houdt, mag lidmaatschap van de werkruimte alleen nooit meer rechten geven dan de forge: elk oppervlak met code — bestanden, diffs, PR-review, codezoekopdrachten — controleert de toekenning van het lid op die repo. Toekenningen gelden per (werkruimte, gebruiker, repo) en zijn gerangschikt none → read → review → write, waarbij write betekent: agentruns aansturen die naar de repo schrijven. Het ontbreken van een rij betekent none; owners en admins hebben impliciet write op elke gekoppelde repo en dragen helemaal geen rijen.

Control Center splitst gedeelde toestand in twee sporen met tegengestelde duurzaamheidsregels, omdat de twee soorten toestand op tegengestelde manieren falen.

Aanwezigheid — wie online is, waar ze zijn, wat ze typen, wat een agent op dit moment doet — is waardeloos zodra ze verouderd is. Als ze werd gepersisteerd, zou elke herverbinding de bewering “de database zegt dat Sam dit ticket bewerkt” tegen de werkelijkheid moeten afwegen. Daarom wordt aanwezigheid nooit naar de database geschreven. Ze leeft in een in-memory aanwezigheidshub op de server: clients publiceren met presence.update, peers abonneren zich met presence.watch en vermeldingen verlopen automatisch na drie gemiste heartbeats (heartbeats lopen elke 10 seconden; de hub ruimt elke 5 op). Er valt niets af te stemmen — alleen iets te laten verlopen.

Twee eigenschappen houden het spoor betrouwbaar:

  • Identiteit komt uit de sessie, nooit uit clientargumenten. De server koppelt het geauthenticeerde apparaat van de verbinding aan een gebruikers-id; een client kan alleen zijn eigen aanwezigheid bijwerken, dus niemand kan de status of locus van een andere deelnemer vervalsen. Een expliciete offline-update (verbinding verbreken of de onzichtbaarheidsschakelaar) verwijdert de vermelding onmiddellijk.
  • De deelnemerslijst loopt via de server. Peers wisselen aanwezigheid nooit rechtstreeks uit, dus elke deelnemer ziet dezelfde lijst, ongeacht de topologie. Agenten hebben geen client, dus de server synthetiseert hun vermeldingen uit de live runstatus — aan het denken, aan het draaien, geblokkeerd op een goedkeuringspoort, klaar — waarbij één representatieve run de status en locus van een agent bepaalt, zodat gelijktijdige runs de lijst niet laten flapperen.

De fan-out is gelaagd per consument: desktop en web rijden op een volledige laag (hoogstens één lijstuitzending per 100 ms), de telefoon op een samenvattingslaag (per 500 ms), zodat updates op cursustempo de PWA niet laten smelten.

De locus van een deelnemer — waar die zich op dit moment bevindt, tot op het artefact — is een verzegelde union: Space, File (repo + pad + optionele regel), Pr, Ticket, PlanNode. Niet elke locus heeft een plek om naartoe te springen: er is geen bestandsviewerroute, dus een File-locus wordt weergegeven als een label in plaats van als bestemming. Het spoor draagt ook zachte claims (“Sam bewerkt dit ticket”, “een agent houdt deze worktree vast”): zichtbaarheid van conflicten, geen locks. Onbekende locussoorten parsen naar null, dus een oude client geeft eenvoudigweg niet weer wat hij niet begrijpt.

Records die moeten overleven — tickets, het gedeelde Notes-document van de ruimte, berichten — nemen het tegenovergestelde pad: gezaghebbend, geordend en gepersisteerd. Gelijktijdige bewerkingen worden opgelost met last-writer-wins in de ontvangstvolgorde van de server, nooit met clientklokken. De oplossing is bewust fijnmazig: ticketbewerkingen zijn per kolom, dus twee mensen die verschillende velden van hetzelfde ticket bewerken, landen allebei en geen van beiden overschrijft de ander (workflowovergangen behouden juist optimistische vergrendeling); het Notes-document van de ruimte is één kolom, dus het hele document is LWW, waarbij zachte claims op het aanwezigheidsspoor gelijktijdig bewerken zichtbaar maken in plaats van vergrendeld. Het is bewust geen CRDT — de bewijslast ligt bij de CRDT, en LWW op ontvangstvolgorde plus zichtbare claims dekt de werkelijke samenwerkingspatronen.

“Last writer” betekent ontvangstvolgorde op de server, nooit een clientklok — een laptop met een scheve klok kan een bewerking die hij verloor niet winnen. Achter het duurzame spoor zit de syncfeed: SQLite-triggers voegen elke mutatie toe aan een wijzigingenlogboek per werkruimte binnen de schrijvende transactie, en kennen een monotoon volgnummer per werkruimte toe in dezelfde transactie als de mutatie zelf. Een serverdienst volgt dat log in versiegebonden deltaframes in volgorde. Clients evolueren van volledige snapshots naar delta’s; een gat in de reeks laat die opslag terugvallen op snapshotmodus in plaats van frames verkeerd toe te passen.

Het aanwezigheidsspoor is niet alleen een deelnemerslijst — het stuurt de samenwerkingswerkwoorden aan:

  • Volgmodus. Klik op een deelnemer en je client rijdt mee op diens locus en navigeert terwijl die navigeert. Volgen geldt “totdat je zelf iets doet”: op het moment dat je op eigen kracht navigeert, wordt het volgen losgekoppeld. Een wegklikbare banner meldt wanneer een andere deelnemer een ruimte waarin jij zit in de spotlight zet (presenteert).
  • Bijsturen. Een bericht aan een draaiende agent wordt in de wachtrij gezet als sturing; de agent leest het opnieuw voordat hij aan zijn volgende beurt begint, zodat je een run midden in de vlucht kunt bijsturen zonder hem te doden.
  • Overnemen. De controle over een gesprek overnemen pauzeert elke actieve run op zijn volgende schone beurtgrens (een Claude Code-run, die geen veilige grens heeft, wordt gestopt) achter een git-snapshot per beurt, zodat het overnemen omkeerbaar is. Een duurzame markering blokkeert nieuwe dispatches naar de ruimte en overleeft een herstart van de server — de server hervat nooit automatisch in de half afgemaakte bewerking van een mens. Eén live bewerker per worktree is een zachte claim: een tweede overname is zichtbaar en wordt geweigerd, niet stil samengevoegd.
  • Teruggeven. De controle teruggeven plaatst een gestructureerde diffsamenvatting van de worktree in de ruimte, zet die als sturing in de wachtrij voor de gepauzeerde runs, hervat ze en wist de markering.

Bijsturen en overnemen zijn interventies achteraf. De autonomieregelaar is de vaste instelling: per ruimte, per agent, ingesteld vanuit de ruimteheader, bepaalt hij wat er gebeurt wanneer een agent een afgeschermde actie bereikt.

Niveau Draadwaarde Effect op een afgeschermde toolaanroep
Alleen voorstellen proposeOnly Botweg geweigerd, met een reden die de agent moet doorgeven — hij kan de actie beschrijven maar niet uitvoeren
Handelen met goedkeuring actWithApproval De fail-closed goedkeuringspoort: één bevestiging verschijnt bij een verbonden operator en zonder goedkeurder betekent het weigering
Vrij handelen actFreely Vooraf goedgekeurd, behalve waar een harde deny-regel uit de beveiligingen van toepassing is — een deny-regel wint altijd
(niet ingesteld) null Hetzelfde als handelen met goedkeuring: de standaardwaarde van de server

Twee dingen vallen in beide richtingen buiten de regelaar. Het eigen uitvoerwerkwoord van een modus is vastgezet en kan nooit worden geweigerd, wat de regelaar ook zegt — de orchestrate-modus kon ooit structureel zijn enige deliverable niet uitstoten omdat een preset de effectklasse weigerde die dat werkwoord declareert. En een harde deny uit de actiebeveiligingen kan niet door actFreely worden overschreven.

Dit alles is additief. De workflow van een solo-operator verandert niet: met één mens en geen agenten heeft de aanwezigheidshub niets uit te zenden, dus het spoor stuurt zijn beginsnapshot en blijft daarna inactief met nul verkeer; de LWW- en sync-machinerie is hetzelfde enkelvoudige-schrijverpad als altijd; en de koppelingssleutel blijft de apparaatreferentie die hij altijd al was. Lidmaatschap is al de toegangsgrens — met één mens is er één owner-rij en geen deelnemerslijst-opsmuk.