feat(foundry): productionize Cesium Map Page and platform runtime

This commit is contained in:
Codex
2026-07-16 02:25:58 +03:00
parent 1a2ca8c82c
commit 5e8c1cc3fc
41 changed files with 6977 additions and 179 deletions
+2 -2
View File
@@ -160,9 +160,9 @@ Pill navigation для верхней панели и компактного п
`DragHandle`, `DragDropRoot`, `DraggableItem`, `DropZone`, `SortableScope`, `SortableItem` и `SortableList` образуют общий React-контракт переноса и сортировки. Он перенесён из рабочего Engine-паттерна: drag начинается только за шесть точек и только после движения на `6 px`, поэтому обычный клик по строке не конфликтует с навигацией.
Sortable-строки ограничены вертикальной осью: горизонтальное движение указателя не сдвигает navigation или composition layout. `DragDropRoot` возвращает реальные active/over rectangles, поэтому consumer может принять перенос только после достаточного перекрытия destination. Module Studio использует порог `20%` ширины переносимой строки: короткое движение, движение влево или касание границы не создаёт page instance.
Sortable-строки ограничены вертикальной осью: горизонтальное движение указателя не сдвигает navigation или composition layout. `DragDropRoot` возвращает реальные active/over rectangles, поэтому consumer может принять перенос только после достаточного перекрытия destination. Module Foundry использует порог `20%` ширины переносимой строки: короткое движение, движение влево или касание границы не создаёт page instance.
В Module Studio исходная строка Page Template остаётся в Page Library после переноса. Каждый подтверждённый drop создаёт новый page instance с уникальным id; состав приложения и левая навигация читают один массив страниц и потому всегда перестраиваются синхронно. Удаление instance выполняется стандартным `ConfirmationModal`, а не мгновенным действием крестика.
В Module Foundry исходная строка Page Template остаётся в Page Library после переноса. Каждый подтверждённый drop создаёт новый page instance с уникальным id; состав приложения и левая навигация читают один массив страниц и потому всегда перестраиваются синхронно. Удаление instance выполняется стандартным `ConfirmationModal`, а не мгновенным действием крестика.
## Icon
+49
View File
@@ -0,0 +1,49 @@
# Foundry Data Product bindings
`NDC Foundry Binding` is a deploy/control-plane operation. Realtime facts do
not pass through the node: Foundry persists an approved page-slot binding and
its same-origin BFF reads snapshot + patch from External Data Plane.
## Private workload API
```text
GET /internal/foundry/v1/data-products
POST /internal/foundry/v1/data-product-bindings
Authorization: Bearer ndc_fndbg_<opaque capability>
```
The POST body is `nodedc.foundry.binding-upsert/v1`. It contains only
application, page, binding, Data Product projection and a stable idempotency
key. Provider, tenant, connection, endpoint, URL, raw payload and credentials
are rejected.
This credential is not a Foundry MCP `fnd1.*` capability. MCP capabilities are
short-lived interactive grants for AI Workspace. It is also not
`NODEDC_INTERNAL_ACCESS_TOKEN`, a browser session, or an EDP writer/reader
grant.
## Workload grant
The opaque value never enters a Foundry env file or application manifest.
Foundry hashes it with SHA-256 and resolves a root-owned, non-writable record
from `NODEDC_FOUNDRY_BINDING_GRANTS_DIR` on every request. Removing or replacing
the record therefore revokes or rotates access immediately.
A `nodedc.module-foundry.binding-grant.v1` record fixes:
- expiry and actions (`catalog.read`, `map-data-product.upsert`);
- actor and owner used by the existing replay-safe Foundry write path;
- exact allowlisted tuples of application, page, binding, slot, Data Product,
semantic types and field projection.
No authorization claim is accepted from request headers or workflow data.
Before persisting a binding, Foundry verifies that the target-specific EDP
reader grant exists, is active, permits the selected product and exposes a
`snapshot+patch` product. The reader grant filename remains
`sha256(applicationId/pageId/bindingId)`. If it is not ready, POST fails with
`409 data_product_reader_grant_not_ready`; an unusable visual binding is never
saved.
The canonical Map entity-stream slot is `points`. Other templates may define
their own typed slots, but the workload grant must name them explicitly.
+847
View File
@@ -0,0 +1,847 @@
# Foundry Map Page + Cesium: production canon
Статус: **действующий production-контракт**
Область: Module Foundry Map Page, Platform Map Gateway, общий TileCache, DC AMD Proxy и DC AMD Connector
Контрольная дата: 2026-07-16
Этот документ — воспроизводимый канон интеграции Cesium в NODE.DC. Он описывает не только текущий Foundry Map Page, но и обязательные границы для следующих продуктов NODE.DC, которым потребуются Cesium Terrain, imagery, 3D Tiles или другие разрешённые provider assets.
Текущий проверенный deploy baseline:
- `dc-amd-proxy-transport-lifecycle-20260716-005`;
- `platform-map-gateway-hot-path-20260716-015`;
- `module-foundry-map-hot-path-20260716-012`.
Идентификаторы baseline нужны для аудита. Их нельзя повторно использовать для новых изменений: каждый следующий deploy получает новый patch id и новый digest.
## 1. Главные инварианты
1. Browser никогда не получает Cesium Ion master token, asset-scoped token, Bing key, Map Gateway admin secret или egress/connector secret.
2. Foundry не хранит Cesium token в `.env`, runtime layout, Application Manifest, browser storage, deploy artifact или исходном коде.
3. Единственный владелец Cesium master token и provider endpoint credentials — private Platform Map Gateway.
4. Все browser-запросы карты идут same-origin через Foundry BFF. Browser не обращается напрямую ни к Map Gateway, ни к AMD Proxy, ни к Cesium/Bing.
5. Один Platform Map Gateway и один NAS-resident mutable TileCache обслуживают все Foundry Applications, страницы, пользователей и browser-инстансы.
6. Warm cache hit читается прямо с NAS и не требует Ion token refresh, AMD Proxy, Windows-машины, VPN или доступности Cesium.
7. Только cold miss или явный refresh проходит по внешнему маршруту `Map Gateway → DC AMD Proxy → DC AMD Connector → VPN → Cesium/Bing`.
8. NAS networking, DNS, default route, Tailscale и VPN не изменяются. Через AMD-машину идёт только allowlisted Cesium/Bing traffic.
9. TileCache не лежит в Git, Docker image layer или application volume. Это отдельный persistent NAS bind directory.
10. Записанный объект не удаляется автоматически и не перезаписывается без явного `nodedc_cache_refresh=1`.
11. Partial response никогда не становится cache entry. Object публикуется atomic rename, а fill считается завершённым только после durable snapshot индекса.
12. Все deploy-изменения доставляются data-only artifact через canonical runner: сначала `plan` точного файла, затем `apply` этого же файла. Live edit и ручной `docker compose up` на NAS не являются deploy-каноном.
## 2. Источники истины в репозитории
Канон должен обновляться вместе с этими файлами:
- Foundry BFF и admin settings: `server/catalog-server.mjs`;
- Foundry session/auth boundary: `server/nodedc-auth.mjs`;
- UI TileCache и health state: `apps/catalog/src/MapFixturePreview.tsx`;
- Cesium adapter и provider startup: `apps/catalog/src/CesiumMapRenderer.tsx`;
- Platform provider/cache boundary: `../../platform/services/map-gateway/src/server.mjs`;
- Map Gateway runtime topology: `../../platform/infra/synology/docker-compose.platform-http.yml`;
- NAS egress service: `../../platform/services/dc-amd-proxy/server.mjs`;
- AMD egress installation, recovery and migration runbook: `../../platform/services/dc-amd-proxy/OPERATIONS.md`;
- Windows connector: `../../platform/services/dc-amd-connector/`;
- одноразовое pairing: `../../platform/tools/dc-amd-pair/`;
- canonical artifact builders: `../../platform/infra/deploy-runner/`.
Если текст этого документа расходится с уже задеплоенным кодом, сначала фиксируется расхождение в Ops, затем синхронно исправляются код, тесты и этот канон. Нельзя молча считать устаревший текст runtime-поведением.
## 3. Архитектура и владельцы
```text
Authenticated browser
│ same-origin GET/HEAD + session cookie
Foundry BFF
│ trusted subject header, no provider credential
Platform Map Gateway
├─ warm hit ───────────────► NAS live TileCache ─► browser
└─ miss / explicit refresh
│ private egress token
DC AMD Proxy on NAS 172.22.0.222:8790
│ authenticated CONNECT transport
DC AMD Connector on Windows 172.22.0.183:8791
│ workstation VPN exit
Cesium Ion / Cesium assets / Bing imagery
│ streamed response
├─► browser immediately
└─► private temp file ─atomic rename─► NAS TileCache
```
### 3.1 Responsibility table
| Компонент | Владеет | Не владеет |
| --- | --- | --- |
| Map Page | provider-neutral scene, presentation, camera, cache intent | tokens, upstream host policy, cache files, VPN |
| Cesium adapter | преобразование scene в Cesium objects, запрос allowlisted asset endpoints | master token, direct provider networking |
| Foundry BFF | user session, same-origin proxy, admin authorization, HMAC signing | provider credentials, TileCache, VPN route |
| Map Gateway | provider allowlist, credential injection, endpoint metadata, TileCache policy/index, upstream diagnostics | UI layout, Windows VPN configuration |
| NAS live TileCache | общие immutable-by-default provider objects и индекс | Git source, per-user state, offline license |
| DC AMD Proxy | узкий authenticated egress, pooling, retry/redirect safety, transport metrics | general proxying, NAS route, provider token persistence |
| DC AMD Connector | CONNECT byte forwarding через текущий Windows VPN | provider URL parsing above host:443, master token, NAS routing |
| Windows workstation | стабильный LAN endpoint, Docker Desktop, VPN exit | Foundry/Map Gateway runtime state |
| deploy runner | root-owned secrets, runtime directories, atomic artifact apply/rollback | application UI, token entry |
### 3.2 Один writer для общего cache
Текущая topology предполагает один активный `map-gateway` process, который является единственным writer для `index.json`. Все Foundry-инстансы используют его через private Docker network.
Масштабирование Foundry горизонтально безопасно: cache общий. Горизонтальное масштабирование самого Map Gateway без доработки запрещено, потому что tile singleflight и coalescing индекса сейчас process-local. Перед запуском нескольких Gateway replicas нужен распределённый per-key lock и согласованный single-writer/transactional index.
## 4. Два независимых маршрута
### 4.1 Control plane: настройка Cesium Ion
```text
Foundry admin browser
→ PUT /api/platform-settings/cesium-ion
→ Foundry revalidates admin session
→ HMAC-signed PUT /api/map/admin/cesium-ion
→ Map Gateway verifies assets 1, 2, 96188
→ atomic private token write
```
Порядок принципиален:
1. UI принимает новое значение как password field по существующей HTTPS-сессии.
2. Foundry повторно проверяет server-side роль; скрытая кнопка сама по себе не является защитой.
3. Foundry не сохраняет token, а формирует HMAC v2 по method, pathname, timestamp, actor id, SHA-256 body и нормализованному Foundry referer.
4. Map Gateway принимает admin request только с валидной подписью и timestamp в пределах 60 секунд.
5. Candidate token проверяется параллельно по canonical assets:
- `1` — Cesium World Terrain;
- `2` — World Imagery/Bing endpoint;
- `96188` — Cesium OSM Buildings.
6. Только если все три проверки успешны, рабочее значение атомарно заменяется.
7. Ошибочный candidate не уничтожает предыдущий рабочий token.
8. Browser получает только `configured`, `verification`, `updatedAt`, `updatedBy`; значение token не возвращается.
Успешная rotation увеличивает внутреннее поколение token, очищает in-memory и disk endpoint credentials для allowlisted assets и не трогает уже записанные tile objects. In-flight refresh старого поколения не может вернуть старый credential после rotation.
### 4.2 Data plane: рендер карты
При старте Map Page Cesium adapter независимо и параллельно запрашивает imagery, terrain и buildings. Отказ imagery не блокирует terrain; отказ buildings не останавливает остальную сцену.
```text
GET /api/map/runtime-config
GET /api/map-gateway/api/map/ion/assets/2/endpoint
GET /api/map-gateway/api/map/ion/assets/1/endpoint
GET /api/map-gateway/api/map/ion/assets/96188/endpoint
```
Endpoint response содержит публичный URL, attribution, тип и `credentialMode: "gateway"`. Затем Cesium `DefaultProxy` направляет все derived resources через:
```text
/api/map-gateway/api/map/cache?url=<encoded-provider-url>
```
Master token или asset credential в этом URL отсутствует. Map Gateway добавляет credential server-side только после cache lookup и только если действительно нужен upstream request.
## 5. Credential model
### 5.1 Cesium Ion master token
Canonical location внутри mutable Map Gateway volume:
```text
/var/lib/nodedc-map-live-cache/secrets/cesium-ion-token
```
Физически на NAS:
```text
/volume1/docker/nodedc-platform/map-gateway/live-tile-cache/secrets/cesium-ion-token
```
Directory создаётся с mode `0700`, token и metadata — `0600`. Private NAS file имеет приоритет над transitional `CESIUM_ION_TOKEN` bootstrap после restart.
Нельзя:
- помещать token в Foundry `.env`;
- собирать его во frontend bundle;
- вставлять в Application Manifest;
- отправлять в chat, Ops comment, diagnostic archive или deploy artifact;
- читать его обратно через UI;
- включать `live-tile-cache/secrets` в обычный tile export или SMB-операции.
### 5.2 Map Gateway admin secret
Runner-owned path:
```text
/volume1/docker/nodedc-platform/secrets/map-gateway-admin-secret
```
Он монтируется read-only в Foundry и Map Gateway как `/run/nodedc-secrets/map-gateway-admin-secret`. Это ключ межсервисной подписи, а не Cesium token и не продуктовая настройка. Он никогда не вводится пользователем.
### 5.3 Map egress proxy token
Runner-managed path:
```text
/volume1/docker/nodedc-platform/secrets/map-egress-proxy-token
```
Map Gateway читает его из read-only mount и ставит в `x-proxy-token` только при запросе к DC AMD Proxy. Browser и Windows connector его не получают.
### 5.4 AMD connector access token
Windows installer создаёт локальный random value в:
```text
C:\NODEDC\dc-amd-connector\runtime\connector-access
```
Одноразовый pairing script читает его внутри запущенного container и передаёт напрямую на NAS без печати. NAS сохраняет paired value в private runtime `dc-amd-proxy`; deploy artifact его не содержит.
### 5.5 Asset-scoped endpoint credentials
Ответ Ion endpoint API может содержать временный `accessToken` либо Bing key. Map Gateway хранит эти значения только в памяти и private files:
```text
live-tile-cache/ion-endpoints/<assetId>.json
```
Файлы имеют mode `0600` и считаются service-sensitive. Master token в них не записывается.
Перед выдачей endpoint browser-у Gateway удаляет credential query parameters. Перед upstream miss Gateway:
1. снова удаляет user-supplied `token`, `key`, `signature` и аналогичные параметры;
2. сопоставляет resource с endpoint по точному origin и наиболее длинному разрешённому path scope;
3. для file endpoint, например `tileset.json`, разрешает sibling resources только внутри его directory;
4. никогда не превращает root-level file endpoint в credential scope всего host;
5. добавляет соответствующий asset credential или Bing key.
Endpoint cache имеет TTL 300 секунд и refresh-ahead 60 секунд по умолчанию. Refresh одного asset singleflight-ится. Истёкший JWT не выдаётся, не сохраняется и не подставляется.
### 5.6 Referer restrictions
Если Cesium token ограничен URL/referer policy, production `FOUNDRY_PUBLIC_URL` должен совпадать с разрешённым Foundry origin. Foundry делегирует только собственный configured origin, а не произвольный browser header. Нормализованный referer входит в HMAC admin request и передаётся в provider requests.
Прямая terminal-проверка token без того же referer может вернуть `401`, хотя production flow работает. Каноническая проверка — через Foundry settings/Map Gateway, потому что она воспроизводит реальный транспорт и проверяет все три обязательных asset.
## 6. Физическое устройство TileCache
### 6.1 NAS paths
Mutable live cache:
```text
/volume1/docker/nodedc-platform/map-gateway/live-tile-cache
```
Container mount:
```text
/var/lib/nodedc-map-live-cache
```
Read-only offline snapshot:
```text
/volume1/docker/nodedc-platform/map-gateway/offline-snapshot
```
Container mount:
```text
/var/lib/nodedc-map-offline-snapshot
```
Обе host directory создаёт root-owned deploy runner. `docker compose down`, image rebuild и service restart их не удаляют.
### 6.2 On-disk format
```text
live-tile-cache/
├── index.json
├── objects/
│ └── <first-two-hash-chars>/
│ └── <sha256-cache-key>.bin
├── ion-endpoints/
│ ├── 1.json
│ ├── 2.json
│ └── 96188.json
└── secrets/
├── cesium-ion-token
└── cesium-ion-token.metadata.json
```
`objects` и `index.json` — собственно TileCache. `ion-endpoints` и `secrets` — private service state; они не являются пользовательскими cache objects.
`index.json` version 1 содержит для каждого SHA-256 key:
- relative file path;
- byte count;
- content type;
- provider ETag, если он корректен;
- `savedAt`, `lastAccessAt`, `expiresAt`.
Binary object сохраняется с расширением `.bin` независимо от media type; content type берётся из индекса. Cache key — SHA-256 canonical provider URL.
### 6.3 Canonical URL and deduplication
До вычисления key удаляются:
- `nodedc_client_revision`;
- `nodedc_cache_profile`;
- `nodedc_cache_mode`;
- `nodedc_cache_refresh`;
- credential-like query parameters.
Query parameters сортируются. Bing subdomains `ecn.t0``ecn.t3` нормализуются в один logical host, чтобы одинаковый quadkey не создавал четыре копии.
Следствие: token rotation, новый browser build или другой Foundry Application не создают новый tile object для тех же provider bytes.
## 7. Product semantics of cache controls
### 7.1 `Кэшировать live-данные` выключено
Adapter добавляет `nodedc_cache_mode=passthrough`. Запрос всё равно проходит через Foundry и Map Gateway, поэтому credential/security boundary сохраняется, но live persistent cache не читается и не записывается.
Это не direct browser-to-Cesium режим.
### 7.2 Cache включён + `Не перезаписывать уже полученный cache` включено
Это основной steady-state режим:
- hit немедленно отдаётся с NAS;
- miss идёт к official upstream и после полного получения дописывается;
- существующий объект не заменяется даже после его provider TTL;
- отключение AMD/VPN не влияет на уже записанную область.
Галка не замораживает cache целиком. Она запрещает перезапись существующих key, но новые tiles, которые пользователь впервые открыл, продолжают долетать и записываться на NAS.
### 7.3 Cache включён + `Не перезаписывать` выключено
Adapter добавляет `nodedc_cache_refresh=1` в provider resource root. Derived requests текущего Application могут заменить существующие objects. Это явно разрешённый update mode, а не автоматический фоновый refresh всего cache.
Обновляются только ресурсы, которые реально запросил renderer; мировой dataset не обходится целиком.
### 7.4 `Обновить текущий viewport`
Это одноразовый refresh. Renderer пересоздаётся с `nodedc_cache_refresh=1`, помечает root provider resources текущего view, после первого render возвращается к сохранённой steady-state policy.
### 7.5 Gateway-level modes
| `MAP_CACHE_MODE` | Hit | Miss | Запись |
| --- | --- | --- | --- |
| `readwrite` | NAS | official upstream | да, после полного response |
| `readonly` | NAS | live pass-through | нет |
| `offline` | NAS/offline policy | `504` | нет и никакого egress |
Application UI не может изменить server-level `MAP_CACHE_MODE`.
### 7.6 Live vs offline profile
`nodedc_cache_profile=live` выбирает mutable store. `offline` выбирает read-only snapshot и никогда не допускает pass-through.
Offline mode дополнительно требует hostname в `MAP_GATEWAY_OFFLINE_PROVIDER_ALLOWLIST`. Пустой список по умолчанию означает осознанный запрет offline redistribution. Наличие технически сохранённых Cesium bytes само по себе не создаёт право на их offline distribution.
## 8. Request algorithm
Для каждого `/api/map/cache` Gateway выполняет следующий порядок:
1. Проверяет trusted subject, method и route.
2. Парсит и валидирует HTTPS target, host allowlist, URL length и отсутствие embedded credentials.
3. Нормализует cache profile/mode/refresh intent и удаляет их из provider URL.
4. Вычисляет credential-free canonical key.
5. Для `passthrough` пропускает persistent store и идёт к upstream через тот же security boundary.
6. Для normal mode сначала ищет object в выбранном store.
7. Если по этому key уже идёт fill/refresh, новый request становится follower и ждёт его commit.
8. Offline/legacy profile никогда не превращает miss в неожиданный upstream fetch.
9. Normal hit без explicit refresh сразу отдаётся с NAS, в том числе stale hit.
10. Только после отсутствия пригодного hit Gateway получает/обновляет endpoint credential и подставляет его в upstream URL.
11. `readonly` отдаёт live response без записи.
12. Range request проксируется как range и пока не записывается partial object.
13. Full miss/refresh запускает streaming fill.
14. При provider failure explicit refresh возвращает предыдущий cached object как `live-stale-upstream-error`, если он существует.
15. При заполненном cache cold miss показывается live как `live-pass-through-cache-full`; существующие objects не удаляются.
Этот порядок гарантирует ключевой fail-safe: warm cache не зависит от control plane и egress.
## 9. Concurrency and performance
### 9.1 Что происходит при 20 одновременных пользователях
Для одного и того же missing key:
1. Первый request становится leader и открывает один upstream stream.
2. Response body делится через stream tee.
3. Client branch начинает передаваться первому browser сразу после provider headers.
4. Cache branch независимо пишется во временный файл.
5. Остальные request того же key становятся followers и не создают новые provider downloads.
6. После atomic commit followers читают опубликованный NAS file.
7. Если один browser закрыл вкладку, server-owned cache fill не отменяется и продолжает обслуживать других.
Для разных keys requests выполняются параллельно. Capacity reservations сериализуют только расчёт общего объёма, а не весь network path.
### 9.2 Publication safety
Cache fill:
- пишет только в private `*.tmp`;
- считает фактические bytes и прерывает object сверх лимита;
- резервирует capacity с учётом replacement;
- делает atomic rename только после полного EOF;
- затем добавляет entry в memory index и ждёт durable index snapshot;
- при abort/error удаляет temp и не публикует index entry.
### 9.3 Index strategy
Warm hit не обновляет `lastAccessAt` и вообще не переписывает `index.json`. Это осознанное следствие no-eviction policy: запись полного O(N) JSON на каждый 20 KB tile делала warm views медленнее provider.
Завершения разных new objects объединяются в короткое 10 ms окно. Один atomic index snapshot включает все накопленные revisions; waiter каждого object завершается только после durable revision.
### 9.4 ETag and browser cache
NAS hit получает безопасный ETag: валидный provider ETag либо deterministic NODE.DC ETag. `If-None-Match` возвращает `304` без чтения body и без записи index.
Foundry BFF разрешает browser cache только для подтверждённых persistent hit/stale states:
```text
Cache-Control: private, max-age=300, stale-while-revalidate=60
Vary: Cookie
```
Provider pass-through, endpoint metadata, health, explicit refresh и errors получают `no-store`. Shared reverse proxy не должен раздавать authenticated map response между пользователями.
### 9.5 AMD connection pool
DC AMD Proxy держит отдельный HTTP/1.1 keep-alive pool на каждый approved origin:
- максимум 8 active sockets per origin;
- максимум 4 idle sockets per origin;
- LIFO reuse;
- keep-alive 30 секунд;
- excess requests ждут в agent queue за bounded socket pool; её фактический пик обязательно контролируется через `maxQueuedRequests`.
Это устраняет новый `NAS → AMD CONNECT → TLS` handshake для каждого tile. При насыщении throughput ограничивается VPN/provider, а не созданием неограниченного числа tunnel. `maxQueuedRequests`, queue/connection/TTFB/total timings показывают, где возникает задержка.
Только idempotent `GET`/`HEAD`, потерявший уже reused keep-alive socket, повторяется один раз. Fresh-socket failure не ретраится автоматически. При cross-origin redirect `Authorization` удаляется; master/API bearer не может уйти с `api.cesium.com` на assets/Bing origin.
## 10. Timeouts, aborts and recovery
### 10.1 Foundry BFF
- headers deadline: 15 секунд по умолчанию;
- body idle deadline: 30 секунд без прогресса;
- body timeout сбрасывается каждым chunk;
- после upstream EOF медленный browser drain не ограничивается абсолютным таймером;
- browser abort отменяет BFF upstream request;
- ошибка после отправки headers закрывает stream, а не пытается отправить второй JSON response.
### 10.2 Map Gateway
- upstream timeout: 30 секунд по умолчанию;
- warm hit не запускает этот timeout;
- refresh failure с прежним object даёт stale fallback;
- client abort leader response не отменяет cache branch;
- headers-sent error уничтожает только response, не Gateway process.
### 10.3 DC AMD Proxy
- connector/TLS connection timeout: 20 секунд;
- provider headers/attempt timeout: 30 секунд;
- response body idle timeout: 30 секунд без bytes;
- abort проходит через queue, CONNECT, TLS, headers, redirect drain, retry и body;
- каждый logical request получает ровно один terminal accounting outcome;
- partial bytes учитываются отдельно от complete response bytes.
### 10.4 Cesium renderer
Provider startup независим. Render error не показывает стандартную Cesium modal с `[object Object]`; UI получает safe code и делает одну bounded попытку восстановить render loop. Постоянный GPU/browser fault не запускает бесконечный retry.
## 11. Authentication and authorization
Production Foundry всегда использует Launcher/Authentik session. Map tile burst не должен валидировать Launcher на каждый object, поэтому:
- validation TTL по умолчанию 20 секунд и зажат в production диапазоне 15–30 секунд;
- запросы одной session делят singleflight validation;
- transient Launcher error использует 2-секундный retry backoff;
- last-known-good identity допускается не более 30 секунд grace и только для `GET`/`HEAD`;
- mutation во время transient auth failure получает `503`;
- только явный `{ok:true, active:false}` удаляет session/cookie;
- timeout, network error и Launcher non-2xx не уничтожают рабочую cookie;
- session хранится process-local, поэтому после Foundry restart старая opaque cookie истекает и нужен новый handoff.
Foundry roles:
- `nodedc:module-foundry:admin` — Platform settings;
- `nodedc:module-foundry:user` — обычная работа без settings;
- `nodedc:module-foundry:blocked` — deny-first;
- `nodedc:module-foundry:access` — transitional user role;
- `nodedc:superadmin` и `user_root` — break-glass admin.
Raw groups не возвращаются browser-у. Map Gateway production route требует trusted `x-nodedc-user-id`, который создаёт только Foundry BFF/private health check. Gateway нельзя публиковать напрямую в Internet.
## 12. DC AMD route
### 12.1 NAS service
`dc-amd-proxy` работает с `network_mode: host`, но слушает только `172.22.0.222:8790`. Host networking нужен, чтобы one-time pairing видел реальный source IP Windows host; service не bind-ится на все NAS interfaces.
Он принимает:
- `GET /healthz` и `GET /status` — safe state/metrics;
- `POST /api/pair` — один раз и только с configured AMD LAN source;
- `GET|HEAD /proxy/cesium/fetch` — только с runner-synchronised map egress token.
Target обязан быть HTTPS port 443 и входить в фиксированный Cesium/Bing allowlist. Direct NAS egress fallback отсутствует (`directEgress: false`).
### 12.2 Windows connector
`dc-amd-connector` bind-ится к стабильному LAN IP `172.22.0.183:8791`, а не к OpenVPN address или public exit IP. Windows Firewall разрешает inbound только с NAS `172.22.0.222`.
Connector:
- принимает только authenticated HTTP `CONNECT`;
- разрешает только port 443 и тот же host allowlist;
- не расшифровывает end-to-end TLS и не видит Ion bearer;
- не является системным Windows proxy;
- не меняет routing/DNS/VPN других приложений;
- ограничен 1 CPU, 256 MB, 64 processes и 4096 file descriptors.
Docker Compose использует `restart: unless-stopped`. Docker Desktop на WSL2 восстанавливается через Startup launcher после входа configured Windows user. До sign-in это не unattended Windows service; в это окно warm TileCache продолжает работать, а cold misses ожидаемо недоступны.
### 12.3 Pairing and moving to another machine
Первичная установка выполняется из elevated PowerShell внутри проверенного connector package:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\install.ps1
```
После того как NAS proxy показывает `state=awaiting_pair`, на Windows запускается package из `platform/tools/dc-amd-pair`:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\pair-dc-amd-proxy.ps1
```
Ожидаемый результат: `AMD connector paired with NAS. No secret value was displayed.`
Перенос:
1. Выбрать новый стабильный LAN IP и проверить VPN на новой машине.
2. Установить тот же versioned connector package с явными `-BindAddress` и `-NasAddress`.
3. Проверить local container health и permitted CONNECT.
4. Подготовить новый NAS proxy artifact/config с новым connector host и pair source.
5. Реализовать и проверить runner-owned pair-rotation transition: текущий NAS pair write-once и не принимает fresh credential поверх существующего state.
6. Через этот transition вывести старую pairing identity, выполнить canonical `plan`, `apply`, one-time pairing и end-to-end validation.
7. Остановить старый connector только после успешного cold-request acceptance на новой машине.
Текущий runner ещё не предоставляет pair rotation как готовую операцию. До её появления миграция с fresh credential не является полностью канонической: нельзя вручную удалять NAS `runtime/connector-access`, копировать старый secret, ослаблять `/api/pair` или одновременно держать две pairing identity. Сначала создаётся отдельная Ops/change задача на audited rotation, затем выполняется перенос по `platform/services/dc-amd-proxy/OPERATIONS.md`.
## 13. Health and observability
### 13.1 Safe checks on NAS
```bash
curl -fsS http://172.22.0.222:8790/status
curl -fsS \
-H 'x-nodedc-user-id: healthcheck' \
http://127.0.0.1:18103/healthz
curl -fsS http://172.22.0.222:9920/healthz
```
Эти команды не печатают secrets. Для endpoint acceptance нужно выводить только safe fields, а не private files:
```bash
curl -fsS \
-H 'x-nodedc-user-id: healthcheck' \
http://127.0.0.1:18103/api/map/ion/assets/1/endpoint \
| /usr/local/bin/docker exec -i nodedc-platform-map-gateway-1 \
node -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>{const v=JSON.parse(s);console.log(JSON.stringify({ok:v.ok,assetId:v.assetId,type:v.type,credentialMode:v.credentialMode,cache:v.cache}))})"
```
Если конкретное имя container изменилось, использовать `docker compose ps`, а не угадывать новое имя.
### 13.2 Map Gateway health fields
`/healthz` возвращает:
- live/offline cache entry count and bytes;
- `mode`, `writePolicy`, `maxBytes`, `atCapacity`, `persistent`;
- hit/stale/miss/refresh/passthrough/fallback counters;
- upstream and egress request/failure counters;
- slow upstream count;
- index snapshot count;
- safe `lastFailure` и timestamp;
- `ionConfigured` и asset allowlist.
Он не возвращает token, provider URL с credential или endpoint credential.
### 13.3 AMD status fields
`/status` обязан показывать:
- `state=paired`;
- `forwarding=amd_connector_only`;
- `directEgress=false`;
- active/idle/queued sockets и pool limits per origin;
- logical, terminal, in-flight, complete, failed and aborted requests;
- opened/failed tunnels, socket reuse, bounded retries;
- complete/partial bytes;
- queue, connection, TTFB, retry, attempt and total timings;
- safe last error code.
Инвариант accounting:
```text
terminalRequests + inFlightRequests == requests
```
После завершившейся нагрузки `inFlightRequests` должен вернуться в `0`.
### 13.4 UI health state
Когда Inspector или Layers открыт, Foundry:
- проверяет runtime config и Gateway health каждые 15 секунд;
- держит только один concurrent health request;
- отменяет check через 10 секунд;
- различает runtime HTTP, health HTTP, invalid response, network и timeout;
- сохраняет last-known-good stats как `stale`, если новая проверка не прошла;
- не даёт запоздалому renderer health overwrite более свежий explicit poll.
Красное сообщение `gateway_not_configured` относится к отсутствию runtime profile, а не автоматически к AMD/VPN. Код ошибки должен сохраняться до UI, иначе оператор ищет проблему не в том слое.
## 14. Failure matrix
| Симптом / safe code | Слой | Что продолжает работать | Проверка / действие |
| --- | --- | --- | --- |
| `module_foundry_auth_required` | Foundry session | public health | пройти Launcher handoff; не обходить BFF |
| `module_foundry_auth_unavailable` | Launcher validation | bounded read-only grace, если ещё активен | Launcher health/internal token; mutation не повторять вслепую |
| `map_gateway_not_configured` | Foundry runtime config | Foundry shell | проверить `NODEDC_MAP_GATEWAY_INTERNAL_URL` и private Docker network |
| `map_gateway_headers_timeout` | BFF→Gateway | shell и уже browser-cached objects | Gateway health, CPU/I/O, request queue |
| `map_gateway_body_idle_timeout` | BFF stream | прочие resources | Gateway/AMD body progress, VPN stalls |
| `map_gateway_auth_required` | Gateway trusted subject | admin signed route/health с корректным header | BFF header boundary, не включать anonymous в production |
| `cesium_ion_not_configured` | Gateway credential store | existing object hits | admin settings; не добавлять token в Foundry env |
| `cesium_ion_token_verification_failed` | provider/control plane | предыдущий token и cache | assets 1/2/96188 permissions, Foundry referer, AMD route |
| `cesium_asset_not_allowed` | asset policy | canonical assets | изменить root-owned allowlist только с reviewed use case |
| `cesium_ion_endpoint_unavailable` | Ion API | warm object hits, usable cached endpoint | AMD state/timings, VPN, provider HTTP status, token/referer policy |
| `map_egress_amd_connector_not_paired` | NAS proxy pairing | warm hits | `/status`, затем controlled pairing |
| `map_egress_amd_connector_timeout` | NAS→Windows | warm hits/stale refresh fallback | Windows host online, LAN/firewall, connector container |
| `map_egress_amd_upstream_tls_timeout` | Windows/VPN→provider | warm hits/stale refresh fallback | VPN exit, DNS/provider reachability on Windows |
| `map_upstream_timeout` | Gateway upstream | warm hits/stale refresh fallback | distinguish queue/CONNECT/TLS/TTFB via AMD metrics |
| `map_cache_capacity_reached` | NAS capacity policy | old hits and live pass-through | provision capacity/export; do not delete arbitrary objects |
| `map_offline_snapshot_miss` | offline snapshot | other snapshot objects | expected miss; seed licensed dataset, never fall through live |
| `map_provider_offline_not_permitted` | provider policy | live mode if allowed | legal/provider review and explicit allowlist |
| `map_cache_object_too_large` | object guard | other objects/live policy | inspect asset type; change limit only after measured review |
| `live-stale-upstream-error` | explicit refresh failed | previous object | expected resilience; investigate upstream asynchronously |
| renderer `cesium_render_error` | browser/GPU/Cesium | shell and provider diagnostics | browser console/GPU; one recovery already attempted |
### 14.1 Reading latency correctly
- Высокий `queueMs`, нормальный connection/TTFB: pool saturated; проверить request fan-out и VPN throughput.
- Высокий `connectionMs`: дорогой NAS→AMD CONNECT или TLS setup; socket reuse недостаточен либо Windows/VPN нестабилен.
- Низкий connection, высокий `ttfbMs`: VPN/provider latency.
- Нормальный AMD timing, высокий Foundry body idle: проблема stream progress между Gateway и BFF либо NAS I/O.
- Много `cacheHits`, но медленная карта: проверить BFF/browser ETag, NAS read latency, renderer/GPU; AMD здесь не должен участвовать.
- `egressRequests` растёт на повторном одинаковом append-only viewport: cache key/refresh policy нарушена либо requests действительно относятся к новым tiles/LOD.
## 15. Canonical deploy and rollback
### 15.1 Pre-deploy validation
Foundry:
```bash
cd NODEDC_DESIGN_GUIDELINE
node --test server/*.test.mjs
npm run build
```
Map Gateway:
```bash
cd platform/services/map-gateway
npm run test:credential-boundary
npm run test:admin-token-boundary
npm run test:live-cache-fallback
npm run test:streaming-cache-fill
```
DC AMD Proxy:
```bash
cd platform/services/dc-amd-proxy
npm run test:connection-pool
docker compose config >/dev/null
```
Production images должны дополнительно собираться с нуля до публикации artifact. Ни один test fixture token не переносится в artifact.
### 15.2 Build data-only artifacts
Из repository root с новыми уникальными patch ids:
```bash
node platform/infra/deploy-runner/build-dc-amd-proxy-artifact.mjs \
dc-amd-proxy-<change>-YYYYMMDD-NNN
node platform/infra/deploy-runner/build-map-gateway-artifact.mjs \
platform-map-gateway-<change>-YYYYMMDD-NNN
node platform/infra/deploy-runner/build-module-foundry-artifact.mjs \
module-foundry-<change>-YYYYMMDD-NNN
```
Artifact обязан содержать только `manifest.env`, `files.txt`, `payload/**`; запрещены secrets, live `.env`, runtime data, cache objects, symlinks, hooks и AppleDouble `._*`.
Создать SHA-256 sidecar, проверить tar listing и скопировать `.tgz` + `.sha256` через SMB в:
```text
/volume1/docker/nodedc-deploy/inbox
```
### 15.3 Plan before apply
На NAS:
```bash
sudo /usr/local/sbin/nodedc-deploy verify-install
sudo /usr/local/sbin/nodedc-deploy plan \
/volume1/docker/nodedc-deploy/inbox/<exact-dc-amd-proxy-artifact>.tgz
sudo /usr/local/sbin/nodedc-deploy plan \
/volume1/docker/nodedc-deploy/inbox/<exact-platform-artifact>.tgz
sudo /usr/local/sbin/nodedc-deploy plan \
/volume1/docker/nodedc-deploy/inbox/<exact-module-foundry-artifact>.tgz
```
До `apply` оператор проверяет digest, component, type=`app-overlay`, payload/compose roots, services, runtime secret mounts, exact files и `state=new`. `sha-already-applied`, неожиданный file или component — стоп, а не повод обходить runner.
### 15.4 Apply order
```text
1. dc-amd-proxy
2. platform / map-gateway
3. module-foundry
```
Команды используют те же exact paths, что прошли `plan`:
```bash
sudo /usr/local/sbin/nodedc-deploy apply \
/volume1/docker/nodedc-deploy/inbox/<exact-dc-amd-proxy-artifact>.tgz
sudo /usr/local/sbin/nodedc-deploy apply \
/volume1/docker/nodedc-deploy/inbox/<exact-platform-artifact>.tgz
sudo /usr/local/sbin/nodedc-deploy apply \
/volume1/docker/nodedc-deploy/inbox/<exact-module-foundry-artifact>.tgz
```
Нельзя использовать `apply latest`, wildcard или вручную распаковывать payload в live roots. Runner создаёт backup и выполняет component health gate. Ошибка одного слоя должна остановить цепочку до следующего apply.
### 15.5 Post-deploy acceptance
1. Все три containers `running` и `healthy`.
2. AMD `/status`: `paired`, `amd_connector_only`, `directEgress=false`.
3. Gateway `/healthz`: `cache.persistent=true`, ожидаемые paths/limits, `ionConfigured=true`.
4. Canonical endpoints 1, 2, 96188 возвращают соответствующие types и `credentialMode=gateway` без credentials.
5. Первый cold viewport начинает рисоваться до завершения disk commit.
6. Повторный viewport увеличивает cache hits, но не egress requests для тех же objects.
7. Выключение Windows VPN не ломает warm viewport; новый uncached viewport даёт диагностируемый transport error.
8. Explicit refresh при недоступном provider отдаёт старый object, если он был.
9. Foundry UI показывает last-known-good cache stats как stale, а не стирает их generic error.
10. Никакой token/key не встречается в browser Network response, logs, artifact listing или Ops comment.
## 16. Reuse in future NODE.DC products
Новый продукт с Cesium не копирует token handling или TileCache внутрь себя. Он подключается к существующей платформенной capability.
Обязательный шаблон:
1. Provider-neutral domain/page contract живёт в продукте.
2. Renderer adapter получает только same-origin runtime config и sanitised asset endpoint.
3. Product BFF подтверждает свою user/session policy и проксирует только разрешённые Map Gateway routes.
4. BFF передаёт trusted subject из server-side identity, а не browser header.
5. Все provider resources используют Gateway proxy; direct provider URL без proxy запрещён.
6. Новый asset id добавляется в reviewed allowlist с ожидаемым type и acceptance test.
7. Cache intent кодируется теми же `profile/mode/refresh` semantics.
8. Cache физически остаётся общим Platform cache; продукт не создаёт per-instance copy.
9. Admin token rotation остаётся одной Platform setting, а не повторяется в каждом продукте.
10. Health UI различает runtime, Gateway, cache и egress layers и сохраняет safe error code.
Если продукту нужен другой provider или offline dataset, сначала расширяется Platform Map Gateway policy. Нельзя просто добавить arbitrary host в frontend. Требуются license review, host/path allowlist, credential scope, cache policy, diagnostics и tests.
## 17. Explicit decisions and forbidden anti-patterns
### Решения
- Shared NAS cache выбран вместо cache на browser/Foundry instance.
- Append-only/no-eviction выбран для предсказуемости; capacity exhaustion не удаляет уже собранную карту.
- Cache-first выполняется до credential injection, чтобы offline resilience была реальной.
- First miss streaming выбран вместо «сначала полностью записать, потом показать».
- One Map Gateway writer выбран до появления distributed locking.
- AMD route ограничен provider hosts и не затрагивает NAS default networking.
- Endpoint credentials кэшируются private и refresh-ятся заранее, master token остаётся отдельным.
- Browser cache разрешён только для подтверждённых NAS hits и только private.
### Запрещено
- token в browser bundle, source, `.env` Foundry, Envoyer-like UI или Application artifact;
- direct Cesium calls из browser;
- proxy всего NAS/Windows traffic через VPN ради Cesium;
- open/general-purpose HTTP proxy на AMD или NAS;
- использование changing VPN IP вместо stable AMD LAN IP;
- cache в Docker writable layer или anonymous volume;
- отдельный TileCache на каждого пользователя/Application/Foundry replica;
- credential query в cache key;
- автоматический refresh каждого hit без product intent;
- полный `index.json` rewrite на каждый warm hit;
- новый CONNECT/TLS tunnel на каждый tile;
- безусловный retry non-idempotent request или fresh-socket failure;
- перенос `Authorization` через cross-origin redirect;
- absolute body deadline, который убивает большой, но прогрессирующий response;
- публикация partial file/index entry;
- автоматический LRU delete без отдельной принятой retention policy;
- silent direct NAS egress, если AMD/VPN недоступен;
- трактовка сохранённых Cesium bytes как разрешения на offline distribution;
- скрытие provider credits на внешнем/коммерческом surface. Текущее скрытие credit container допустимо только как зафиксированный sandbox debt;
- ручной `sudo`/live edit вместо exact runner `plan` + `apply`.
## 18. Known boundaries and next extensions
1. Range requests сейчас проходят live и не записываются как chunk cache. Range-aware cache добавляется только после измерения реальных 3D Tiles/Gaussian assets.
2. Runtime cache сам не prefetch-ит мир. Для offline regions нужен отдельный licensed seed/prefetch job и versioned export/import artifact.
3. Offline snapshot read-only; miss никогда не становится live request.
4. `index.json` подходит текущему single-writer scale. Multiple Gateway writers требуют нового storage contract.
5. Windows Docker Desktop recovery начинается после user sign-in. Если потребуется unattended SLA, connector должен стать отдельно спроектированным Windows service/host, а не скрытым изменением текущей схемы.
6. DC AMD Proxy можно расширять на новые потоки только отдельным policy change. Нельзя добавлять arbitrary URL forwarding к существующему Cesium route.
7. Provider attribution metadata сохраняется, но visible credit surface должен быть возвращён до внешнего release.
## 19. Definition of done for any Cesium change
Изменение считается завершённым только если:
- credential boundary доказан automated test;
- cache hit не делает provider/egress request;
- same-key concurrency делает один upstream fill;
- abort/partial response не публикует object;
- refresh failure сохраняет старый object;
- endpoint rotation не возвращает credential старого поколения;
- BFF корректно передаёт conditional headers и abort;
- session validation не умножается на tile count;
- AMD metrics сохраняют exact terminal accounting;
- logs не содержат URL query, token, cookie, session/user id или secret;
- production images собираются с нуля;
- data-only artifacts проходят inspection и checksum;
- canonical runner показывает ожидаемый `plan` до `apply`;
- post-deploy acceptance проверяет cold, warm, VPN-off и refresh-failure scenarios;
- Ops card и этот документ обновлены вместе с кодом.
Итоговая модель проста: **Platform Map Gateway владеет credential и общим NAS TileCache; Foundry владеет authenticated same-origin BFF и UI intent; AMD-машина предоставляет только узкий VPN egress. Warm data всегда возвращается локально, а live route используется только там, где cache действительно не хватает или оператор явно разрешил refresh.**
+23
View File
@@ -0,0 +1,23 @@
# Platform settings in Module Foundry
`Page Library → Настройки Platform` is visible only to a Foundry administrator. It currently manages the Cesium Ion master token for the shared Platform Map Gateway.
The UI is deliberately not an editor for Foundry `.env`, Envoyer, Docker Compose or a deploy artifact. Those paths are root/operator-owned and would expose a provider secret to the wrong persistence boundary.
The request path is:
```text
admin browser → same-origin Foundry API → HMAC-signed private Gateway route → NAS live cache volume/secrets
```
The browser submits a password field over the existing HTTPS session. Foundry checks the revalidated Launcher/Authentik identity server-side, signs the one request with the internal service secret, and forwards the value without storing it in Foundry runtime data. Before replacing the current private value, Map Gateway verifies the candidate token against the canonical Cesium terrain and imagery assets. Only then does it atomically replace its private token file. The browser receives only `configured`, audit metadata, and the safe verification state — never a token or endpoint credential.
## Foundry groups
- `nodedc:module-foundry:admin` — can see the Page Library gear and change Platform secrets.
- `nodedc:module-foundry:user` — may enter Foundry but cannot access Platform settings.
- `nodedc:module-foundry:blocked` — deny-first; Foundry rejects the session even if another Foundry role was left assigned.
- `nodedc:module-foundry:access` — legacy member role and currently equivalent to `user` while existing Launcher grants are migrated.
- `nodedc:superadmin` and `user_root` — admin for break-glass/bootstrap continuity.
Raw groups never go back to the browser. Foundry returns only the resolved `admin` or `user` role in `/api/session/profile`; every settings endpoint rechecks admin access independently of the UI.
+27 -9
View File
@@ -1,13 +1,13 @@
# Map Template
`Map Page 0.1.0` — первый готовый функциональный шаблон NDC Module Studio. Он описывает пространственный интерфейс через стабильные доменные сущности, а не через API конкретного renderer.
`Map Page 0.1.0` — первый готовый функциональный шаблон NDC Module Foundry. Он описывает пространственный интерфейс через стабильные доменные сущности, а не через API конкретного renderer.
## Границы ответственности
- Page Template владеет компоновкой страницы, Inspector, Toolbar, Assistant entry point, слотами данных и системными действиями.
- Map Scene Fixture задаёт минимальный проверочный набор пространственных сущностей и состояний.
- Application Manifest фиксирует экземпляр страницы, выбранный Design Profile и feature visibility.
- Platform capability binding позже связывает слоты шаблона с NDC runtime и потоками данных.
- Platform capability binding связывает слоты шаблона с scoped data products через Foundry runtime BFF.
- Renderer adapter преобразует provider-neutral scene в Cesium или другой поддерживаемый renderer.
Engine/NDC остаётся средой создания и исполнения автоматизаций. Он не является владельцем визуального языка Map Template и не экспортирует в Studio внутренние Cesium objects.
@@ -46,22 +46,22 @@ Fixtures малы и детерминированы. Большие геодан
Server-side runtime contract:
- `GET /api/map/runtime-config` сообщает версию renderer и readiness возможностей, но не возвращает master token;
- `GET /api/map/ion/assets/:assetId/endpoint` разрешает только allowlisted assets и обменивает server-side master token на asset-scoped endpoint token;
- `CESIUM_ION_TOKEN` передаётся процессу через deployment environment/secret;
- `GET /api/map/ion/assets/:assetId/endpoint` разрешает только allowlisted assets и возвращает public provider URL без credential; private Gateway добавляет asset credential только к своему upstream request;
- `CESIUM_ION_TOKEN` передаётся только private Platform Map Gateway через deployment environment/secret, не в Foundry;
- `CESIUM_ION_ASSET_ALLOWLIST` ограничивает terrain/buildings/Gaussian assets;
- production endpoint переезжает в `platform/services/map-gateway`.
Development gateway уже позволяет проверить World Terrain и 3D Buildings, не сериализуя master token во frontend. Asset-scoped token является частью официального ion endpoint contract и ограничен конкретным asset.
Development и production gateway позволяют проверить World Terrain и 3D Buildings, не сериализуя никакой Cesium/Bing credential во frontend.
## TileCache boundary
Runtime tile cache не является исходным кодом и не хранится в Git или Docker image layer. Целевой `map-gateway` использует отдельный persistent volume либо object storage и поддерживает:
Runtime tile cache не является исходным кодом и не хранится в Git или Docker image layer. Production `map-gateway` использует NAS bind directory `/volume1/docker/nodedc-platform/map-gateway/live-tile-cache` и поддерживает:
- online-record и offline-fallback;
- cache-first read/write и явный live-refresh;
- нормализацию cache key без credentials;
- upstream allowlist и SSRF protection;
- ограничения размера объекта и общего объёма;
- LRU/eviction, stats, health и наблюдаемость;
- append-only no-overwrite по умолчанию, явный refresh viewport, stats, health и наблюдаемость;
- отдельный export/import версионируемых seed snapshots при необходимости.
Существующий Engine cache используется как donor поведения, но его runtime-файлы не копируются в Studio.
@@ -76,5 +76,23 @@ Runtime tile cache не является исходным кодом и не х
2. спроектировать batching/instancing contract для больших потоков объектов и геозон;
3. вынести development gateway в `platform/services/map-gateway` и подключить persistent cache;
4. добавить реальный Gaussian Splat 3D Tiles acceptance asset;
5. добавить capability binding между NDC runtime и slots страницы;
5. расширить entity-stream adapter с `points` на traces, routes и zones;
6. оформить renderer adapter capability matrix для Cesium и будущих providers.
## Live data-product runtime
`Map Page` хранит только provider-neutral binding: `dataProductId`, approved
semantic types, field projection и `slotId` (`points` для live point entities).
При открытии Application page browser делает same-origin запрос к Foundry:
```text
Application/Page/Binding → Foundry BFF → External Data Plane snapshot → SSE patch
```
Foundry сопоставляет target с root-owned opaque reader grant в закрытом
deployment directory. Browser, manifest и Cesium adapter не получают provider
endpoint, tenant/connection scope, reader token или raw provider payload.
Сначала приходит snapshot, затем только patch events с cursor; при пропуске
cursor BFF требует resync snapshot. Renderer держит отдельный `CustomDataSource`
на binding и обновляет stable entity id без пересоздания viewer. History и
sampling остаются политикой Data Plane, а не Map Template.
+161
View File
@@ -0,0 +1,161 @@
# 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, провайдера карт или внешний API из Foundry MCP |
| Создавать/обновлять provider-neutral map pin bindings | Хранить provider tokens, transport payload или credentials в manifest |
| Связывать versioned data product с approved Map entity-stream slot | Хранить provider ID, tenant/connection, endpoint или credential в data binding |
Каждая запись требует `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 <short-lived server-issued Foundry capability>
MCP-Protocol-Version: 2025-06-18
```
Capability выдаётся только сервером Foundry после штатной entitlement-проверки
AI Workspace. Она подписана существующим внутренним service credential,
привязана к `actorId` и `ownerKey`, живёт не более 10 минут и не даёт worker
доступа к самому platform credential. Заголовки с actor/owner от worker не
принимаются: контекст извлекается только из подписанной capability.
Доступные инструменты:
- `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_upsert_map_data_product_binding`
`foundry_upsert_map_pin_binding` хранит только визуальную, provider-neutral привязку `elevated-spike`: стабильный id, subject, координаты, semantic status и ссылку на источник сущности. Поток живых данных не передаётся в MCP по одной позиции: визуальная привязка и поток данных будут связываться следующими contract/ontology слоями.
`foundry_upsert_map_data_product_binding` сохраняет только декларацию
`data product → Map entity-stream slot`: versioned data product id, semantic
types и допустимую field projection. Endpoint, provider, tenant, connection,
token, credential и raw payload валидатор отклоняет. Page runtime получает
scoped snapshot/patch поток через Platform, но не через Foundry MCP.
## 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/stream?after=:cursor
```
Маршрут разрешает persisted binding, а затем серверно находит opaque EDP reader
grant по `sha256(applicationId/pageId/bindingId)`. Grant находится в
root-owned read-only directory, передаётся только как `Authorization` во
внутренний External Data Plane и никогда не попадает в browser, manifest,
MCP или лог. В browser отдаётся только canonical data-product envelope:
snapshot, safe `nodedc.data-product.patch/v1` upserts и cursor. `Last-Event-ID`
авторитетнее старого `after` при автоматическом SSE reconnect.
## 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>
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 не требует у пользователя новый secret. `NODEDC_INTERNAL_ACCESS_TOKEN`
уже существующий server-to-server credential платформы: он находится только в
server environment и используется и для Launcher/Authentiк handoff, и для
внутренней entitlement-проверки. Worker получает лишь короткоживущую capability.
После появления домена в 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 и media uploads. Он является единственным источником изменяемого Foundry state; Git не используется для runtime-данных и tile cache.
```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 между ontology/gateway потоками и сохранёнными visual bindings.
3. Реальные `elevated-spike`, targets, zones, routes и toolbar commands на instance Map Page.
4. Access/Hub registration и публикация module route.
5. Отдельная пустая программируемая страница — только после того, как готовые page templates и их MCP-контуры отработаны.
+14 -2
View File
@@ -1,6 +1,6 @@
# NDC Module Studio
# NDC Module Foundry
Текущий living catalog эволюционирует в Module Studio без создания второго визуального приложения. Существующие разделы остаются `Visual Library`; рядом живут `Page Library` и `Applications`. Первый вертикальный срез не подключает Engine, Cesium, Authentik, Hub или Deploy.
Текущий living catalog эволюционирует в **NDC Module Foundry** без создания второго визуального приложения. Существующие разделы остаются `Visual Library`; рядом живут `Page Library` и `Applications`. Foundry — модульный контур для создания экземпляров готовых приложений, а `Design Guideline` остаётся каноническим языком и каталогом компонентов.
## Page Template contract v0.1
@@ -86,3 +86,15 @@ Visual Library использует отдельный Hub-style selector про
- `DELETE /api/applications/:id` с переносом draft в локальное deleted-storage.
Catalog server проверяет полный layout-контракт Design Profile и существование выбранной Application Manifest ссылки при создании и сохранении модуля. Опубликованные снимки лежат отдельно от draft-head в `runtime-data/design-profile-releases` и исключены из Git; в production этот lifecycle должен перейти в Platform `design-profile-core` без изменения публичного контракта.
## Baseline MCP
Foundry предоставляет базовый MCP-контур для уже существующего сквозного AI Workspace Assistant. Он не создаёт отдельную оркестрацию: штатный entitlement adapter выдаёт доступ к Foundry только авторизованному пользователю, после чего ассистент получает ограниченный набор инструментов.
- `Page Library` и канонические шаблоны доступны только на чтение;
- изменяются только экземпляры в `Applications`;
- доступны создание модуля, изменение его metadata, добавление экземпляра готовой страницы и upsert provider-neutral map pin bindings;
- удаление модуля через MCP намеренно отсутствует;
- каждая write-операция требует idempotency key и сохраняет аудит операции в persistent runtime store.
Полная конфигурация, endpoint и границы MCP описаны в [MCP контуре Module Foundry](MODULE_FOUNDRY_MCP.md).