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

Совместная работа — идентичность, членство и присутствие

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

Субъекты: пользователи и агенты — равноправные действующие лица

Заголовок раздела «Субъекты: пользователи и агенты — равноправные действующие лица»

Всё, что записывает «кто это сделал», — сообщения, тикеты, ревью, планы, журналы запусков, записи аудита — разрешается через субъекта: запечатанное объединение UserPrincipal и AgentPrincipal с компактным представлением в протоколе (user:<id> / agent:<id>). Атрибуция никогда не сводит N людей (или N агентов) к одному служебному аккаунту, поэтому ревью, оставленное агентом, и ревью, оставленное человеком, различимы везде, где они появляются.

Поскольку субъект — единица идентичности, он же и единица ограничений:

  • Ограничения частоты — на каждый субъект, а не на сессию. Защита от флуда на удалённых вызовах инструментов считает бюджет по пользователю, а не по соединению: все устройства участника берут из одного общего скользящего окна (120 вызовов в минуту, из которых не более 30 могут изменять данные), поэтому открытие трёх устройств не утраивает ничей лимит, а один шумный участник не может оставить остальных без квоты.
  • Отмена действий — на каждый субъект. Журнал действий каждого клиента фиксирует только те изменения, которые выполнил его субъект. Вы можете нажать ⌘Z и отменить собственное изменение тикета, но не изменение коллеги: к моменту, когда вы его увидите, долговечный канал уже разрешил его (см. ниже).

Членство — это граница доступа, а не ключ привязки

Заголовок раздела «Членство — это граница доступа, а не ключ привязки»

Два разных учётных данных отвечают на два разных вопроса. Ключ привязки (см. Удалённое управление и мобильный клиент) аутентифицирует устройство на сервере. Членство даёт пользователю этого устройства право действовать внутри рабочего пространства. Привязанное устройство, пользователь которого не состоит ни в одном рабочем пространстве, не может ни читать его, ни изменять.

Членство — это одна строка на пару (рабочее пространство, пользователь), хранимая в собственном файле базы данных этого рабочего пространства: поэтому вопрос «кто входит в это рабочее пространство?» — обычное чтение с ограничением области, тогда как обратный вопрос «в каких рабочих пространствах состою я?» неизбежно охватывает несколько файлов и остаётся одним из немногих разрешённых веерных обращений между рабочими пространствами. Именно его выполняет средство выбора рабочего пространства, пока ни одно рабочее пространство ещё не выбрано.

Каждая строка несёт роль из пятиуровневой иерархии, упорядоченной по рангу:

Роль Что разрешено
owner Полный контроль, включая удаление рабочего пространства; ровно один на рабочее пространство
admin Управление участниками, приглашениями, настройками, разрушительными операциями
member Повседневное чтение и запись: сообщения, тикеты, запуски агентов
viewer Только чтение по всему рабочему пространству; glob-шаблоны исключения секретов скрывают подходящие пути на поверхностях с кодом
guest Тот же минимум «только чтение», что у наблюдателя, но на самой низкой ступени иерархии

Любое решение о доступе сводится к вопросу «не ниже ли роль вызывающего, чем X». Минимальный уровень выводится из типа операции, если она не задаёт его явно: для чтения нужен guest, для изменения — member, для разрушительной операции — admin. Пообъектные ACL сверх прав на репозитории, описанных ниже, сознательно не рассматриваются.

Приглашения — так выдаётся членство, из раздела Настройки → Рабочее пространство → Участники. Администратор выпускает одноразовый код (в виде ссылки или QR-кода), срок действия которого по умолчанию истекает через семь дней; сервер хранит только SHA-256-хеш кода, а сам код показывается ровно один раз. Его активация JIT-создаёт пользователя, записывает членство с ролью из приглашения (никогда не owner — право владения передаётся явно и никогда не выдаётся приглашением) и передаёт управление привязке устройства. Учётные данные устройства, выпущенные после привязки, сами ограничены сроком в 30 дней, так что привязка — не бессрочный ключ.

Права на репозитории лежат ниже членства. Поскольку сервер хранит полные копии репозиториев, одно лишь членство в рабочем пространстве не должно давать больше прав, чем даёт хост кода: каждая поверхность с кодом — файлы, диффы, ревью PR, поиск по коду — проверяет права участника именно на этот репозиторий. Права задаются на тройку (рабочее пространство, пользователь, репозиторий) и упорядочены как none → read → review → write, где write означает запуск агентов, которые пишут в репозиторий. Отсутствие строки означает none; владельцы и администраторы неявно обладают write на каждом подключённом репозитории и не имеют строк вовсе.

Control Center делит общее состояние на два канала с противоположными правилами долговечности, потому что два вида состояния отказывают по-разному.

Присутствие — кто в сети, где он находится, что печатает, чем прямо сейчас занимается агент — теряет всякую ценность в тот самый момент, когда устаревает. Если бы его сохраняли, при каждом переподключении приходилось бы сверять «в базе данных сказано, что Сэм редактирует этот тикет» с реальностью. Поэтому присутствие никогда не записывается в базу данных. Оно живёт в концентраторе присутствия в памяти сервера: клиенты публикуют через presence.update, другие участники подписываются через presence.watch, а записи автоматически истекают после трёх пропущенных сигналов активности (сигналы активности отправляются каждые 10 секунд; концентратор проводит очистку каждые 5). Сверять нечего — можно только дать им устареть.

Два свойства делают этот канал надёжным:

  • Идентичность берётся из сессии, а не из аргументов клиента. Сервер сопоставляет аутентифицированное устройство соединения с идентификатором пользователя; клиент может обновлять только собственное присутствие, поэтому никто не может подделать статус или местоположение другого участника. Явное обновление offline (отключение или переключатель невидимости) немедленно удаляет запись.
  • Состав формируется на сервере. Участники никогда не обмениваются присутствием напрямую, поэтому все видят один и тот же состав независимо от топологии. У агентов нет клиента, поэтому сервер синтезирует их записи из текущего состояния запусков — думает, выполняется, заблокирован на воротах одобрения, завершён, — причём состояние и местоположение агента определяет один представительный запуск, чтобы параллельные запуски не заставляли состав метаться.

Веерная рассылка разбита на уровни по типу потребителя: десктоп и веб получают полный уровень (не более одной выдачи состава за 100 мс), телефон — сводный уровень (раз в 500 мс), так что обновления с частотой движения курсора не могут перегреть PWA.

Местоположение участника — где он сейчас находится, вплоть до артефакта — это запечатанное объединение: Space, File (репозиторий + путь + необязательная строка), Pr, Ticket, PlanNode. Не у каждого местоположения есть куда перейти: маршрута для просмотра файлов нет, поэтому местоположение File отображается как метка, а не как пункт назначения. Канал также несёт мягкие заявки («Сэм редактирует этот тикет», «агент занимает этот worktree»): это видимость конфликтов, а не блокировки. Неизвестные виды местоположения разбираются в null, поэтому старый клиент просто не отображает то, чего не понимает.

Записи, которые должны уцелеть, — тикеты, общий документ «Заметки» пространства, сообщения — идут противоположным путём: они авторитетны, упорядочены и сохраняются на диск. Параллельные правки разрешаются по принципу побеждает последний записавший в порядке получения сервером, а не по часам клиентов. Разрешение сознательно мелкозернистое: правки тикетов ведутся по столбцам, поэтому два человека, редактирующие разные поля одного тикета, оба сохраняются, и никто никого не затирает (переходы рабочего процесса вместо этого сохраняют оптимистичную блокировку); документ «Заметки» пространства — один столбец, поэтому весь документ работает по LWW, а мягкие заявки в канале присутствия делают параллельное редактирование видимым, а не заблокированным. Это сознательно не CRDT: бремя доказательства лежит на CRDT, а LWW по порядку получения плюс видимые заявки покрывают реальные сценарии совместной работы.

«Последний записавший» означает порядок получения сервером, а не часы клиента: ноутбук с убежавшими часами не может выиграть правку, которую он проиграл. За долговечным каналом стоит поток синхронизации: триггеры SQLite дописывают каждое изменение в журнал изменений рабочего пространства внутри записывающей транзакции, выделяя монотонный порядковый номер рабочего пространства в той же транзакции, что и само изменение. Серверная служба отслеживает этот журнал и формирует из него версионированные дельта-кадры в порядке номеров. Клиенты переходят от полных снимков к дельтам; разрыв в последовательности возвращает соответствующее хранилище в режим снимков, а не приводит к ошибочному применению кадров.

Следить, направлять, перехватывать и возвращать управление

Заголовок раздела «Следить, направлять, перехватывать и возвращать управление»

Канал присутствия — это не просто состав: он приводит в действие глаголы совместной работы:

  • Режим слежения. Щёлкните по участнику, и ваш клиент последует за его местоположением, переходя туда же, куда переходит он. Слежение действует «до вашего первого действия»: как только вы переходите куда-то сами, слежение отключается. Закрываемый баннер сообщает, когда другой участник проводит показ пространства, в котором вы находитесь.
  • Направление. Сообщение выполняющемуся агенту ставится в очередь как управляющее; агент перечитывает его перед возобновлением следующего хода, так что можно подтолкнуть запуск на ходу, не убивая его.
  • Перехват. Принятие управления разговором ставит на паузу каждый активный запуск на его следующей чистой границе хода (запуск Claude Code, у которого нет безопасной границы, останавливается), сохраняя снимок git для каждого хода, так что перехват обратим. Долговечный маркер блокирует новые отправки в пространство и переживает перезапуск сервера: сервер никогда не возобновляет работу автоматически в незавершённую правку человека. Один активный редактор на worktree — это мягкая заявка: второй перехват виден и отклоняется, а не тихо сливается.
  • Возврат управления. Возвращая управление, вы публикуете в пространство структурированную сводку диффа worktree, ставите её в очередь как управляющее сообщение для приостановленных запусков, возобновляете их и снимаете маркер.

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

Уровень Значение в протоколе Влияние на вызов инструмента, требующий одобрения
Только предлагать proposeOnly Отклоняется сразу, с причиной, которую агенту велено передать: он может описать действие, но не выполнить его
Действовать с одобрения actWithApproval Ворота одобрения с отказом по умолчанию: одно подтверждение показывается подключённому оператору, а без одобряющего — отказ
Действовать свободно actFreely Одобрено заранее, кроме случаев, когда применяется жёсткое правило deny из ограничений — правило deny всегда побеждает
(не задано) null То же, что «действовать с одобрения»: значение по умолчанию на сервере

Две вещи остаются вне регулятора в обе стороны. Собственный выходной глагол режима закреплён и не может быть отклонён, что бы ни говорил регулятор: режим «Оркестрация» когда-то структурно не мог выдать свой единственный результат, потому что предустановка отклоняла класс эффектов, который объявляет этот глагол. А жёсткий deny из ограничений действий нельзя переопределить с помощью actFreely.

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