Ga naar inhoud

Werkruimtes en isolatie

Een werkruimte is de isolatietenant op het hoogste niveau in Control Center. Hij groepeert agenten, repositories, ruimtes, tickets, geheugen, pipelines en leden in één begrensde context. Alles wat binnen een werkruimte gebeurt, blijft erbinnen.

Een typische werkruimte komt overeen met één project of één codebase. Je kunt aparte werkruimtes hebben voor “api-service”, “frontend-v2” en “infrastructure”.

De rijen van een werkruimte staan in het eigen SQLite-bestand van die werkruimte, op <dataDir>/<workspaceId>/workspace.db. Elke werkruimte krijgt een map in plaats van een kaal bestand, omdat een werkruimte meer verzamelt dan een database — worktrees per gesprek, de agent- en skillbestanden, chatbotreferenties — en een map geven betekent dat dat allemaal samen leeft en samen wordt verwijderd.

Entiteit Scope
Agenten Horen bij precies één werkruimte
Repository’s Geregistreerd in één werkruimte (zie hieronder)
Ruimtes, gesprekken en berichten Werkruimtegebonden
Tickets, projecten, plannen, orkestraties Werkruimtegebonden
Geheugenfeiten, beleidsregels, domeinen, toegangsrechten Werkruimtegebonden
Pipeline-runs, sjablonen, triggers Werkruimtegebonden
Pull request-status, reviewruimtes, reviewcohorten Werkruimtegebonden
Agent-runlogs en werkgeheugen Werkruimtegebonden
Leden, uitnodigingen, rechten per repo Werkruimtegebonden
Symbolen en randen van de codegraph Afgebakend op repoId binnen het werkruimtebestand

De meeste van deze rijen hebben een workspaceId-kolom; aparte databasebestanden handhaven de isolatie. Zie de isolatie-invariant hieronder.

Repo’s zijn werkruimtegebonden; identiteit over werkruimtes loopt via het pad

Sectie getiteld “Repo’s zijn werkruimtegebonden; identiteit over werkruimtes loopt via het pad”

Een rij voor een repo staat in het eigen databasebestand van zijn werkruimte — het bestand is de scope, dus de tabel heeft geen workspaceId-kolom en geen koppeltabel.

Je voegt een repo aan een werkruimte toe via Instellingen → Werkruimte → Repository’s. Dezelfde checkout in twee werkruimtes registreren maakt twee onafhankelijke rijen met twee id’s; de identiteit van een repo over werkruimtes loopt via het bestandssysteempad (of GitHub-eigenaar/naam), nooit via het id.

Registratie is strikt: het pad moet binnen een git-worktree liggen, er moet een origin-remote zijn en die remote moet naar een ondersteunde forge wijzen (github.com, gitlab.com of bitbucket.org). De forge wordt uit de remote gelezen en op de repo opgeslagen, dus één werkruimte kan repo’s van alle drie bevatten. Al het andere wordt geweigerd. De mappenbrowser waarin je kiest toont het bestandssysteem van de serverhost, beperkt tot de allow-list --repo-roots van de server (die standaard de homedirectory van de servergebruiker is).

Een repository uit een werkruimte verwijderen wist de rij in één keer en laat de symbolen, randen, bestanden, indexcheckpoints en rechten per lid van de codegraph mee verwijderen. De checkout op schijf blijft onaangeroerd.

De codegraph staat in hetzelfde bestand, gesleuteld op repoId, omdat verschillende werkruimtes dezelfde repository op verschillende branches met verschillende code kunnen hebben.

Gegevenslekken tussen werkruimtes worden als bugs behandeld. Isolatie is structureel en wordt op meerdere niveaus gehandhaafd:

  • Databaselaag. Elke werkruimte heeft zijn eigen SQLite-bestand, uitgedeeld door WorkspaceDatabaseManager.of(workspaceId). Een werkruimtedatabase declareert users, workspaces of de tabellen van een andere werkruimte niet, dus een cross-workspace-leesopdracht is geen bug die je kunt schrijven — hij compileert niet. Repositories zoeken hun DAO per call op in plaats van er een te cachen, en omdat elke repositorymethode een verplichte workspaceId aanneemt, kan de werkruimte nooit worden afgeleid of standaard gekozen.
  • Het werkruimte-id is een padsegment. Omdat het id een mapnaam wordt, moet het overeenkomen met een veilig patroon van één segment (begint alfanumeriek, daarna alfanumerieke tekens, punten, underscores, koppeltekens; geen ..). Id’s zijn overal in het product UUID’s. Al het andere gooit een fout in plaats van te worden opgeschoond — dat is de pad-traversal-guard, en hij faalt luidruchtig in plaats van de gegevens van een werkruimte naar een pad te schrijven dat iemand anders heeft gekozen.
  • Verdediging in de diepte. De workspaceId-kolommen staan er nog steeds en worden nog steeds geschreven. Hoewel ze binnen één bestand overbodig zijn, houden ze de sync-feed-triggers, FTS-indexen en bestaande rijvormen ongewijzigd, en maken ze een bestand zelfbeschrijvend wanneer je het op zichzelf bekijkt — precies waarop export en import vertrouwen.
  • Domeinlaag. Services die entiteiten op id muteren, controleren entity.workspaceId == workspaceId op één chokepoint voordat ze doorgaan, en gooien WorkspaceMismatchException bij een mismatch — de luidruchtige fout voor een id dat uit de verkeerde context kwam. Luidruchtig weigeren is belangrijk: een stille no-op verbergt de bug en doorgaan lekt.
  • RPC-laag. De repo-RPC-dispatcher is het chokepoint. Elke operatie met werkruimtescope moet workspace_id in zijn eigen argumenten meedragen — er is geen “huidige werkruimte” per sessie om te lekken, en een onbekend id wordt al als niet-gevonden geweigerd voordat er iets een database opent, zodat een slecht id geen spook-workspace.db kan materialiseren. Echt globale operaties (de nieuwsfeed, de fleetwachtrij) verklaren zichzelf unscoped als een expliciete, getoetste beslissing. MCP-tools volgen dezelfde regel: elke tool die gegevens met werkruimtescope aanraakt, vereist workspace_id.
  • Cross-workspace-fan-out is opsombaar. Een vraag over meerdere werkruimtes beantwoorden betekent meerdere databases openen. Die fan-out is beperkt tot één helper, CrossWorkspaceQueries, zodat de volledige lijst van zaken die de grens legitiem overschrijden in de call sites van één bestand woont in plaats van te verdwijnen achter doc-comments. Die call sites zijn: serverbrede aggregatie, opstart-reconcilers, retentie- en GC-sweeps, back-up, event-routers — en de lidmaatschapzoekopdracht hieronder.
  • Alleen toegang op id is niet genoeg. Een entiteit opzoeken op zijn UUID bewijst niet dat hij bij de werkruimte van de aanroeper hoort. De bestandssplitsing maakt het verkeerde bestand onbereikbaar; de validatie in de domeinlaag vangt een verkeerd id af dat via een legitiem bestand binnenkomt.

Lidmaatschap geeft toegang, niet de koppelingssleutel

Sectie getiteld “Lidmaatschap geeft toegang, niet de koppelingssleutel”

Het bezitten van een apparaatreferentie authenticeert een apparaat. Het machtigt niets binnen een werkruimte. Elke operatie met werkruimtescope bepaalt de lidmaatschapsrol van de aanroeper en handhaaft een ondergrens die van het soort operatie is afgeleid — lezen vereist guest, een mutatie vereist member, een destructieve bewerking vereist admin — en operaties die repo-inhoud blootstellen controleren daarnaast de toegang per repo van de aanroeper. Een geldige koppelingssleutel zonder lidmaatschap krijgt Not a member of this workspace.

Het gevolg voor isolatie is fraai: lidmaatschapsrijen staan in het eigen bestand van de werkruimte, dus “wie zit in deze werkruimte?” is een gewone leesopdracht met werkruimtescope. De omgekeerde vraag — “in welke werkruimtes zit ik?” — is per definitie over bestanden verdeeld, dus het is een van de weinige goedgekeurde fan-outs van CrossWorkspaceQueries, en dat is wat de werkruimtekiezer uitvoert voordat er een werkruimte is gekozen.

Zie Multiplayer voor de rollenladder en de rechten per repo.

Niet alles is werkruimtegebonden. Een tweede database, <dataDir>/global.db, bevat wat echt serverbreed is:

  • het werkruimteregister, zodat de wisselaar elke werkruimte kan tonen zonder ook maar één werkruimtebestand te openen — en zodat de naam, het logo, de eigenaar, de handmatige volgorde en de markering voor zachte verwijdering van een werkruimte buiten het bestand wonen dat ze beschrijven;
  • identiteit: gebruikers, gebruikersvoorkeuren en gekoppelde apparaten, omdat één mens één gebruiker is in elke werkruimte en een gekoppeld apparaat het verwijderen van een werkruimte overleeft;
  • de fleetwachtrij (workers, jobs, plaatsingslog), waarvan de scheduler bij elke tick de hele wachtrij tegen elke worker matcht;
  • de nieuwsfeed, die per gebruiker is (niet per werkruimte) — een feed toevoegen volgt je in elke werkruimte;
  • workspace_routes, de pre-auth-index die antwoordt op “welke werkruimte bezit deze sleutel?” voor entreepunten die alleen met een secret of een ondoorzichtig id aankomen (een uitnodigingshash, een webhooktoken, een deeplink) en zonder werkruimte. Een misser daar is een niet-gevonden; er is bewust geen scan-fallback;
  • installatiebrede instellingen en identiteit: het installatie-id, instellingen op hostniveau, SSO-verbindingen en beheerde actiebeleidsregels (een installatiebrede begrenzing die de beveiligingen van werkruimtes alleen maar strenger kan maken).

Bij het opstarten wordt alleen dit bestand geopend. Werkruimtebestanden worden lazy geopend bij het eerste gebruik, zodat de opstartkosten vlak blijven ongeacht hoeveel geschiedenis de werkruimtes opbouwen, en de quick_check per bestand wordt betaald bij het eerste gebruik in plaats van op de weg naar de ready-banner.

Een werkruimtebestand bevat alleen een workspace_meta met één rij voor zelfidentificatie (het id, de installatie die het heeft aangemaakt, de schemaversie waarmee het is aangemaakt, en wanneer). Alles wat een werkruimte beschrijft, staat in het register.

Een werkruimte aanmaken doet meer dan een rij in het register invoegen. In één bewerking stempelt de server de aanmakende gebruiker als ownerUserId, legt hij het lidmaatschap met de eigenaarsrol van die gebruiker vast en publiceert hij een WorkspaceCreated-domeingebeurtenis. Luisteraars op die gebeurtenis zaaien vervolgens, idempotent en op de achtergrond:

  • een CEO-agent plus vier specialisten — qa, architect, engineer, librarian — waarbij de specialisten aan de CEO rapporteren;
  • de ingebouwde pipelinesjablonen en hun triggers (triggers op gebeurtenissen en handmatige triggers worden ingeschakeld geleverd; geplande zijn opt-in, behalve de dagelijkse worktree-GC-sweep);
  • de starter-evalsuites.

Dit gebeurt voor elke werkruimte, niet alleen de eerste. De geseede agenten worden zonder adapter aangemaakt, dus ze draaien op Control Center (built-in) met Anthropics standaardmodel (claude-opus-4-8, tenzij je een ander vastzet) totdat je per agent een adapter en model instelt via Instellingen → Werkruimte → Agenten. Zie Het agentmodel.

Het wegschrijven van het eigenaarslidmaatschap is belangrijker dan het lijkt: zonder dat zou de pas aangemaakte werkruimte geen leden hebben en zou elke call met werkruimtescope die de maker daarna doet, worden geweigerd.

Een werkruimte verwijderen markeert de registerrij en ontkoppelt de map. De werkruimte verdwijnt uit elke lijst en zoekopdracht. De registerrij blijft staan (zacht verwijderd), zodat het id nog bekend is bij back-up en onderhoud, maar workspace.delete gooit de map van die werkruimte onmiddellijk weg — database, worktrees, agentbestanden en de rest — via WorkspaceDatabaseManager.dropAndClose. Alleen de eigenaar van de werkruimte kan hem verwijderen.

Omdat één werkruimte één bestand is, is een werkruimte doorgeven een bestandsbewerking in plaats van een dump tabel voor tabel:

  • workspace.export is één VACUUM INTO — een consistente, gedefragmenteerde momentopname die onder een leestransactie wordt gemaakt terwijl de server blijft serveren.
  • workspace.import neemt zo’n bestand over en vervangt alles wat de doelwerkruimte nu bevat. De ingebedde workspace_meta laat een import onderscheid maken tussen “mijn eigen bestand, opnieuw overgenomen” en “een bestand van een andere installatie” (dat laatste is toegestaan, maar wordt gelogd in plaats van onzichtbaar te blijven).
  • server.backupNow maakt een momentopname van de hele installatie in een map met tijdstempel die de live datadir spiegelt — manifest.json, global.db en één <workspaceId>/workspace.db per werkruimte — dus herstellen is hem terugkopiëren.
  • server.listBackups leest die map terug, nieuwste eerst, met de grootte en de werkruimtelijst van elke momentopname. Een momentopname waarvan het manifest ontbreekt of bestanden noemt die er niet zijn, wordt nog steeds gerapporteerd, gemarkeerd als onvolledig — hem verbergen is hoe een operator gaat geloven dat hij een back-up heeft die hij niet heeft.

De back-upbewerkingen staan onder Instellingen → Server → Back-up en herstel: Nu back-uppen maakt een momentopname van de installatie, de lijst toont welke momentopnamen volledig zijn en laat de eigenaar van de installatie er na bevestiging een verwijderen, en elke werkruimte biedt exporteren en importeren. De ingebouwde pipeline Workspace backup neemt volgens een wekelijks schema alleen de database van die werkruimte mee en verwijdert zijn eigen back-ups die ouder zijn dan de ingestelde bewaartermijn. Een werkruimte herstellen uit een momentopname van de installatie is workspace.import richten op de <workspaceId>/workspace.db van de momentopname; een hele installatie herstellen blijft een terugkopieeractie van de momentopnamemap met de server gestopt.

Wat geen van alle meeneemt, is de rest van de werkruimtemap. Een werkruimte verzamelt meer dan een database — geplakte afbeeldingen, skill- en agentbestanden, chatreferenties, worktrees — en export en import verplaatsen het bestand, dus alles ernaast blijft waar het staat. Dat is bewust voor referenties en wegwerpbaar voor worktrees; voor de rest is het een grens die je moet kennen in plaats van een functie. De back-upreferentie bevat de volledige tabel.

Een back-up verplaatsen tussen de server en je apparaat

Sectie getiteld “Een back-up verplaatsen tussen de server en je apparaat”

Elke operatie hierboven spreekt in paden op de server, wat alleen een volledig antwoord is als de server je eigen machine is. Drie ondertekende HTTP-routes — /backup/workspace, /backup/snapshot en /backup/restore — dragen de bytes voor elke andere topologie, op dezelfde geauthenticeerde baan die de mediaproxy en het afbeeldingsarchief al gebruiken. Op de pagina is dit een knop Downloaden naast elke export en elke momentopname (jij kiest waar hij landt), en Bestand kiezen en uploaden om te herstellen uit een bestand dat op de machine staat die je gebruikt. De upload is degene die werkt als de server niet je machine is: workspace.import heeft het bestand al op de host nodig, en zo komt het daar.

Drie eigenschappen zijn het kennen waard:

  • Een download maakt een verse kopie en de server houdt er niets van. Downloaden is niet “het bestand ophalen dat de exportknop heeft gemaakt” — er wordt opnieuw geëxporteerd, dat wordt gestreamd en daarna verwijderd. Het antwoord is no-store met Accept-Ranges: none om dezelfde reden: een hervat bereik zou twee verschillende exports aan elkaar plakken tot één bestand dat geldig lijkt maar het niet is.
  • Een upload wordt naar schijf gestreamd, nooit gebufferd, aan beide kanten, en de klaargezette kopie wordt op elk uitpad verwijderd — ook bij een weigering. Een werkruimtedatabase is geen schermafbeelding.
  • Een doorgestuurde verbinding heeft geen HTTP-origin, dus daar zijn de overdrachtsbedieningselementen uitgeschakeld met een melding waarom. Alles wat in paden aan de serverkant spreekt, blijft via de relay werken; alleen de byte-banen hebben een directe verbinding nodig.

Je kunt meerdere werkruimtes naast elkaar draaien. Elke werkruimte werkt onafhankelijk: agenten in de ene werkruimte kunnen geen berichten sturen naar agenten in een andere, kunnen het geheugen van een andere werkruimte niet lezen en zien de tickets ervan niet. Het bepalen van de ontvanger voor berichten tussen agenten is exact en gaat nooit een werkruimte over.

Er is geen dashboard over werkruimtes heen. Het analytics-oppervlak op /workspaces/:workspaceId/observability abonneert zich op een begrensde globale feed van recente runs en vernauwt die meteen tot de actieve werkruimte, dus wat je daar ziet is één werkruimte tegelijk. De enige echt multi-werkruimte-oppervlakken die een mens aanraakt, zijn de werkruimtekiezer en de wisselaar.

Eén praktische grens die het kennen waard is: een werkruimte die iemand gebruikt blijft open — er is geen LRU. Voorbij 32 daarvan waarschuwt de server in plaats van te weigeren, omdat elk geopend bestand een achtergrond-isolate en een page-cache vasthoudt. Cross-workspace-fan-out opent bestanden kortstondig en sluit ze daarna als niets anders ze heeft opgeëist.