Sari la conținut

Spații de lucru și izolare

Un spațiu de lucru este chiriașul de izolare de nivel superior din Control Center. El grupează agenți, depozite, spații, tichete, memorie, pipeline-uri și membri într-un context delimitat. Tot ce se întâmplă în interiorul unui spațiu de lucru rămâne în interiorul lui.

Un spațiu de lucru tipic corespunde unui proiect sau unei baze de cod. Poți avea spații de lucru separate pentru „api-service”, „frontend-v2” și „infrastructure”.

Rândurile unui spațiu de lucru se află în fișierul SQLite propriu acelui spațiu, la <dataDir>/<workspaceId>/workspace.db. Fiecare spațiu de lucru primește un director, nu un simplu fișier, pentru că un spațiu de lucru acumulează mai mult decât o bază de date — worktree-uri pentru fiecare conversație, fișierele agenților și ale skill-urilor, acreditările boților de chat — iar faptul că primește un folder înseamnă că toate acestea trăiesc și sunt șterse împreună.

Entitate Domeniu de aplicare
Agenți Aparțin exact unui spațiu de lucru
Depozite Înregistrate într-un singur spațiu de lucru (vezi mai jos)
Spații, conversații și mesaje Limitate la spațiul de lucru
Tichete, proiecte, planuri, orchestrări Limitate la spațiul de lucru
Fapte de memorie, politici, domenii, acordări de acces Limitate la spațiul de lucru
Rulări de pipeline, șabloane, trigger-e Limitate la spațiul de lucru
Starea pull request-urilor, spații de revizuire, cohorte de revizuire Limitate la spațiul de lucru
Jurnale de rulare ale agenților și memoria de lucru Limitate la spațiul de lucru
Membri, invitații, acordări per depozit Limitate la spațiul de lucru
Simboluri și muchii ale grafului de cod Limitate după repoId în fișierul spațiului de lucru

Majoritatea acestor rânduri poartă o coloană workspaceId; fișierele de bază de date separate impun izolarea. Vezi invariantul de izolare de mai jos.

Repo-urile sunt limitate la spațiul de lucru; identitatea între spații de lucru este dată de cale

Secțiune intitulată „Repo-urile sunt limitate la spațiul de lucru; identitatea între spații de lucru este dată de cale”

Un rând de depozit se află în fișierul de bază de date propriu spațiului său de lucru — fișierul este domeniul de aplicare, așa că tabelul nu are nicio coloană workspaceId și niciun tabel de legătură.

Adaugi un repo într-un spațiu de lucru la Setări → Spațiu de lucru → Depozite. Înregistrarea aceluiași checkout în două spații de lucru creează două rânduri independente, cu două id-uri; identitatea unui repo între spații de lucru este dată de calea din sistemul de fișiere (sau de perechea proprietar/nume de pe GitHub), niciodată de id.

Înregistrarea este strictă: calea trebuie să fie în interiorul unui arbore de lucru git, trebuie să aibă un remote origin, iar acel remote trebuie să indice către o forge acceptată (github.com, gitlab.com sau bitbucket.org). Forge-ul este citit din remote și stocat pe depozit, așa că un spațiu de lucru poate ține depozite din toate cele trei. Orice altceva este refuzat. Browserul de foldere din care alegi listează sistemul de fișiere al gazdei serverului, limitat la lista de permisiuni --repo-roots a serverului (care implicit este directorul home al utilizatorului serverului).

Eliminarea unui depozit dintr-un spațiu de lucru șterge rândul cu desăvârșire și propagă în cascadă simbolurile de cod, muchiile, fișierele, punctele de control ale indexului și acordările per membru. Checkout-ul de pe disc rămâne neatins.

Graful de cod se află în același fișier, indexat după repoId, pentru că spații de lucru diferite pot avea același depozit pe ramuri diferite, cu cod diferit.

Scurgerile de date între spații de lucru sunt tratate ca bug-uri. Izolarea este structurală, impusă la mai multe niveluri:

  • Stratul de bază de date. Fiecare spațiu de lucru are propriul fișier SQLite, distribuit de WorkspaceDatabaseManager.of(workspaceId). O bază de date de spațiu de lucru nu declară users, workspaces sau tabelele vreunui alt spațiu de lucru, așa că o citire între spații de lucru nu este un bug pe care îl poți scrie — nu se compilează. Depozitele își rezolvă DAO-ul la fiecare apel în loc să pună unul în cache, iar pentru că fiecare metodă de depozit cere un workspaceId obligatoriu, spațiul de lucru nu poate fi niciodată dedus sau implicit.
  • Id-ul spațiului de lucru este un segment de cale. Pentru că id-ul devine nume de director, el trebuie să respecte un model sigur cu un singur segment (începe cu alfanumeric, apoi alfanumerice, puncte, liniuțe de subliniere, cratime; fără ..). În tot produsul, id-urile sunt UUID-uri. Orice altceva aruncă o excepție în loc să fie sanitizat — aceasta este garda împotriva traversării de cale și eșuează zgomotos în loc să scrie datele unui spațiu de lucru la o cale aleasă de altcineva.
  • Apărare în adâncime. Coloanele workspaceId sunt încă acolo și încă sunt scrise. Redundante în cadrul unui fișier, ele păstrează neschimbate trigger-ele fluxului de sincronizare, indexurile FTS și forma existentă a rândurilor și fac un fișier auto-descriptiv atunci când este inspectat singur — pe asta se bazează exportul și importul.
  • Stratul de domeniu. Serviciile care modifică entități după id validează entity.workspaceId == workspaceId la un singur punct de trecere înainte de a continua și aruncă WorkspaceMismatchException la nepotrivire — eșecul zgomotos pentru un id venit din contextul greșit. Refuzul zgomotos contează: un no-op silențios ascunde bug-ul, iar continuarea scurge date.
  • Stratul RPC. Dispecerul repo-RPC este punctul de trecere. Fiecare operație limitată la spațiul de lucru trebuie să poarte workspace_id în propriile argumente — nu există un „spațiu de lucru curent” per sesiune care ar putea scurge, iar un id necunoscut este refuzat ca inexistent înainte ca ceva să deschidă o bază de date, așa că un id greșit nu poate materializa un workspace.db fantomă. Operațiile cu adevărat globale (fluxul de știri, coada flotei) se declară fără domeniu de aplicare, printr-o decizie explicită și revizuită. Instrumentele MCP urmează aceeași regulă: orice instrument care atinge date limitate la spațiul de lucru cere workspace_id.
  • Distribuirea între spații de lucru este enumerabilă. A răspunde la o întrebare despre mai multe spații de lucru înseamnă a deschide mai multe baze de date. Această distribuire este limitată la un singur helper, CrossWorkspaceQueries, așa că lista completă a lucrurilor care traversează legitim granița stă în punctele de apel dintr-un singur fișier, în loc să se difuzeze în spatele comentariilor de documentație. Acele puncte de apel sunt: agregarea la nivel de server, reconcilierile de pornire, curățările de retenție și GC, backupul, routerele de evenimente — și căutarea apartenenței de mai jos.
  • Accesul doar după id nu este suficient. Căutarea unei entități după UUID-ul ei nu dovedește că aparține spațiului de lucru al apelantului. Separarea fișierelor face fișierul greșit inaccesibil; validarea din stratul de domeniu prinde un id greșit care ajunge printr-un fișier legitim.

Apartenența, nu cheia de asociere, este cea care acordă accesul

Secțiune intitulată „Apartenența, nu cheia de asociere, este cea care acordă accesul”

Deținerea unei acreditări de dispozitiv autentifică un dispozitiv. Ea nu autorizează nimic în interiorul unui spațiu de lucru. Fiecare operație limitată la spațiul de lucru rezolvă rolul de apartenență al apelantului și impune un prag derivat din tipul operației — citirea cere guest, o mutație cere member, o operație distructivă cere admin — iar operațiile care expun conținut de depozit verifică în plus acordarea per depozit a apelantului. O cheie de asociere validă, dar fără apartenență, primește Not a member of this workspace.

Consecința pentru izolare este elegantă: rândurile de apartenență se află în fișierul propriu al spațiului de lucru, așa că „cine este în acest spațiu de lucru?” este o citire obișnuită, limitată. Întrebarea inversă — „în ce spații de lucru sunt?” — este prin definiție răspândită în mai multe fișiere, așa că este una dintre puținele distribuiri sancționate ale CrossWorkspaceQueries și este ceea ce rulează selectorul de spații de lucru înainte ca vreun spațiu de lucru să fie ales.

Vezi Multiplayer pentru ierarhia de roluri și acordările per depozit.

Nu totul este limitat la spațiul de lucru. O a doua bază de date, <dataDir>/global.db, ține ceea ce este cu adevărat la nivel de server:

  • registrul spațiilor de lucru, ca comutatorul să poată lista fiecare spațiu de lucru fără a deschide vreun fișier de spațiu de lucru — și ca numele, sigla, proprietarul, ordinea în manual și marcajul de ștergere logică ale unui spațiu de lucru să se afle în afara fișierului pe care îl descriu;
  • identitatea: utilizatori, preferințele utilizatorilor și dispozitivele asociate, pentru că un singur om este un singur utilizator în toate spațiile de lucru, iar un dispozitiv asociat supraviețuiește ștergerii unui spațiu de lucru;
  • coada flotei (workeri, sarcini, jurnal de plasare), al cărei planificator potrivește întreaga coadă cu fiecare worker la fiecare ciclu;
  • fluxul de știri, care este per utilizator (nu per spațiu de lucru) — adăugarea unui flux te urmează în fiecare spațiu de lucru;
  • workspace_routes, indexul dinainte de autentificare care răspunde la „cărui spațiu de lucru îi aparține această cheie?” pentru punctele de intrare care sosesc doar cu un secret sau un id opac (un hash de invitație, un token de webhook, un link profund) și fără niciun spațiu de lucru. O ratare acolo înseamnă inexistent; în mod deliberat nu există nicio soluție de rezervă cu scanare;
  • setări și identitate la nivel de instalare: id-ul instalării, setările la nivel de gazdă, conexiunile SSO și politicile de acțiuni administrate (o limită la nivel de instalare care poate doar înăspri guardrail-urile spațiilor de lucru).

Pornirea deschide doar acest fișier. Fișierele spațiilor de lucru se deschid leneș, la prima atingere, așa că costul pornirii rămâne constant indiferent cât de mult istoric acumulează spațiile de lucru, iar quick_check per fișier se plătește la prima utilizare, nu pe drumul către bannerul de disponibilitate.

Un fișier de spațiu de lucru conține doar un workspace_meta cu un singur rând, pentru autoidentificare (id-ul lui, instalarea care l-a creat, versiunea de schemă cu care a fost creat, când anume). Tot ce descrie un spațiu de lucru se află în registru.

Crearea unui spațiu de lucru face mai mult decât să insereze un rând în registru. Într-o singură operație, serverul îl marchează pe utilizatorul care creează drept ownerUserId, înregistrează apartenența acelui utilizator cu rol de proprietar și publică un eveniment de domeniu WorkspaceCreated. Ascultătorii acelui eveniment populează apoi, idempotent și în fundal:

  • un agent CEO plus patru specialiști — qa, architect, engineer, librarian — specialiștii raportând CEO-ului;
  • șabloanele de pipeline încorporate și trigger-ele lor (trigger-ele de eveniment și cele manuale vin activate; cele programate sunt opționale, cu excepția curățării zilnice de GC a worktree-urilor);
  • suitele eval de pornire.

Acest lucru se întâmplă pentru fiecare spațiu de lucru, nu doar pentru primul. Agenții creați prin populare sunt creați fără adaptor, așa că rulează pe Control Center (built-in) cu modelul implicit al Anthropic (claude-opus-4-8 dacă nu fixezi altul) până când setezi un adaptor și un model per agent la Setări → Spațiu de lucru → Agenți. Vezi Modelul agenților.

Scrierea apartenenței de proprietar contează mai mult decât pare: fără ea, spațiul de lucru abia creat nu ar avea niciun membru, iar fiecare apel limitat la spațiul de lucru pe care îl face în continuare creatorul ar fi refuzat.

Ștergerea unui spațiu de lucru marchează rândul din registru și rupe legătura cu directorul. Spațiul de lucru dispare din fiecare listă și din fiecare interogare. Rândul din registru rămâne (șters logic), așa că id-ul este în continuare cunoscut de backup și de mentenanță, dar workspace.delete elimină imediat directorul acelui spațiu de lucru — baza de date, worktree-urile, fișierele agenților și restul — prin WorkspaceDatabaseManager.dropAndClose. Doar proprietarul spațiului de lucru îl poate șterge.

Pentru că un spațiu de lucru este un singur fișier, a da un spațiu de lucru mai departe este o operație pe fișiere, nu un dump tabel cu tabel:

  • workspace.export este un singur VACUUM INTO — un snapshot consistent și defragmentat, luat într-o tranzacție de citire în timp ce serverul continuă să servească.
  • workspace.import adoptă un astfel de fișier, înlocuind tot ce deține în acel moment spațiul de lucru țintă. workspace_meta încorporat permite unui import să distingă „propriul meu fișier, readoptat” de „un fișier din altă instalare” (cel din urmă este permis, dar consemnat în jurnal, nu invizibil).
  • server.backupNow face un snapshot al întregii instalări într-un director cu timestamp care oglindește directorul de date live — manifest.json, global.db și un <workspaceId>/workspace.db per spațiu de lucru — așa că restaurarea înseamnă să-l copiezi înapoi.
  • server.listBackups citește acel director înapoi, cele mai noi primele, cu dimensiunea și lista de spații de lucru ale fiecărui snapshot. Un snapshot al cărui manifest lipsește sau care numește fișiere ce nu sunt acolo este totuși raportat, marcat ca incomplet — ascunderea lui este modul în care un operator ajunge să creadă că are un backup pe care nu îl are.

Operațiile de backup se află sub Setări → Server → Backup și restaurare: Fă backup acum ia un snapshot al instalării, lista arată ce snapshoturi sunt complete și îi permite proprietarului instalării să șteargă unul după confirmare, iar fiecare spațiu de lucru oferă export și import. Pipeline-ul încorporat Workspace backup ia doar baza de date a acelui spațiu de lucru, după un program săptămânal, și își șterge propriile backupuri mai vechi decât vârsta de retenție configurată. Restaurarea unui spațiu de lucru dintr-un snapshot al instalării este workspace.import îndreptat către <workspaceId>/workspace.db al snapshotului; restaurarea întregii instalări rămâne o copiere înapoi, cu serverul oprit, a directorului snapshotului.

Ce nu transportă niciuna dintre ele este restul directorului spațiului de lucru. Un spațiu de lucru acumulează mai mult decât o bază de date — imagini lipite, fișiere de skill-uri și de agenți, acreditări de chat, worktree-uri — iar exportul și importul mută fișierul, așa că tot ce stă alături de el rămâne acolo unde este. Acest lucru este deliberat pentru acreditări și de unică folosință pentru worktree-uri; pentru restul este o limită de cunoscut, nu o funcționalitate. Referința pentru backup are tabelul complet.

Fiecare operație de mai sus vorbește în căi de pe server, ceea ce este un răspuns complet doar atunci când serverul este propria ta mașină. Trei rute HTTP semnate — /backup/workspace, /backup/snapshot și /backup/restore — cară octeții pentru orice altă topologie, pe aceeași cale autentificată pe care o folosesc deja proxy-ul media și depozitul de imagini. În pagină, acesta este un buton Descarcă lângă fiecare export și fiecare snapshot (tu alegi unde ajunge) și Alege un fișier și încarcă pentru a restaura dintr-un fișier aflat pe mașina pe care o folosești. Încărcarea este varianta care funcționează când serverul nu este mașina ta: workspace.import are nevoie ca fișierul să fie deja pe gazdă, iar așa ajunge acolo.

Trei proprietăți merită cunoscute:

  • O descărcare creează o copie nouă, iar serverul nu păstrează nimic din ea. Descărcarea nu înseamnă „preia fișierul pe care l-a creat butonul de export” — exportă din nou, transmite în flux rezultatul și îl șterge. Răspunsul este no-store cu Accept-Ranges: none din același motiv: un interval reluat ar îmbina două exporturi diferite într-un singur fișier care pare valid, dar nu este.
  • O încărcare este transmisă în flux direct pe disc, niciodată tamponată, la ambele capete, iar copia pregătită este ștearsă pe fiecare cale de ieșire — inclusiv la un refuz. O bază de date de spațiu de lucru nu este o captură de ecran.
  • O conexiune prin releu nu are origine HTTP, așa că acolo controalele de transfer sunt dezactivate, cu o notă care explică de ce. Tot ce vorbește în căi de pe server continuă să funcționeze prin releu; doar căile de octeți au nevoie de o conexiune directă.

Poți rula mai multe spații de lucru în paralel. Fiecare funcționează independent: agenții dintr-un spațiu de lucru nu pot trimite mesaje agenților din altul, nu pot citi memoria altui spațiu de lucru și nu pot vedea tichetele acestuia. Rezolvarea destinatarilor pentru mesageria de la agent la agent este exactă și nu traversează niciodată un spațiu de lucru.

Nu există niciun tablou de bord care să acopere mai multe spații de lucru. Suprafața de analiză de la /workspaces/:workspaceId/observability se abonează la un flux global mărginit de rulări recente și îl restrânge imediat la spațiul de lucru activ, așa că ceea ce vezi acolo este un singur spațiu de lucru odată. Singurele suprafețe cu adevărat multi-spațiu pe care le atinge o persoană sunt selectorul de spații de lucru și comutatorul.

O limită practică pe care merită să o știi: un spațiu de lucru pe care cineva îl folosește rămâne deschis — nu există LRU. Dincolo de 32 dintre ele, serverul avertizează în loc să refuze, pentru că fiecare fișier deschis ține un isolate de fundal și un cache de pagini. Distribuirea între spații de lucru deschide fișiere tranzitoriu și le închide după aceea, dacă nimic altceva nu le-a revendicat.