# NDC Module Foundry — базовый MCP-контур

## Назначение

`NDC Module Foundry` — модульный контур поверх канонического `NODE.DC Design Guideline`. Он создаёт и редактирует **экземпляры** готовых страниц в `Applications`; сам `Page Library` и его шаблоны остаются неизменяемым каноном.

MCP не вводит новую оркестрацию. Доступ выдаётся уже существующему AI Workspace Assistant через его generic entitlement adapter. Поэтому ассистент может продолжать работать с Engine, Ops, Ontology и другими подключёнными модулями по действующим правилам платформы, а Foundry определяет только границы действий **внутри Foundry**.

## Границы v0.1

| Разрешено | Не разрешено |
| --- | --- |
| Просматривать Page Library и экземпляры Applications | Менять канонический Page Library или дизайн-компоненты |
| Создавать application instance | Удалять application instance через MCP |
| Изменять название, slug и описание instance | Создавать свободный canvas или произвольный React-интерфейс |
| Добавлять повторные instances зарегистрированной страницы | Вызывать Engine, provider API или произвольный внешний endpoint из Foundry MCP |
| Создавать/обновлять provider-neutral map pin bindings | Хранить provider tokens, transport payload или credentials в manifest |
| Создавать/обновлять versioned provider-neutral `map.style_profile` | Хардкодить provider-specific statuses, цвета или размеры в renderer |
| Задавать text label/order binding и сохранять полный Map view/window layout по `bindingId` | Использовать редактируемое имя как machine identity или превращать empty selection в all |
| Связывать versioned data product с approved Map entity-stream slot | Хранить provider ID, tenant/connection, endpoint или credential в data binding |
| Управлять server-owned consumer только для persisted approved binding | Передавать reader capability, EDP URL или raw provider payload через MCP |

Каждая запись требует `idempotencyKey`. Операция сохраняется в persistent runtime volume и повторный вызов с тем же ключом и тем же входом вернёт исходный результат без дублирования. Повтор ключа с иным входом завершается конфликтом.

## MCP endpoint

`POST /api/mcp`

Поддерживается MCP protocol `2025-06-18`. Endpoint принимает JSON-RPC `initialize`, `tools/list`, `tools/call` и `ping`.

Обязательные HTTP-заголовки каждого вызова:

```http
Authorization: Bearer <Foundry capability or Foundry Agent credential>
MCP-Protocol-Version: 2025-06-18
```

Endpoint поддерживает два существующих контура входа, не смешивая их:

- AI Workspace получает короткоживущую Foundry capability после штатной
  entitlement-проверки. Она подписана существующим внутренним service
  credential, привязана к `actorId` и `ownerKey`, живёт не более 10 минут и не
  раскрывает platform credential worker-у;
- внешний Codex получает отдельный durable Foundry Agent credential через
  настройки текущего пользователя Foundry. Credential действует до явного
  `revoke` и не является AI Workspace capability.

Заголовки с actor/owner от клиента не принимаются: пользовательский контекст
извлекается только из проверенной capability или server-side записи Agent.

Доступные инструменты:

- `foundry_status`
- `foundry_list_applications`
- `foundry_get_application`
- `foundry_create_application`
- `foundry_update_application_metadata`
- `foundry_add_page_instance`
- `foundry_upsert_map_pin_binding`
- `foundry_remove_map_pin_binding`
- `foundry_upsert_map_presentation_profile`
- `foundry_update_map_page_settings`
- `foundry_save_map_page_view_state`
- `foundry_upsert_map_data_product_binding`
- `foundry_plan_map_data_product_consumer`
- `foundry_apply_map_data_product_consumer`
- `foundry_get_map_data_product_consumer_status`
- `foundry_accept_map_data_product_consumer`
- `foundry_rollback_map_data_product_consumer`

`foundry_upsert_map_pin_binding` хранит только визуальную, provider-neutral привязку `elevated-spike`: стабильный id, subject, координаты, semantic status и ссылку на источник сущности. Поток живых данных не передаётся в MCP по одной позиции.

`foundry_remove_map_pin_binding` удаляет одну устаревшую визуальную привязку по
стабильному `bindingId`. Операция не затрагивает data-product consumer, его
последний безопасный snapshot или канонический шаблон `Page Library`.

`foundry_upsert_map_presentation_profile` управляет отдельным версионированным
`map.style_profile` страницы. Профиль содержит renderer-neutral геометрию
таргета, label contract, semantic styles, правила классов, facet filters,
counters и sort order. Условия классов могут ссылаться только на объявленные
provider-neutral facet fields. Data Product binding выбирает профиль через
`presentationProfileId`; binding также может задавать пользовательский
`displayName` и порядок строки в dropdown `Объекты`. Provider raw status,
endpoint и credential в профиль не допускаются. Полная machine-readable JSON Schema профиля публикуется прямо
в `tools/list`: label использует `mode`, `fields`, плашку, `fontWeight` и
`sizePx`, а target,
facets, styles, classes и sort имеют закрытые наборы полей. Поэтому новый MCP
клиент может создать профиль для другого spatial domain без чтения исходников
Foundry и без неописанного generic JSON.

`foundry_update_map_page_settings` меняет подложку, terrain, здания, атмосферу,
освещение и сетку только у указанного экземпляра Map внутри `Application`.
Операция не пишет в `Page Library`, принимает закрытый typed patch и поэтому
может безопасно воспроизводить визуальное окружение через MCP.

`foundry_save_map_page_view_state` является MCP-эквивалентом одной Application
кнопки `Сохранить`. Операция атомарно сохраняет camera/map height, typed patch
base settings, точный список раскрытых секций Inspector, exact
visibility/facet selections и все canonical binding windows (`open`, `rect`,
`maximized`, `zIndex`). Состояние keyed только по
`bindingId`: editable `displayName` не участвует в identity. Missing facet не
ограничивает выборку, явно пустой список остаётся empty после reopen и не
мигрирует обратно в `Все`.

`foundry_upsert_map_data_product_binding` сохраняет только декларацию
`data product → Map entity-stream slot`: versioned data product id, semantic
types, допустимую field projection и необязательную ссылку на существующий
presentation profile. Endpoint, provider, tenant, connection,
token, credential и raw payload валидатор отклоняет. Page runtime получает
scoped snapshot/patch поток через Platform, но не через Foundry MCP.

Пять consumer-инструментов управляют тем же runtime, который обслуживает Map
Page. `plan` проверяет persisted binding, Data Product version/delivery,
versioned policy и безопасно показывает `readerGrantAction=ensure|reuse`.
Если grant отсутствует, EDP сам разрешает единственный active writer scope по
Data Product id; provider/tenant/connection в Foundry не возвращаются. `apply`
принимает exact `planId`, подписанно передаёт EDP только SHA-256 digest нового
target-scoped token, коммитит snapshot и включает consumer; `status` возвращает только safe cursor,
счётчики subject/status/reconnect и количество viewers/upstream streams;
`accept` берёт bounded diagnostic lease; `rollback` останавливает consumer, но
сохраняет последний safe snapshot. Capability value, grant path, internal URL и
fact attributes в MCP diagnostics не возвращаются.

Reader grant версионируется поколениями. При замене Data Product Foundry не
перезаписывает immutable request generation и не ослабляет EDP conflict fence:
он выпускает отдельный successor token/generation, проверяет им новый catalog и
коммитит новый snapshot. Только после успешного snapshot predecessor generation
отзывается exact revoke. Если bootstrap не удался, persisted predecessor
snapshot и его grant остаются рабочими. Если временно не удался только revoke,
следующий exact plan получает action `finalize-reader-grant-rotation` и завершает
отзыв без повторного bootstrap.

## Runtime data-product boundary

Для Map Page Foundry предоставляет только same-origin runtime routes:

```text
GET /api/applications/:applicationId/pages/:pageId/data-bindings/:bindingId/snapshot
GET /api/applications/:applicationId/pages/:pageId/data-bindings/:bindingId/history?from=:iso&to=:iso&resolutionMs=:ms&sourceIds=:ids&limit=:n&cursor=:opaque
GET /api/applications/:applicationId/pages/:pageId/data-bindings/:bindingId/stream?after=:cursor
```

Маршрут разрешает persisted binding, а затем server-owned consumer находит
opaque EDP reader grant по `sha256(applicationId/pageId/bindingId)` и persisted
active generation. Generation 1 сохраняет совместимый legacy filename, а
successor capabilities хранятся отдельными root-owned immutable files.
Нормальный grant создаётся внутри persistent private runtime Foundry; runner монтирует
отдельный Ed25519 private key только для подписи digest-only provisioner
request, а EDP получает только public trust. Старый root-owned read-only grant
directory остаётся fallback для уже выданных grant. Сам token передаётся
только как `Authorization` во внутренний External Data Plane и никогда не
попадает в browser, manifest, MCP или лог. В browser отдаётся только safe Foundry projection канонического data-product envelope:
snapshot, bounded `nodedc.data-product.history/v1`, safe
`nodedc.data-product.patch/v1` upserts и cursor. History query проходит через
тот же exact binding/read grant и не создаёт L2 execution на каждого viewer.
`Last-Event-ID`
авторитетнее старого `after` при автоматическом SSE reconnect.

Consumer state хранится отдельно от application manifest в persistent runtime
volume. Snapshot atomically заменяет subject projection, включая явный empty
snapshot. Patch применяется только от exact previous cursor; новый cursor и
semantic state сначала сохраняются, затем fan-out отправляется browsers.
Повторный cursor идемпотентно игнорируется, gap вызывает snapshot rebase.
Несколько viewers одного binding делят один upstream EDP stream; закрытие
последнего viewer закрывает subscription, но не влияет на независимый producer
в Engine L2.

Freshness и remove принадлежат versioned provider-neutral policy из
`registry/data-product-consumer-policies.json`. Stale вычисляется по
`observedAt`; terminal statuses сохраняются. Временная transport/credential
ошибка не удаляет subject. Удаление допустимо только при отсутствии в
авторитетном snapshot rebase или по canonical `tombstone`/`revoked` operation.
Renderer получает стабильный `sourceId + semanticType`, persisted coordinates
и Foundry-owned `presentationStatus`. Операционные visual classes, фильтры,
счётчики и сортировка разрешаются отдельно из orthogonal state facets и
page-owned `map.style_profile`; provider identity на стиль и lifecycle не
влияет.

## Внешний Codex: Foundry + отдельная Ontology MCP

В профиле Foundry раздел `Настройки → Codex Agent API` создаёт Agent текущего
пользователя и выдаёт одноразовую setup-команду. Команда устанавливает в Codex
две независимые MCP-конфигурации:

```text
nodedc_module_foundry → POST /api/mcp
nodedc_ontology       → POST /api/ontology-mcp
```

Это одна операция установки, но не одна MCP. Foundry credential принимается
только `/api/mcp`, Ontology credential — только `/api/ontology-mcp`; их
перекрёстное использование отклоняется. Ontology route после своей проверки
проксирует MCP JSON-RPC во внутренний `Ontology Core /mcp` с уже существующим
server-only `NODEDC_INTERNAL_ACCESS_TOKEN`. Foundry не импортирует ontology
tools в свой список и не становится маршрутизатором платформенных MCP.

Agent credentials не имеют календарного срока жизни: они валидны до явного
отзыва Agent. Setup code одноразовый и живёт 15 минут. Raw credentials
возвращаются только в момент redeem; в Foundry runtime сохраняются только
SHA-256 digests, device metadata и bounded audit. Один `revoke` закрывает обе
credentials конкретного Agent.

Первый срез намеренно ограничен текущим аутентифицированным пользователем:
Codex может создавать и настраивать его Applications средствами Foundry MCP и
читать общую Ontology через отдельную read-only MCP. Sharing, роли,
межпользовательская видимость, release/publication и новые product ACL в этот
срез не входят.

Установщик идемпотентно обновляет только блоки `nodedc_module_foundry` и
`nodedc_ontology` в `~/.codex/config.toml`, сохраняет остальные MCP (включая
Engine и Ops), делает backup конфигурации и устанавливает skill
`foundry-context`. Перед успешным завершением он выполняет `initialize` и
`tools/list` для обеих MCP. После установки Codex Desktop нужно полностью
перезапустить.

## Entitlement для AI Workspace

`POST /api/ai-workspace/entitlements`

Endpoint предназначен для уже существующего AI Workspace generic adapter. Он принимает его штатный request-контракт и выдаёт динамический MCP grant только если совпала одна из политик:

- `FOUNDRY_ALLOW_ALL_AUTHENTICATED=true` — временно разрешает всем уже аутентифицированным пользователям;
- `FOUNDRY_ALLOWED_OWNER_IDS` — список дополнительных разрешённых user id / owner key;
- `FOUNDRY_ALLOWED_OWNER_GROUPS` — список дополнительных разрешённых групп.

По умолчанию доступ получает `user_root` (`dcctouch@gmail.com`) и техническая
группа Authentik `nodedc:module-foundry:access`. В Launcher эта группа выдаёт
роль сервиса `member`; все остальные пользователи получают «нет доступа» до
явной выдачи права администратором Hub. Многопользовательское владение
Applications в v0.1 не вводится: это общий внутренний контур с аудитом actor id.

Для production предпочтительны явно заданные users/groups; режим `ALLOW_ALL_AUTHENTICATED` допустим только для внутренней песочницы.

Штатный запрос adapter выглядит так:

```json
{
  "schemaVersion": "ai-workspace.entitlement-request.v1",
  "appId": "module-foundry",
  "owner": {
    "userId": "authenticated-user-id",
    "key": "owner-or-workspace-key",
    "groups": ["optional-group"]
  },
  "activeContext": {},
  "runContext": {},
  "requestedAt": "2026-07-13T00:00:00.000Z"
}
```

## Server-side configuration

Все значения находятся только в server environment. В browser bundle не передаются tokens, entitlement keys и runtime volume paths.

```dotenv
FOUNDRY_PUBLIC_URL=https://<future-foundry-domain>
FOUNDRY_MCP_URL=https://<future-foundry-domain>/api/mcp
# Existing NODE.DC server-to-server credential; never send it to a browser or worker.
NODEDC_INTERNAL_ACCESS_TOKEN=<existing-platform-service-value>
# Internal-only Ontology Core address; never expose the Core directly.
NODEDC_ONTOLOGY_CORE_URL=http://ontology-core:8080
FOUNDRY_MCP_CAPABILITY_TTL_MS=600000
# Optional exceptional identities, not the default dcctouch superadmin grant.
FOUNDRY_ALLOWED_OWNER_IDS=<comma-separated-extra-ids>
FOUNDRY_ALLOWED_OWNER_GROUPS=<comma-separated-extra-groups>
FOUNDRY_ALLOW_ALL_AUTHENTICATED=false
FOUNDRY_MCP_ALLOWED_ORIGINS=https://<ai-workspace-domain>
```

Foundry не требует у пользователя вводить platform secret.
`NODEDC_INTERNAL_ACCESS_TOKEN` — уже существующий server-to-server credential
платформы: он находится только в server environment и используется для
Launcher/Authentiк handoff, внутренней entitlement-проверки и server-side
Ontology Core proxy. AI Workspace worker получает лишь короткоживущую
capability; внешний Codex — отдельные Agent credentials, но не internal token.

После появления домена в deployment AI Workspace добавляется только штатная настройка adapter; новый worker, отдельная оркестрация или специальный bridge не нужны:

```text
AI_WORKSPACE_ENTITLEMENT_ADAPTERS_JSON='{"module-foundry":{"url":"https://<future-foundry-domain>/api/ai-workspace/entitlements","required":false}}'
```

Если в переменной уже есть другие adapters, `module-foundry` добавляется в тот же JSON-объект — существующие `ops`, `engine`, `launcher` и другие grants не заменяются.

## Deployment package

Foundry подготовлен к отдельному deployment как stateless server + один named persistent volume. Runtime volume содержит Applications, Design Profiles, idempotency audit, data-product consumer state/cursors, media uploads, Agent/device records, setup-code digests и Agent audit. Он является единственным источником изменяемого Foundry state; Git не используется для runtime-данных и tile cache. Raw Foundry/Ontology Agent credentials и EDP reader capabilities в volume не записываются.

```bash
cp .env.example .env
# использовать уже имеющийся NODEDC_INTERNAL_ACCESS_TOKEN из server environment
docker compose --env-file .env -f infra/docker-compose.module-foundry.yml up -d --build
curl http://127.0.0.1:9920/healthz
```

Внешний маршрут подготовлен: `https://foundry.nodedc.ru` → `172.22.0.222:9920` → container `:3333`. Compose на NAS должен слушать именно `172.22.0.222:9920`, а не все интерфейсы. До запуска Foundry proxy закономерно возвращает `502`; это проверяемый признак, что DNS/TLS/proxy уже доходят до внутреннего destination. Публикация допустима только после проверки Launcher's handoff, server-side session revalidation, синхронизации Authentik-группы и выдачи Hub access. Только после этого добавляется Foundry entitlement adapter в platform AI Workspace deployment.

## Следующие слои

1. Онтологический contract приложения, страницы, page instance, Map Page и visual entities.
2. Расширение runtime adapter с points на targets, zones, routes и tracks.
3. Управляемая публикация и повторное использование presentation profiles между Applications.
4. Access/Hub registration и публикация module route.
5. Отдельная пустая программируемая страница — только после того, как готовые page templates и их MCP-контуры отработаны.
