План работ: Оргструктура и модель доступа
Source spec: docs/org-structure.md (расширенная версия, 55 разделов / 8 фаз).
Статус: Волны A–G завершены (A — UI организации и доступности; B — явное членство в проектах + аудит доступа; C — роли на узлах, авторизация ранов, execution identity, snapshot контекста; D — триггеры на узлах и запуск под identity; E — human_request как сущность, резолюция получателя, timeout-политики; F — org-политики с рестриктивным merge и enforcement; G — отчёт расхождения, удаление legacy-колонок, документация модели доступа). Отложенные хвосты: UI ролей на узлах и execution identities, внешние пользователи, provider credentials. Порядок волн согласован с фазами спеки; UI организации был вынесен вперёд (волна A), потому что бэкенд фаз 1–2 уже существовал и без UI неюзабелен.
Размеры задач: S / M / L — относительно одной волны работы.
#0. Текущее состояние (baseline после 0e1ec70)
Готово (Фаза 1 спеки + часть Фазы 2):
- миграция 000034:
org_unit(дерево, materialized path),org_unit_idна agent/skill/mcp-server/provider/user,workspace_project_org_unit(со стороны проекта — CASCADE, со стороны узла — RESTRICT за проверкой «пустого узла»); - стор
workspace/org.go: CRUD дерева, move с пересчётом путей поддерева, запрет циклов, удаление только пустых узлов, привязка ресурсов, инспекция узла; Principal.VisibleUnits(цепочка предков) + фильтрация списков и одиночных GET: агенты, навыки, MCP, провайдеры, проекты; admin и kernel-internal токены — без фильтра;- переходное правило проектов:
org-видимость AND legacy allowed_users; - API:
/v1/org/unitsCRUD (мутации — admin),/v1/org/units/{id}/resources,PUT /v1/org/resources/{kind}/{id}/binding; - обычные PUT ресурсов не сбрасывают привязку (сменить скоуп может только admin через binding); whoami отдаёт
org_unit_id.
Уже существовало в системе до оргструктуры (релевантно фазам 4–5):
- каналы пользователей + preferred channel (
workspace_user.channels,PUT /users/{id}/channels), notify-адаптеры; web-канал — inbox в UI; ask_human/request_approval: приостановка Run (waiting), структурированные опции, timeout, события траектории (human.requestedи т.д.), возобновление Run;- триггеры
workspace_trigger(project-scoped: schedule/webhook/event), инструменты агентаcreate/update/delete_trigger; - Run фиксирует версию агента (миграция 000031).
Известные пробелы против расширенной спеки:
- видимость ≠ право использования:
POST /v1/agent/runsпроверяет роль+проект, но не доступ к агенту/скиллам/MCP/провайдеру (§22); - нет
workspace_project_member(explicit membership, §16), нет ролей на узлах (§13), нет execution identity (§20), нет аудита (§36), у триггеров нетorg_unit_id(§18); - получатель
ask_human— конкретныйuser:xxx, нет резолюции role/org_unit/project_owner (§26).
#1. Карта фаз спеки → волны плана
| Фаза спеки | Содержание | Состояние | Волна |
|---|---|---|---|
| 1 Org Model | дерево, path, CRUD, привязки, visibility-тесты | ✅ готово | — |
| 2 Project Model | project↔units, members, project roles, миграция | ✅ готово (units + members + аудит) | B |
| 3 Authorization | visibility/use/manage/administer, проверки на операциях, execution identity | ✅ готово (provider credentials отложены до secret store) | C |
| 4 Triggers | scope триггеров, авторизация, запуск под identity, аудит | ✅ готово (org_unit_id + авторизация + re-auth + запуск под identity + аудит + UI) | D |
| 5 HITL | human_request, recipient resolution, каналы, timeout-политики | ✅ готово (сущность + резолюция + политики + аудит + UI-инбокс) | E |
| 6 Policies | наследуемые политики выполнения | ✅ готово (сущность + рестриктивный merge + enforcement в workflow и kernel + UI) | F |
| 7 UI | Org Tree, доступность, membership, identity, политики | Org Tree/доступность/membership/политики готово; identity — select в форме триггера, полный UI отложен | A + B + F (готово); хвосты — ниже |
| 8 Migration & Cleanup | расхождение моделей, удаление legacy | ✅ готово (отчёт scripts/access-divergence.sql, миграция 000043, runbook §11) |
G |
#2. Волны работ
#Волна A. UI организации и доступности — M — фаза 7 (частично)
-
Org.tsx: дерево со сворачиванием; инспекция узла (ресурсы по видам, пользователи, проекты); создание/переименование; перемещение выбором родителя с подтверждением («меняет видимость»); удаление пустого узла через наш confirm-диалог. -
Layout.tsx: раздел «Организация» (admin; остальным по ролям на узлах — после волны C). - Поле «Доступность» (селект узла, дефолт «Вся организация»; для admin — эффективная область «Development └── 14 дочерних») в формах Agents/Skills/Mcp/Providers.
- Проект: мультивыбор узлов («Организационные области»); участники — в волне B; legacy
allowed_usersскрыт и заморожен на чтение. - Пользователь: выбор основного подразделения вместо списка проектов.
- Карточки списков: бейдж узла / «глобально».
- Мастер корня: если
org_unitпуст — создание организации (онбординг/страница «Организация»). -
workspaceApi.ts:orgApi; i18n ru/en; обе темы;ListFilter.
#Волна B. Project Model — M — фаза 2
-
workspace_project_member (project_id, user_id, role, created_at, created_by, PK(project_id,user_id))— миграция 000035 (в ней жеaccess_audit_log). - Доступ к проекту =
org-unit access OR explicit membership(заменяет переходное AND-правило;allowed_usersзаморожен на чтение и больше не учитывается; PUT без поля сохраняет колонку как есть). - API:
GET/POST /v1/workspace/projects/{id}/members,DELETE /v1/workspace/projects/{id}/members/{userID};DELETE /v1/org/resources/{kind}/{id}/binding(алиас PUT с "");GET /v1/org/audit?limit=. Отдельные…/org-units/{unit_id}эндпоинты не делались — replace-семантика черезPUT /v1/workspace/projects/{id}сorg_unitsпокрывает сценарий. - Аудит-минимум: события
project.member_added/removed,resource.bound/unbound,org_unit.created/updated/moved/deleted(§36). - UI: участники проекта в диалоге проекта (мультивыбор пользователей с ролью, синхронизация diff-ом при сохранении).
- Внешние пользователи MVP:
user_typeиexpires_at— опционально, отложено до потребности (сама модель explicit membership готова).
#Волна C. Authorization — L — фаза 3
- Модель эффективной роли в controlplane:
Principal.OrgRoles(гранты с self-path узла) +MaxRoleAt(unitID, unitPath)— максимум из роли установки, membership-роли и грантов на сам узел и его предков; глобальные ресурсы — только роль установки. Unit-тесты на дерево (грант на предка/сам узел/сиблинга/потомка/глобальный ресурс). - Роли на узлах: миграция 000036
org_unit_role; сторListOrgUnitRoles/SetOrgUnitRole/RemoveOrgUnitRole/ListAllOrgUnitRoleGrants; APIGET/PUT/DELETE /v1/org/units/{id}/roles/{userID}(мутации — admin, ParseRole-валидация); аудитrole.granted/revoked;refreshUserTokensподхватывает гранты без рестарта; роли в инспекции узла (join имён); узел с грантами не удаляется. - Project scope из орг-модели:
VisibleProjectIDs— для назначенных в узел не-админов токен получает конкретный список проектов (org-пересечение ∪ membership) вместо legacyuser.projects; admin и непривязанные —*(переходный режим).refreshUserTokensвызывается после мутаций проектов/участников/грантов. - Проверки на операциях (§22):
authorizeRunUseвPOST /v1/agent/runsиPOST /v1/workspace/tasks/{id}/runs(гейт ослаблен до reader, авторизация — 403 с точной причиной): видимость проекта (org ∪ membership) + эффективная роль ≥ writer; агент — OrgVisible + writer на его узле; скиллы/MCP рана — OrgVisible; «висячие» ссылки пропускаются молча, как в рантайме. PUT агента проверяет OrgVisible скиллов/MCP (иначе 403). - Snapshot execution context (§34–35): миграция 000037
workspace_run.exec_context; заполняется при старте рана (tasks/trigger/schedule): agent_id/agent_version/skill_ids/mcp_ids/model/project_id/actor/execution_identity_id/authorized_by{user_id, role(эффективная), org_unit_id}; изменение оргструктуры не затрагивает живые раны; читается всеми рановыми запросами. - Execution Identity — сущность для автоматических запусков (§20): миграция 000038; CRUD
GET/POST/PUT/DELETE /v1/workspace/execution-identities+ аудитexecution_identity.created/updated/deleted; отсутствующие allowed-списки дефолтятся к["*"](не «ничего»); wiring в триггеры — волна D, UI — фаза 7. - Аудит
user.org_unit_changed(from/to) при смене узла пользователя. - Provider credentials (§23) — отложено:
api_key_ref— ссылка на env/secret, не значение; ужесточение после появления реального хранилища секретов. CRUD ресурсов остаётся на installation-role, org-гранты пока дают запуск ранов. - UI ролей на узлах и execution identities — фаза 7 (спека §42, §47): секция «Роли на узле» в инспекторе Org (гранты reader/writer/operator/admin, бейдж «admin (installation)», revoke-диалог) и секция «Идентификаторы исполнения» с CRUD-диалогом (allowed-списки, орг-узел); ListFilter, i18n ru/en.
#Волна D. Triggers — M — фаза 4
-
org_unit_idу триггеров — миграция 000039 (+execution_identity_id, FK RESTRICT, индекс); видимость в списках: скрытые триггеры отвечают 404 на GET/PUT/DELETE, list фильтруется по OrgVisible. - Авторизация триггера (§21):
authorizeTriggerUseпри создании (право на unit/проект/агента/identity — 403/404 с точной причиной) + re-auth при PUT;org_unit_idв PUT игнорируется (перепривязка — админ-эндпоинт binding). - Запуск Run под execution identity:
authorizeTriggerRunпри каждом fire (webhook и scheduler) — identity существует, IdentityAllows (агент/проект/MCP/провайдер), OrgVisible агента и MCP в UnitChain(identity.OrgUnitID); отказ —trigger.rejected+ 403 без создания задачи/run;exec_context.execution_identity_idв снапшоте. Merge-семантика PUT:{enabled}не затирает name/agent_id/config/identity (баг UI-тоггла). - Аудит
trigger.created/updated/deleted(details: name/agent/enabled/identity/unit). - UI триггера (§46): выбор unit (OrgUnitSelect, edit — только админ), выбор execution identity с подсказкой, бейджи unit/identity на карточках; policy/human-правила — волны E/F.
#Волна E. HITL — M — фаза 5
-
human_requestкак отдельная сущность (§28) — миграция 000040, сторworkspace/humanrequest.go; статусы pending/delivered/answered/expired/cancelled/rejected (waiting слит в delivered); список —GET /v1/workspace/human-requestsc lazy-экспирацией, UI-инбокс: бейдж в Layout + метка «спрашивает вас» на запусках в Runs (события — fallback для старых ядер). - Recipient resolution (§26):
user:/ имя /role:/org:/project_owner(первый активный admin — owner-колонки у проекта нет) / пусто → actor; проверка права обращения (§30) — fail-closed поhuman_targetsexecution identity (пустой список/*= все), отказ → событиеhuman.rejectedбез паузы. - Адаптеры Matrix/Telegram + fallback по preferred channel — уже были в
kernel/agent/notify.go; волна E добавила туда резолюцию получателя и запись сущности. - Timeout-политики (§33): fail/retry/fallback/escalate/cancel — в модели выполнения (workflow), не в транспорте; retry/escalate — ровно один доп. раунд (escalate →
project_owner); cancel останавливает run безrun.completed. - Аудит HITL (§37):
access_audit_log(human_request.created/delivered/answered/cancelled/expired/rejected) на всех путях — activity, approval-endpoint, lazy sweep.
#Волна F. Policies — M/L — фаза 6
- Сущность Policy (§24) — миграция 000041 (
org_policy, JSONB-списки, CHECK на режимах, идемпотентна), сторworkspace/policy.go: CRUD +EffectivePolicy(unitIDs...)— каждый юнит разворачивается в цепочку предков, глобальные строки всегда применяются, merge рестриктивный (allowlist'ы пересекаются, капы — min, deny/read_only/tools выигрывают, голый*не сужает). - Минимум ограничений: allowed_models (403 на старте рана; пустая модель резолвится в дефолт
TEMPORALITY_MODEL_ID), allowed_mcp (неразрешённые серверы выкидываются из рана), max_tokens/max_budget_usd (USD-бюджет деградирует в токен-кап без цены вKERNEL_MODEL_PRICES), timeout_seconds, network deny, sandbox read_only, approval tools. Enforcement: workflow (policy.limit, forced finale со статусамиtime_limit/token_limit/budget_limit, approval для не-безопасных тулов) + 4 точки применения в kernel (run-config сactor_idдля делегированных, POST /runs, webhook, scheduler — срабатывание политики = rejection как §21). Аудитpolicy.created/updated/deleted. - UI политик + визуализация эффективного доступа: секция Policies в Org (список с бейджем узла и тегами ограничений, create/edit/delete, фильтр), панель «Действующая политика» в инспекторе узла (merge + источники); i18n ru/en; новые статусы ранов в Workspace.
#Волна G. Migration & Cleanup — S — фаза 8
- Отчёт расхождения old/new модели на стенде (этап 4 спеки §48):
scripts/access_divergence.sh+scripts/access-divergence.sql(read-only; A — пользователи, B — проекты, C — сводка; проверен на стенде и на синтетических расхождениях в rollback-транзакции). - Удаление колонок
allowed_users/workspace_user.projectsмиграцией 000043. Чтение для решений доступа отключено волнами B/C; вычищена круглота через CRUD (поля убраны из структур/хендлеров/ответов и UI-форм), колонка больше не упоминается в SQL стора. - Документация модели доступа:
docs/runbook.md§11 (видимость, перепривязка, чтение отчёта, что удалено).
#3. Тестирование и приёмка (по §51 спеки)
- Иерархия: ресурс A виден A/B/C; ресурс C виден только C.
- Глобальный ресурс (
NULL) виден всем. - Кросс-функциональный проект: пользователи всех связанных узлов видят; несвязанные — нет.
- Explicit membership: доступ вне орг-области после добавления.
- Роли: installation admin / org-unit admin / project writer / project reader / обычный пользователь.
- Trigger: разрешённый триггер создаётся; с недоступным агентом — нет; права создателя не наследуются; identity имеет собственные права.
- Run: недоступные агент/MCP/провайдер отклоняются на POST /runs и внутри выполнения.
- HITL: полный цикл ask_human → delivery → waiting → answer → resume.
- Изменение узла пользователя: новый Run по новым правам; живой Run не исчезает; критическое действие перепроверяется.
#4. Принятые решения
- Переходная видимость = org AND legacy (до волны B): обе модели должны пропустить; без настройки структуры поведение не меняется. В волне B заменяется на
org OR explicit membership, legacy замораживается. - Пересчёт
path— в приложении, одной транзакциейMoveOrgUnit, без PL/pgSQL-триггеров. - Мутации дерева и привязок — только admin (изменение видимости для всех); writer создаёт ресурсы с доступностью «вся организация».
VisibleUnits = nilу служебных токенов — рантайм не зависит от прав пользователя; privileged API не используется для обхода авторизаций пользовательского API (§41).- Только аддитивная модель (§7): без deny/override/исключений поддеревьев — зафиксировано как ограничение первой версии.
- Видимость ≠ полномочия (§10): фильтрация списков — это visibility; use/manage/administer появляются в волне C на уровне операций.
- Trigger ≠ пользователь (§19): автоматические Run — под execution identity с собственными правами.
- Org Unit у пользователя один (primary, §11–12): таблица дополнительных memberships — будущее расширение без переделки модели.
#5. Не-цели (первой версии, §54 спеки)
- Deny rules, сложные ACL, произвольные permission sets.
- Матричная оргструктура (несколько узлов пользователя).
- HR sync / SCIM / федерация организаций.
- Выбор получателя LLM-ом, workflow согласований.
- ltree вместо materialized path.