Workspaces and isolation
เนื้อหานี้ยังไม่มีในภาษาของคุณ
What is a workspace?
Section titled “What is a workspace?”A workspace is the top-level isolation tenant in Control Center. It groups agents, repositories, spaces, tickets, memory, pipelines and members into a bounded context. Everything that happens inside a workspace stays inside it.
A typical workspace maps to one project or one codebase. You might have separate workspaces for “api-service”, “frontend-v2” and “infrastructure”.
Workspace-scoped data
Section titled “Workspace-scoped data”A workspace’s rows live in that workspace’s own SQLite file, at <dataDir>/<workspaceId>/workspace.db. Each workspace gets a directory rather than a bare file, because a workspace accumulates more than a database — per-conversation worktrees, the agent and skill files, chat bot credentials — and giving it a folder means all of that lives and is deleted, together.
| Entity | Scope |
|---|---|
| Agents | Belong to exactly one workspace |
| Repositories | Registered into one workspace (see below) |
| Spaces, conversations and messages | Workspace-scoped |
| Tickets, projects, plans, orchestrations | Workspace-scoped |
| Memory facts, policies, domains, access grants | Workspace-scoped |
| Pipeline runs, templates, triggers | Workspace-scoped |
| Pull request state, review spaces, review cohorts | Workspace-scoped |
| Agent run logs and working memory | Workspace-scoped |
| Members, invites, per-repo grants | Workspace-scoped |
| Code graph symbols and edges | Scoped by repoId within the workspace file |
Most of these rows carry a workspaceId column; separate database files enforce isolation. See the isolation invariant below.
Repos are workspace-scoped; identity across workspaces is by path
Section titled “Repos are workspace-scoped; identity across workspaces is by path”A repo row lives in its workspace’s own database file — the file is the scope, so the table has no workspaceId column and no join table.
You add a repo into a workspace at Settings → Workspace → Repositories. Registering the same checkout in two workspaces creates two independent rows with two ids; repo identity across workspaces is by filesystem path (or GitHub owner/name), never by id.
Registration is strict: the path must be inside a git work tree, it must have an origin remote and that remote must point at a supported forge (github.com, gitlab.com or bitbucket.org). The forge is read from the remote and stored on the repo, so one workspace can hold repos from all three. Anything else is rejected. The folder browser you pick from lists the server host’s filesystem, confined to the server’s --repo-roots allow-list (which defaults to the server user’s home directory).
Removing a repository from a workspace deletes the row outright and cascades its code symbols, edges, files, index checkpoints and per-member grants. The checkout on disk is untouched.
The code graph lives in the same file, keyed by repoId, because different workspaces may have the same repository on different branches with different code.
The isolation invariant
Section titled “The isolation invariant”Cross-workspace data leaks are treated as bugs. Isolation is structural, enforced at several levels:
- Database layer. Each workspace has its own SQLite file, handed out by
WorkspaceDatabaseManager.of(workspaceId). A workspace database does not declareusers,workspaces, or any other workspace’s tables, so a cross-workspace read is not a bug you can write — it does not compile. Repositories resolve their DAO per call rather than caching one and because every repository method takes a requiredworkspaceId, the workspace can never be inferred or defaulted. - The workspace id is a path segment. Because the id becomes a directory name, it must match a safe single-segment pattern (alphanumeric start, then alphanumerics, dots, underscores, hyphens; no
..). Ids are UUIDs everywhere in the product. Anything else throws rather than being sanitized — that is the path-traversal guard and it fails loudly rather than writing a workspace’s data to a path someone else chose. - Defense in depth. The
workspaceIdcolumns are still there and still written. Redundant within a file, they keep the sync-feed triggers, FTS indexes and existing row shapes unchanged and they make a file self-describing when it is inspected on its own — which is what export and import rely on. - Domain layer. Services that mutate entities by id validate
entity.workspaceId == workspaceIdat a single chokepoint before proceeding and throwWorkspaceMismatchExceptionon mismatch — the loud failure for an id that arrived from the wrong context. Denying loudly matters: a silent no-op hides the bug and proceeding leaks. - RPC layer. The repo-RPC dispatcher is the chokepoint. Every workspace-scoped op must carry
workspace_idin its own args — there is no per-session “current workspace” to leak and an unknown id is refused as not-found before anything opens a database, so a bad id cannot materialize a ghostworkspace.db. Genuinely global ops (the newsfeed, the fleet queue) declare themselves unscoped as an explicit, reviewed decision. MCP tools follow the same rule: any tool touching workspace-scoped data requiresworkspace_id. - Cross-workspace fan-out is enumerable. Answering a question about several workspaces means opening several databases. That fan-out is confined to one helper,
CrossWorkspaceQueries, so the complete list of things that legitimately cross the boundary lives in one file’s call sites instead of diffusing behind doc comments. Those call sites are: server-wide aggregation, startup reconcilers, retention and GC sweeps, backup, event routers — and the membership lookup below. - ID-only access is not sufficient. Looking up an entity by its UUID does not prove it belongs to the caller’s workspace. The file split makes the wrong file unreachable; the domain-layer validation catches a wrong id that arrives through a legitimate file.
Membership, not the pairing key, is what grants access
Section titled “Membership, not the pairing key, is what grants access”Holding a device credential authenticates a device. It does not authorize anything inside a workspace. Every workspace-scoped op resolves the caller’s membership role and enforces a floor derived from the op’s kind — read needs guest, a mutation needs member, a destructive operation needs admin — and ops that expose repo content additionally check the caller’s per-repo grant. A valid pairing key with no membership gets Not a member of this workspace.
The consequence for isolation is neat: membership rows live in the workspace’s own file, so “who is in this workspace?” is an ordinary scoped read. The inverse question — “which workspaces am I in?” — is by definition spread across files, so it is one of the few sanctioned CrossWorkspaceQueries fan-outs and it is what the workspace picker runs before any workspace has been chosen.
See Multiplayer for the role ladder and repo grants.
The global registry
Section titled “The global registry”Not everything is workspace-scoped. A second database, <dataDir>/global.db, holds what is genuinely server-wide:
- the workspace registry, so the switcher can list every workspace without opening a single workspace file — and so a workspace’s name, logo, owner, manual order and soft-delete marker live outside the file they describe;
- identity: users, user preferences and paired devices, because one human is one user across every workspace and a paired device survives a workspace being deleted;
- the fleet queue (workers, jobs, placement log), whose scheduler matches the whole queue against every worker on each tick;
- the newsfeed, which is per-user (not per-workspace) — adding a feed follows you into every workspace;
workspace_routes, the pre-auth index that answers “which workspace owns this key?” for entry points that arrive with nothing but a secret or an opaque id (an invite hash, a webhook token, a deep link) and no workspace. A miss there is a not-found; there is deliberately no scan fallback;- install-wide settings and identity: the install id, host-level settings, SSO connections and managed action policies (an install-wide clamp that can only tighten workspace guardrails).
Boot opens only this file. Workspace files open lazily on first touch, so startup cost stays flat no matter how much history the workspaces accumulate and the per-file quick_check is paid on first use rather than on the path to the ready banner.
A workspace file carries only a single-row workspace_meta for self-identification (its id, the install that created it, the schema version it was created with, when). Everything descriptive about a workspace is in the registry.
Creating a workspace
Section titled “Creating a workspace”Creating a workspace does more than insert a registry row. In one operation the server stamps the creating user as ownerUserId, records that user’s owner-role membership and publishes a WorkspaceCreated domain event. Listeners on that event then seed, idempotently and in the background:
- a CEO agent plus four specialists —
qa,architect,engineer,librarian— with the specialists reporting to the CEO; - the built-in pipeline templates and their triggers (event and manual triggers ship enabled; scheduled ones are opt-in except the daily worktree-GC sweep);
- the starter eval suites.
This happens for every workspace, not only the first. The seeded agents are created with no adapter, so they run on Control Center (built-in) with Anthropic’s default model (claude-opus-4-8 unless you pin another) until you set an adapter and model per agent at Settings → Workspace → Agents. See The agent model.
The owner-membership write matters more than it looks: without it the freshly created workspace would have no members and every workspace-scoped call the creator makes next would be denied.
Deleting, exporting, importing
Section titled “Deleting, exporting, importing”Deleting a workspace marks the registry row and unlinks the directory. The workspace disappears from every list and lookup. The registry row stays (soft-deleted) so the id is still known to backup and maintenance, but workspace.delete immediately drops that workspace’s directory — database, worktrees, agent files and the rest — via WorkspaceDatabaseManager.dropAndClose. Only the workspace owner can delete it.
Because one workspace is one file, handing a workspace around is a file operation rather than a table-by-table dump:
workspace.exportis a singleVACUUM INTO— a consistent, defragmented snapshot taken under a read transaction while the server keeps serving.workspace.importadopts such a file, replacing whatever the target workspace currently holds. The embeddedworkspace_metalets an import distinguish “my own file, re-adopted” from “a file from another install” (the latter is allowed, but logged rather than invisible).server.backupNowsnapshots the whole install into a timestamped directory that mirrors the live data dir —manifest.json,global.dband one<workspaceId>/workspace.dbper workspace — so restoring is copying it back.server.listBackupsreads that directory back, newest first, with the size and workspace list of each snapshot. A snapshot whose manifest is missing or names files that are not there is still reported, flagged incomplete — hiding it is how an operator comes to believe they have a backup they do not.
The backup operations live under Settings → Server → Backup & restore:
Back up now takes an install snapshot, the list shows which snapshots are
complete and lets the install owner delete one after confirmation, and each
workspace offers export and import. The built-in Workspace backup pipeline
takes only that workspace’s database on a weekly schedule and deletes its own
backups older than the configured retention age. Restoring a workspace from an
install snapshot is workspace.import pointed at the snapshot’s
<workspaceId>/workspace.db; restoring a whole install remains a stopped-server
copy-back of the snapshot directory.
What none of them carry is the rest of the workspace directory. A workspace accumulates more than a database — pasted images, skill and agent files, chat credentials, worktrees — and export and import move the file, so everything beside it stays where it is. That is deliberate for credentials and disposable for worktrees; for the rest it is a limit to know about rather than a feature. The backup reference has the full table.
Moving a backup between the server and your device
Section titled “Moving a backup between the server and your device”Every op above speaks in paths on the server, which is a complete answer only when the server is your own machine. Three signed HTTP routes — /backup/workspace, /backup/snapshot and /backup/restore — carry the bytes for every other topology, on the same authenticated lane the media proxy and the image store already use. On the page this is a Download button beside each export and each snapshot (you pick where it lands), and Choose a file and upload to restore from a file sitting on the machine you are using. The upload is the one that works when the server is not your machine: workspace.import needs the file to already be on the host, and this is how it gets there.
Three properties are worth knowing:
- A download mints a fresh copy and the server keeps none of it. Downloading is not “fetch the file the export button made” — it exports again, streams that, and deletes it. The response is
no-storewithAccept-Ranges: nonefor the same reason: a resumed range would splice two different exports into one file that looks valid and is not. - An upload is streamed to disk, never buffered, on both ends, and the staged copy is deleted on every path out — including a refusal. A workspace database is not a screenshot.
- A relayed connection has no HTTP origin, so the transfer controls are disabled there with a note saying why. Everything that speaks in server-side paths keeps working over the relay; only the byte lanes need a direct connection.
Multiple workspaces
Section titled “Multiple workspaces”You can run multiple workspaces side by side. Each operates independently: agents in one workspace cannot message agents in another, cannot read another workspace’s memory and cannot see its tickets. Recipient resolution for agent-to-agent messaging is exact and never crosses a workspace.
There is no cross-workspace dashboard. The analytics surface at /workspaces/:workspaceId/observability subscribes to a bounded global feed of recent runs and immediately narrows it to the active workspace, so what you see there is one workspace at a time. The only genuinely multi-workspace surfaces a person touches are the workspace picker and the switcher.
One practical limit worth knowing: a workspace someone is using stays open — there is no LRU. Past 32 of those the server warns rather than refusing, because each open file holds a background isolate and a page cache. Cross-workspace fan-out opens files transiently and closes them afterwards if nothing else has claimed them.
Related concepts
Section titled “Related concepts”- Multiplayer: membership, roles and repo grants — the access half of isolation
- The agent model: agents belong to exactly one workspace
- Memory and knowledge: memory is workspace-scoped
- Architecture: where the databases sit in the client/server split
- Back up and restore: taking a snapshot, moving a workspace, keeping copies elsewhere
- Backup, export and import: every operation, route, manifest field and limit
- Add repositories: registering a checkout into a workspace