Перейти к содержимому

Рабочие пространства и изоляция

Рабочее пространство — тенант изоляции верхнего уровня в Control Center. Оно объединяет агентов, репозитории, пространства, тикеты, память, конвейеры и участников в ограниченный контекст. Всё, что происходит внутри рабочего пространства, остаётся внутри него.

Обычно рабочее пространство соответствует одному проекту или одной кодовой базе. У вас могут быть отдельные рабочие пространства для «api-service», «frontend-v2» и «infrastructure».

Данные, привязанные к рабочему пространству

Заголовок раздела «Данные, привязанные к рабочему пространству»

Строки рабочего пространства живут в его собственном файле SQLite по пути <dataDir>/<workspaceId>/workspace.db. Каждому рабочему пространству достаётся каталог, а не отдельный файл, потому что оно накапливает больше, чем базу данных, — worktree для каждого разговора, файлы агентов и навыков, учётные данные чат-бота, — и папка означает, что всё это живёт и удаляется вместе.

Сущность Область действия
Агенты Принадлежат ровно одному рабочему пространству
Репозитории Регистрируются в одном рабочем пространстве (см. ниже)
Пространства, разговоры и сообщения Привязаны к рабочему пространству
Тикеты, проекты, планы, оркестрации Привязаны к рабочему пространству
Факты памяти, политики, домены, выданные доступы Привязаны к рабочему пространству
Запуски конвейеров, шаблоны, триггеры Привязаны к рабочему пространству
Состояние pull request, пространства ревью, когорты ревью Привязаны к рабочему пространству
Журналы запусков агентов и рабочая память Привязаны к рабочему пространству
Участники, приглашения, права на уровне репозитория Привязаны к рабочему пространству
Символы и рёбра графа кода Ограничены repoId внутри файла рабочего пространства

Большинство этих строк содержит столбец workspaceId; изоляцию обеспечивают отдельные файлы баз данных. Об инварианте изоляции — ниже.

Репозитории привязаны к рабочему пространству; между пространствами идентичность определяется путём

Заголовок раздела «Репозитории привязаны к рабочему пространству; между пространствами идентичность определяется путём»

Строка репозитория живёт в собственном файле базы данных рабочего пространства — сам файл и есть область действия, поэтому в таблице нет столбца workspaceId и нет таблицы связей.

Репозиторий добавляется в рабочее пространство в разделе Настройки → Рабочее пространство → Репозитории. Регистрация одной и той же рабочей копии в двух рабочих пространствах создаёт две независимые строки с двумя идентификаторами; идентичность репозитория между рабочими пространствами определяется путём в файловой системе (или парой владелец/имя на GitHub), но никогда — идентификатором.

Регистрация строгая: путь должен находиться внутри рабочего дерева git, у него должен быть удалённый репозиторий origin, и этот удалённый репозиторий должен указывать на поддерживаемый хост кода (github.com, gitlab.com или bitbucket.org). Хост кода считывается из удалённого репозитория и сохраняется в репозитории, поэтому в одном рабочем пространстве могут быть репозитории всех трёх. Всё остальное отклоняется. Браузер папок, из которого вы выбираете, показывает файловую систему хостовой машины сервера, ограниченную списком разрешённых путей --repo-roots (по умолчанию — домашний каталог пользователя, под которым работает сервер).

Удаление репозитория из рабочего пространства удаляет строку сразу и каскадно удаляет его символы кода, рёбра, файлы, контрольные точки индекса и права участников. Рабочая копия на диске не затрагивается.

Граф кода живёт в том же файле с ключом repoId, потому что в разных рабочих пространствах один и тот же репозиторий может быть на разных ветках с разным кодом.

Утечки данных между рабочими пространствами считаются багами. Изоляция структурна и обеспечивается на нескольких уровнях:

  • Уровень базы данных. У каждого рабочего пространства свой файл SQLite, который выдаёт WorkspaceDatabaseManager.of(workspaceId). База рабочего пространства не объявляет таблиц users, workspaces или любого другого рабочего пространства, поэтому чтение из чужого пространства — это баг, который нельзя написать: он не компилируется. Репозитории получают свой DAO на каждый вызов, а не кэшируют его, и поскольку каждый метод репозитория принимает обязательный workspaceId, рабочее пространство никогда нельзя вывести неявно или подставить по умолчанию.
  • Идентификатор рабочего пространства — это сегмент пути. Поскольку идентификатор становится именем каталога, он должен соответствовать безопасному шаблону из одного сегмента (начинается с буквы или цифры, далее буквы и цифры, точки, подчёркивания, дефисы; без ..). Во всём продукте идентификаторы — это UUID. Всё остальное приводит к исключению, а не к очистке: это защита от обхода пути, и она падает громко, вместо того чтобы записать данные рабочего пространства по пути, выбранному кем-то другим.
  • Эшелонированная защита. Столбцы workspaceId никуда не исчезли и по-прежнему заполняются. Избыточные внутри одного файла, они сохраняют неизменными триггеры лент синхронизации, индексы FTS и существующую форму строк, а ещё делают файл самоописываемым при отдельном просмотре — именно на это опираются экспорт и импорт.
  • Доменный уровень. Сервисы, которые меняют сущности по идентификатору, перед выполнением проверяют entity.workspaceId == workspaceId в одной точке контроля и при несовпадении бросают WorkspaceMismatchException — громкий отказ для идентификатора, пришедшего из чужого контекста. Громко отказывать важно: тихий no-op скрывает баг, а продолжение работы приводит к утечке.
  • Уровень RPC. Диспетчер repo-RPC и есть точка контроля. Каждая операция, привязанная к рабочему пространству, должна нести workspace_id в собственных аргументах: никакого «текущего рабочего пространства» на сессию, которое могло бы утечь, нет, а неизвестный идентификатор отклоняется как not-found до того, как что-либо откроет базу данных, поэтому плохой идентификатор не может материализовать фантомный workspace.db. Действительно глобальные операции (лента новостей, очередь флота) объявляют себя непривязанными — это явное, осознанно принятое решение. Инструменты MCP следуют тому же правилу: любой инструмент, работающий с данными рабочего пространства, требует workspace_id.
  • Веерные обращения между пространствами поддаются перечислению. Ответ на вопрос о нескольких рабочих пространствах означает открытие нескольких баз данных. Этот веер ограничен одним вспомогательным классом, CrossWorkspaceQueries, поэтому полный список того, что законно пересекает границу, живёт в местах вызова одного файла, а не размывается по комментариям к документации. Эти места вызова: агрегация по всему серверу, реконсилеры при запуске, проходы очистки по сроку хранения и сборки мусора, резервное копирование, маршрутизаторы событий — и поиск членства, описанный ниже.
  • Одного доступа по идентификатору недостаточно. Поиск сущности по её UUID не доказывает, что она принадлежит рабочему пространству вызывающего. Разделение файлов делает неправильный файл недостижимым; проверка на доменном уровне ловит неверный идентификатор, пришедший через легитимный файл.

Доступ даёт членство, а не ключ сопряжения

Заголовок раздела «Доступ даёт членство, а не ключ сопряжения»

Наличие учётных данных устройства аутентифицирует устройство. Оно ничего не разрешает внутри рабочего пространства. Каждая операция, привязанная к рабочему пространству, определяет роль вызывающего по членству и требует минимальный уровень, выведенный из вида операции — чтение требует guest, изменение требует member, разрушительная операция требует admin, — а операции, открывающие содержимое репозитория, дополнительно проверяют права вызывающего на конкретный репозиторий. Действительный ключ сопряжения без членства получает Not a member of this workspace.

Следствие для изоляции изящно: строки членства живут в файле самого рабочего пространства, поэтому вопрос «кто входит в это рабочее пространство?» — обычное чтение в пределах области действия. Обратный вопрос — «в каких рабочих пространствах состою я?» — по определению размазан по файлам, поэтому это одно из немногих санкционированных веерных обращений CrossWorkspaceQueries, и именно его выполняет средство выбора рабочего пространства, пока ни одно из них ещё не выбрано.

О лестнице ролей и правах на уровне репозитория см. Совместная работа и присутствие.

Не всё привязано к рабочему пространству. Вторая база данных, <dataDir>/global.db, хранит то, что действительно относится ко всему серверу:

  • реестр рабочих пространств, чтобы переключатель мог перечислить все рабочие пространства, не открывая ни одного файла рабочего пространства, — и чтобы название, логотип, владелец, заданный вручную порядок и маркер мягкого удаления рабочего пространства жили вне файла, который они описывают;
  • идентичность: пользователи, их предпочтения и сопряжённые устройства, потому что один человек — это один пользователь во всех рабочих пространствах, а сопряжённое устройство переживает удаление рабочего пространства;
  • очередь флота (воркеры, задания, журнал размещения), планировщик которой на каждом такте сопоставляет всю очередь с каждым воркером;
  • лента новостей, она привязана к пользователю, а не к рабочему пространству: добавленная лента следует за вами в каждое рабочее пространство;
  • workspace_routes — индекс, работающий до аутентификации и отвечающий на вопрос «какому рабочему пространству принадлежит этот ключ?» для точек входа, которые приходят без рабочего пространства, имея лишь секрет или непрозрачный идентификатор (хеш приглашения, токен webhook, глубокую ссылку). Промах здесь — это not-found; резервного сканирования намеренно нет;
  • настройки и идентичность всей установки: идентификатор установки, настройки уровня хоста, подключения SSO и управляемые политики действий (ограничение на уровне всей установки, которое может только ужесточать ограничения рабочего пространства).

При запуске открывается только этот файл. Файлы рабочих пространств открываются лениво, при первом обращении, поэтому стоимость запуска не растёт, сколько бы истории ни накопили рабочие пространства, а quick_check для каждого файла выполняется при первом использовании, а не на пути к баннеру готовности.

Файл рабочего пространства содержит только однострочную таблицу workspace_meta для самоидентификации (его идентификатор, установка, которая его создала, версия схемы, с которой он был создан, когда). Всё описательное о рабочем пространстве — в реестре.

Создание рабочего пространства — это больше, чем вставка строки в реестр. За одну операцию сервер проставляет создающему пользователю ownerUserId, записывает его членство с ролью владельца и публикует доменное событие WorkspaceCreated. Слушатели этого события затем идемпотентно и в фоне создают:

  • агента-CEO и четырёх специалистов — qa, architect, engineer, librarian — при этом специалисты подчиняются CEO;
  • встроенные шаблоны конвейеров и их триггеры (триггеры событий и ручные триггеры поставляются включёнными; триггеры по расписанию нужно включать самому, кроме ежедневной сборки мусора worktree);
  • стартовые наборы eval.

Это происходит для каждого рабочего пространства, а не только для первого. Созданные при инициализации агенты не имеют адаптера, поэтому работают на Control Center (built-in) с моделью Anthropic по умолчанию (claude-opus-4-8, если вы не закрепите другую), пока вы не зададите адаптер и модель для каждого агента в разделе Настройки → Рабочее пространство → Агенты. См. Модель агента.

Запись членства владельца значит больше, чем кажется: без неё у только что созданного рабочего пространства не было бы участников, и каждый следующий вызов, привязанный к рабочему пространству, создатель получал бы отказ.

Удаление рабочего пространства помечает строку в реестре и отвязывает каталог. Рабочее пространство исчезает из всех списков и результатов поиска. Строка в реестре остаётся (мягко удалённой), поэтому идентификатор по-прежнему известен резервному копированию и обслуживанию, но workspace.delete сразу удаляет каталог этого рабочего пространства — базу данных, worktree, файлы агентов и всё остальное — через WorkspaceDatabaseManager.dropAndClose. Удалить его может только владелец рабочего пространства.

Поскольку одно рабочее пространство — это один файл, передать рабочее пространство кому-то — это файловая операция, а не выгрузка таблица за таблицей:

  • workspace.export — это один VACUUM INTO: согласованный дефрагментированный снимок, снятый в транзакции чтения, пока сервер продолжает обслуживать запросы.
  • workspace.import принимает такой файл, заменяя всё, что целевое рабочее пространство содержит сейчас. Встроенная таблица workspace_meta позволяет импорту отличить «свой файл, принятый заново» от «файла из другой установки» (последнее разрешено, но записывается в журнал, а не остаётся незаметным).
  • server.backupNow снимает снимок всей установки в каталог с меткой времени, повторяющий структуру живого каталога данных: manifest.json, global.db и по одному <workspaceId>/workspace.db на рабочее пространство, — поэтому восстановление сводится к копированию его обратно.
  • server.listBackups читает этот каталог обратно, от новых к старым, с размером и списком рабочих пространств каждого снимка. Снимок, у которого нет манифеста или который ссылается на отсутствующие файлы, всё равно показывается с пометкой «неполный»: скрыв его, оператор решил бы, что у него есть резервная копия, которой нет.

Операции резервного копирования живут в разделе Настройки → Сервер → Резервное копирование и восстановление: Создать снимок снимает снимок установки, список показывает, какие снимки полные, и позволяет владельцу установки удалить снимок после подтверждения, а каждое рабочее пространство предлагает экспорт и импорт. Встроенный конвейер Workspace backup берёт только базу данных этого рабочего пространства по недельному расписанию и удаляет свои копии старше заданного срока хранения. Восстановить рабочее пространство из снимка установки — это workspace.import, нацеленный на <workspaceId>/workspace.db внутри снимка; восстановление всей установки по-прежнему выполняется копированием каталога снимка обратно при остановленном сервере.

Ни одна из них не переносит остальное содержимое каталога рабочего пространства. Рабочее пространство накапливает больше, чем базу данных — вставленные изображения, файлы навыков и агентов, учётные данные чатов, worktree, — а экспорт и импорт переносят файл, поэтому всё рядом с ним остаётся на месте. Для учётных данных это сделано намеренно, для worktree это несущественно; в остальном это ограничение, о котором стоит знать, а не возможность. Полная таблица — в справочнике по резервному копированию.

Перенос резервной копии между сервером и вашим устройством

Заголовок раздела «Перенос резервной копии между сервером и вашим устройством»

Каждая операция выше говорит путями на сервере, а это исчерпывающий ответ только тогда, когда сервер — ваша собственная машина. Три подписанных HTTP-маршрута — /backup/workspace, /backup/snapshot и /backup/restore — передают байты для всех остальных топологий, по той же аутентифицированной полосе, которой уже пользуются прокси медиа и хранилище изображений. На странице это кнопка Скачать рядом с каждым экспортом и каждым снимком (вы выбираете, куда его сохранить) и Выберите файл и загрузите, чтобы восстановить из файла, лежащего на машине, за которой вы сидите. Именно загрузка работает, когда сервер — не ваша машина: workspace.import требует, чтобы файл уже был на хосте, и так он туда попадает.

Стоит знать три свойства:

  • Скачивание создаёт свежую копию, и сервер не хранит её. Скачивание — это не «забрать файл, который сделала кнопка экспорта»: он экспортирует заново, передаёт этот файл потоком и удаляет его. Ответ помечен no-store с Accept-Ranges: none по той же причине: возобновлённый диапазон склеил бы два разных экспорта в один файл, который выглядит валидным, но им не является.
  • Загрузка пишется на диск потоком, без буферизации, на обоих концах, а промежуточная копия удаляется при любом исходе — включая отказ. База данных рабочего пространства — не скриншот.
  • У соединения через ретранслятор нет HTTP-origin, поэтому элементы управления передачей там отключены с пояснением причины. Всё, что говорит путями на стороне сервера, продолжает работать через ретранслятор; прямое соединение нужно только полосам для байтов.

Можно запускать несколько рабочих пространств одновременно. Каждое работает независимо: агенты одного рабочего пространства не могут писать агентам другого, не могут читать память другого рабочего пространства и не видят его тикеты. Адресация получателя при обмене сообщениями между агентами точна и никогда не выходит за пределы рабочего пространства.

Единой панели по всем рабочим пространствам нет. Поверхность наблюдаемости по адресу /workspaces/:workspaceId/observability подписывается на ограниченный глобальный поток последних запусков и сразу сужает его до активного рабочего пространства, поэтому там вы видите одно рабочее пространство за раз. Единственные действительно многопространственные поверхности, к которым прикасается человек, — это выбор рабочего пространства и переключатель.

Ещё одно практическое ограничение, о котором стоит знать: рабочее пространство, которым кто-то пользуется, остаётся открытым — никакого LRU нет. Когда таких открытых файлов становится больше 32, сервер предупреждает, а не отказывает, потому что каждый открытый файл держит фоновый изолят и кэш страниц. Веерные обращения между пространствами открывают файлы на время и закрывают их после, если больше никто их не занял.