From 31e078d6e52ce01b415ad3d41b5d29959020d295 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 17 Jul 2026 18:09:55 +0300 Subject: [PATCH] docs(platform): align connector, EDP, and deploy canon --- README.md | 2 +- docs/ADR_L2_OWNED_EXTERNAL_CONNECTORS.md | 404 ++++++++---------- docs/AI_WORKSPACE_ONTOLOGY_MCP.md | 16 +- infra/deploy-runner/README.md | 135 +++++- infra/synology/README.md | 74 +++- packages/external-provider-contract/README.md | 289 ++++++++----- .../providers/gelios/v1/README.md | 105 +++++ .../docs/GELIOS_DOMAIN_ONTOLOGY.md | 220 +++++++--- 8 files changed, 831 insertions(+), 414 deletions(-) create mode 100644 packages/external-provider-contract/providers/gelios/v1/README.md diff --git a/README.md b/README.md index 5e5f4e9..efdb43c 100644 --- a/README.md +++ b/README.md @@ -49,4 +49,4 @@ Ontology Core живёт в `services/ontology-core` как docs-first сема Map Gateway живёт в `services/map-gateway` как общий platform boundary для provider credentials, безопасного asset endpoint exchange, tile/3D Tiles proxy и persistent offline TileCache. Runtime cache не является source artifact и хранится в named volume/object storage, а не в Git. -Внешние бизнес-поставщики подключаются по `packages/external-provider-contract`: adapter конкретного API принадлежит изолированному NDC Agent L2 workflow, а Platform даёт один provider-neutral External Data Plane для raw retention, canonical facts, current/history projections и scoped read products. Connection instance несёт tenant scope, credential reference и collection profile, но не secret value. Provider/domain mapping, entity filtering и renderer logic не попадают в Platform Data Plane; writer capability привязана к immutable EDP binding и выдаётся только отдельному Engine provisioner, не shared internal bearer. Подробный канон — `docs/ADR_L2_OWNED_EXTERNAL_CONNECTORS.md`. +Внешние бизнес-поставщики подключаются по `packages/external-provider-contract`: adapter конкретного API принадлежит изолированному NDC L2 workflow, а Platform даёт один provider-neutral External Data Plane для raw retention, canonical facts, current/history projections и scoped read products. Connection instance несёт tenant scope, credential reference и collection profile, но не secret value. Provider/domain mapping, entity filtering и renderer logic не попадают в Platform Data Plane. Provider-issued credentials сохраняются в native NDC L2 Credentials; внутренний scoped publish grant генерируется и сохраняется generic control plane внутри native credential boundary, EDP получает только digest, а MCP видит только opaque reference/status. Legacy plaintext provisioning и managed digest-only ensure включаются независимо и по умолчанию закрыты. Подробный канон — `docs/ADR_L2_OWNED_EXTERNAL_CONNECTORS.md`. diff --git a/docs/ADR_L2_OWNED_EXTERNAL_CONNECTORS.md b/docs/ADR_L2_OWNED_EXTERNAL_CONNECTORS.md index 289dc7b..e0362c9 100644 --- a/docs/ADR_L2_OWNED_EXTERNAL_CONNECTORS.md +++ b/docs/ADR_L2_OWNED_EXTERNAL_CONNECTORS.md @@ -1,274 +1,238 @@ -# ADR: L2-owned external connectors and provider-neutral Data Plane +# ADR: внешние коннекторы принадлежат NDC L2 workflow Статус: **accepted**. -Дата: 2026-07-14. +Дата: 2026-07-16. Владелец решения: NODE.DC Platform. ## Контекст -NODE.DC — платформа разработки. Подключение внешнего API не должно создавать -ещё один provider-specific сервис, в котором навсегда зашиты логика клиента, -нормализация и правила отображения. Иначе каждый новый account или новый тип -объекта превращается в изменение Platform runtime. +NODE.DC состоит из двух разных слоёв: -Нужна одна повторяемая форма: Platform даёт безопасные границы, контракт -хранения и выдачи данных; изолированный workflow NDC Agent L2 является -адаптером конкретного API и остаётся редактируемым через предоставленный -Codex/Engine MCP. +- платформенный слой даёт NDC L1, NDC L2, Ontology, credentials, Data Products + и Foundry-boundaries; +- пользовательские автоматизации собирают из этих возможностей конкретную + бизнес-логику. -Это решение заменяет provider-adapter часть -`ADR_EXTERNAL_PROVIDER_DATA_PLANE.md` и все provider-specific runtime -предписания `ADR_GELIOS_DATA_PLANE.md`. Они сохраняются как исторические -черновики, но не являются каноном для новой реализации. +Новый account, provider credential, tenant, расписание или интерфейс не должны создавать +новый platform service и не должны требовать изменения NODE.DC source. Команда +NODE.DC подключается только для нового поставщика либо для расширения +подтверждённых capabilities и ontology уже поддержанного поставщика. + +Gelios — первый живой источник и acceptance-кейс этого канона, а не отдельная +архитектурная ветка. Документ заменяет provider-runtime решения из +`ADR_EXTERNAL_PROVIDER_DATA_PLANE.md` и `ADR_GELIOS_DATA_PLANE.md`; те +документы остаются только историей решения. ## Решение -### 1. Адаптер внешнего API принадлежит L2 +### 1. Пакет поставщика создаётся один раз -Один изолированный L2 workflow представляет одно connection instance и -является единственным местом для: +Каждый поддержанный поставщик получает один версионируемый **provider package**. +Он является знанием платформы о внешнем API и содержит: -- обращения к API поставщика через ссылку на credential из Engine; -- выбора read-capability, cursor/pagination, rate limit, retry и расписания; -- получения raw envelope, разбиения большого ответа на native batch-операции; -- семантического mapping к версии ontology и формирования canonical facts; -- idempotency key, watermark и lifecycle источника; -- deploy/run/execution/trace через Engine MCP. +- стабильный `providerId` и версии подтверждённого API; +- credential contract без значения секрета; +- каталог safe-read capabilities, pagination/rate-limit metadata и response + shapes; +- mappings из provider fields в версии Ontology; +- совместимые Data Products и scrubbed contract fixtures; +- явно отделённый каталог команд без активного command transport. -L2 не хранит значение provider secret или writer token: он использует только -Engine-bound credential references. Plaintext writer token выдаётся trusted -provisioner ровно один раз при create/rotate binding и затем хранится только в -Engine credential store, привязанном к этому L2; External Data Plane хранит -лишь его hash. Provisioner читает отдельный runner-owned secret file, который -read-only монтируется только в EDP и позднее в dedicated Engine provisioner, -работающий под выделенным UID/GID `11006`; это не shared `.env`, не -`NODEDC_INTERNAL_ACCESS_TOKEN` и не provider credential. До deployment этого -provisioner `EXTERNAL_DATA_PLANE_PROVISIONING_ENABLED=false`, поэтому issuance -routes fail closed. Token не попадает в L2 graph/profile, contract artifact, -execution log/trace, raw envelope, Foundry или UI. Command/write-capabilities -не получают transport route в read workflow. +Пакет не является runtime service, scheduler, базой данных или customer +configuration. Он расширяется только когда фактически подтверждён новый API, +новый тип данных или новая ontology revision. -Для новой учётной записи создаётся новый instance/profile этого workflow с -другой credential reference и connection configuration. Это не требует -изменения Platform source. Если поставщик раскрывает новый объект или поле, -сначала расширяется ontology/capability contract, затем изменяется именно -соответствующий L2 workflow. +Одна учётная запись поставщика задаётся данными, а не кодом: -### 2. Platform Data Plane нейтрален к provider и домену +`provider package + credential reference + connection profile + NDC L2 workflow instance` -Platform предоставляет один versioned **External Data Plane**. Он принимает -batch по внутреннему аутентифицированному контракту, хранит raw envelope с -retention/provenance, canonical facts, current/history projections и отдаёт -scoped data products/realtime updates. В нём запрещены: +Поэтому второй account или другая provider credential pair создаёт ещё один credential/profile и +workflow instance. Платформенный source, Data Plane и Foundry при этом не +меняются. -- ветвления по названию provider, customer, account, vehicle или renderer; -- provider field mapping, unit allowlist, business filtering и visual rules; -- provider token, прямой browser-to-provider доступ и command transport. +### 2. NDC L1 проектирует, NDC L2 workflow исполняет -Его canonical write-форма, которую Data Plane валидирует и сохраняет после -авторизации, содержит только нейтральные элементы: +NDC L1 получает пользовательское намерение, проверяет доступные capabilities и +Ontology, проектирует или изменяет разрешённый NDC L2 workflow через NDC MCP и +анализирует execution evidence. -```text -source: providerId + tenantId + connectionId -contract: contractVersion + ontologyRevision + dataProductId -batch: runId + sequence + idempotencyKey + receivedAt -raw: optional restricted ref + contentType + hash + retention policy -facts: stable sourceId + semanticType + observedAt + attributes + geometry? -``` +Один изолированный NDC L2 workflow представляет один connection instance и +владеет исполняемой механикой: -Для нового L2 writer-а это не wire-форма. Он вызывает -`POST /internal/data-plane/v1/data-products/:dataProductId/publish` с bearer -writer token и формой `nodedc.data-product.publish/v1`, в которой вообще нет -`source`, contract metadata, transport или persistence policy. Любой caller -scope и scope headers запрещены. +- safe-read вызовами provider API через credential reference; +- pagination, cursor, retry, rate limit, batch size и collection cadence; +- проверкой response shape и разбиением ответа на items; +- mapping к точной ontology revision; +- idempotency, watermark и публикацией canonical facts; +- private workflow state, когда он действительно нужен. -External Data Plane разрешает token в собственный immutable writer binding -`(tenantId, connectionId, providerId, allowedDataProductIds, expiresAt)`, -проверяет active/non-expired binding, совпадение `providerId` и разрешение -`dataProductId`, затем сам materializes canonical `source` с tenant/connection -и только после этого валидирует и сохраняет batch. Присланный caller scope -отклоняется, а не merge/override-ится. Изменение tenant, connection, provider, -allowed data products или срока требует нового binding; для существующего -binding допустимы только token rotation и revoke. +Provider-specific mapping сначала реализуется штатными workflow nodes и малым +Code-преобразователем. Это позволяет проверить реальный API без преждевременного +создания custom nodes. В custom NDC nodes переносится только повторившаяся и +доказанная **платформенная boundary-механика**, а не уникальная логика +поставщика. -Выпуск, rotation и revoke binding принимаются только от dedicated provisioning -principal с runner-owned secret file; он не монтируется в shared `.env` или L2. -Shared legacy bearer не может создавать writer capability. +### 3. Credentials являются данными connection instance -Идентификатор источника стабилен в пределах `(providerId, connectionId, -sourceId)`. Отсутствие объекта в очередном ответе помечается lifecycle-state -или временем последнего наблюдения; запись и её история не удаляются. +Gelios выдаёт ровно два provider secrets: access token и refresh token. Текущий +HTTP request binding `httpBearerAuth` использует access token. Автоматический +refresh в принятом runtime не доказан, поэтому provider package фиксирует его +как `operator_managed`; ни один из token values не попадает в graph, package, +Ontology, Data Plane, Foundry, execution logs, MCP или Ops. Новый account или +provider-issued token pair означает новые native credential records и +connection instance, но не новую Platform-сущность. -### 3. Граница данных и интерфейса +Локальное имя credential (например, с суффиксом `read access`) не является +provider scope. В target-контракте Read-классификацию задаёт allowlisted +method/endpoint в capability catalog и Engine workflow policy; тот же access +token нельзя называть отдельным «read token» или «write token» только из-за +label. Сейчас deployed Engine safe-ref policy ограничивает generic HTTP +credential только по host, но ещё не связывает его с package/version и exact +method/path. Поэтому Gelios `GET /api/v1/units` пока является декларативной +capability, а не завершённой runtime-security гарантией; canonical acceptance +требует capability-bound Engine policy и её MCP proof. -```text -Provider API (safe read) - -> Engine provider-credential reference - -> L2 connector instance (adapter + mapping + batches) - -> Engine writer-credential reference - -> External Data Plane Data Product publish (scope materialized server-side) - -> External Data Plane (generic persistence + projections) - -> scoped data product / realtime stream - -> Foundry binding / user interface -``` +Writer capability для публикации Data Product — отдельный внутренний native NDC +L2 credential, не третий Gelios token. Generic Engine/Platform control plane +генерирует capability внутри native credential boundary, сохраняет её там же и +передаёт EDP только SHA-256 digest вместе с точным provider, connection и +набором Data Products. Пользователь и MCP получают только opaque +reference/status. Capability не записывается в graph, provider package, env, +file, Ops или trace и не копируется через пользовательский UI. -Ontology Core описывает semantic types, связи, capability catalog и mapping -versions, но не хранит runtime payloads или секреты. Foundry получает только -approved data product и решает presentation: pins, visibility, layers and -filters. Оно не получает provider endpoint или credential. +Широкий credential sink, resolver/daemon с произвольной записью secret и любой +provider-specific credential service запрещены. Нужна одна узкая операция +`ensure data-product publish grant`: scope выводится из granted L1→L2 target, +зарегистрированного connection profile и разрешённого Data Product; issuance, +rotation и native binding должны быть idempotent, CAS/crash-safe и auditable. +Engine генерирует capability внутри credential boundary и передаёт EDP только +SHA-256 digest через idempotent binding key + generation; EDP не возвращает +plaintext. Новая generation создаётся до переключения node reference, а старая +отзывается только после успешного bind/acceptance. +Старые ручные EDP `POST/rotate` endpoints, которые возвращают capability один +раз, включаются отдельным legacy-флагом и не входят в canonical acceptance. +Digest-only managed endpoint имеет независимый флаг и принимает только +Ed25519-signed Engine service requests: exact audience/method/request target/raw +body hash входят в подпись, timestamp ограничен по skew, nonce защищён bounded +fail-closed replay cache. Legacy provisioner bearer на managed routes не +действует. EDP получает только deployment public key; matching private key +остаётся только внутри Engine server boundary. Endpoint остаётся выключенным, +пока key provisioning, Engine signer и exact native credential binding policy +не пройдут runtime acceptance. +В deployed Engine MCP этой операции пока нет — это текущий platform gap перед +canonical publish proof, а не действие пользователя. Это решение не заявляет +автоматический refresh Gelios: до отдельного runtime proof он остаётся +`operator_managed`. -### 4. Полнота без entity allowlist +### 4. External Data Plane нейтрален к поставщику -Collection profile выбирает разрешённые **read-capabilities**, а не список -конкретных объектов. Если выбранный read endpoint возвращает все доступные -источнику сущности, L2 передаёт все валидные элементы. Новые сущности -добавляются автоматически; скрытие/отображение — задача data product/UI, а не -сбора. Технические лимиты существуют только как размер batch, backpressure, -quota и защита от повреждённого ответа, но не как бизнес-фильтр по ID. +Platform предоставляет один versioned External Data Plane. Он принимает +canonical facts через scoped writer binding, хранит current/history +projections и публикует snapshot/patch contracts. В нём запрещены: -### 5. Масштабирование и native NDC Agent nodes +- ветвления по provider, customer, account, entity или renderer; +- provider field mapping и бизнес-фильтрация; +- provider credential, endpoint, schedule или command transport; +- caller-supplied tenant/connection scope. -L2 не запускает отдельный execution на каждую сущность. Он использует native -nodes NDC Agent: HTTP Request, Split Out, Split In Batches/Loop Over Items, -Aggregate, Postgres (когда нужен private workflow state), Set/If/Merge, -Schedule Trigger and Respond to Webhook. Code node допустим только как малый -преобразователь boundary-shape, когда эквивалентной native node нет; он не -становится скрытым сервисом или provider database. +`NDC Data Product Publish` отправляет только +`nodedc.data-product.publish/v1`. External Data Plane materializes immutable +scope из writer binding, проверяет разрешённый Data Product и его ontology +revision, затем сохраняет batch. -Shared telemetry/history не записывается L2 напрямую в физические таблицы -Platform Postgres: это создало бы coupling к schema. Direct Postgres допустим -для private state конкретного workflow или отдельной project-owned базы. -Общий контур использует только External Data Plane contract и bulk batches. +EDP runtime импортирует только package subpath +`@nodedc/external-provider-contract/data-plane`. Его deploy artifact и image +содержат только provider-neutral wire validators; `providers/gelios`, mappings, +fixtures и tests туда не входят. Изменение или добавление provider package не +пересобирает и не перезапускает EDP: каталог поставщиков разворачивается через +Engine/Ontology/control-plane путь отдельно. -### 5.1. Каталог custom nodes NODE.DC +Collection cadence, history cadence и presentation cadence независимы: -Встроенные узлы runtime сохраняют свои штатные названия (`HTTP Request`, -`Schedule Trigger`, `Loop Over Items` и т. п.). Любой узел, код которого -принадлежит NODE.DC, обязан одновременно выполнять три условия: +- NDC L2 забирает источник с частотой, нужной бизнес-задаче; +- current projection принимает каждое валидное изменение; +- declarative history policy может хранить все точки или sampling; +- Foundry читает snapshot+patch и отдельно ограничивает частоту render. -- видимое имя начинается с `NDC `; -- runtime type принадлежит package namespace `n8n-nodes-ndc.*`; -- package-level test отклоняет публикацию узла без этого префикса и namespace. +Для разной частоты БД и интерфейса не создаются второй provider sink, отдельный +gateway или параллельный прямой push в renderer. -Первый канонический набор состоит из: +### 5. Полнота означает все сущности разрешённой capability -- `NDC Data Product Publish` — runtime write boundary L2 → Data Plane; -- `NDC Data Product Read` — scoped current snapshot read по reader grant; -- `NDC Foundry Binding` — deploy/control-plane связь Data Product с - `Application → Page → typed slot`, но не транспорт каждого realtime tick. +Connection profile выбирает safe-read capabilities, а не зашитый в Platform +список entity IDs. Если разрешённый endpoint возвращает все доступные credential +сущности, NDC L2 обрабатывает каждый валидный item. Новый объект появляется +автоматически. -`NDC Foundry Output` не используется как техническое имя: оно ошибочно -подразумевает прямой transport L2 → renderer. Provider-specific adapter, -mapping и collection profile остаются логикой конкретного L2 workflow на -штатных nodes; они не оформляются как custom NODE.DC nodes и не добавляют -provider branch в Data Plane или Foundry. +Пользовательская фильтрация, слои и видимость находятся после сбора — в +automation/Data Product/Foundry. Технические ограничения допустимы только как +pagination, quota, batch size, backpressure и защита от повреждённого ответа. -`NDC Foundry Binding` отправляет только -`nodedc.foundry.binding-upsert/v1`. Её постоянный L2 credential — отдельный -revocable `ndc_fndbg_*` workload grant с allowlist точных -`Application → Page → Binding → Slot → Data Product` targets. Короткоживущая -`fnd1.*` capability интерактивного Foundry MCP, browser session, EDP -writer/reader grant и `NODEDC_INTERNAL_ACCESS_TOKEN` для этой ноды запрещены. +### 6. Custom NDC nodes ограничены платформенными границами -Runtime node не содержит URL, endpoint, provider ID, tenant ID, connection ID, -token, contract version или history cadence. Узел выбирает только разрешённый -Data Product; всё остальное materializes из opaque writer/reader binding и -immutable Data Product definition. +Первый канонический набор: -Package `n8n-nodes-ndc` устанавливается как штатный private community package в -`/home/node/.n8n/nodes/node_modules`, чтобы n8n `PackageDirectoryLoader` -сохранил namespace `n8n-nodes-ndc.*`. `~/.n8n/custom` и -`N8N_CUSTOM_EXTENSIONS` запрещены для этого пакета: они создают namespace -`CUSTOM.*`. Ручное копирование или `npm install` в живом container, patch -Engine core и подмена built-in node запрещены. Production использует -проверенный offline tarball, immutable release, atomic current/previous switch, -read-only mount и restart всех n8n execution processes; acceptance завершается -только когда Engine schema и MCP catalog возвращают точные package-qualified -types. +- `NDC Data Product Publish` — NDC L2 → External Data Plane; +- `NDC Data Product Read` — scoped snapshot/patch read; +- `NDC Foundry Binding` — control-plane связь Data Product с + `Application → Page → typed slot`. -### 5.2. Realtime delivery contract +Эти nodes не содержат provider ID, tenant ID, connection ID, endpoint, token, +mapping или cadence. Runtime package сохраняет технический namespace +`n8n-nodes-ndc.*`, но пользовательские документы и интерфейсы используют +только терминологию NDC. -Новый writer использует: +`NDC Foundry Binding` не транспортирует каждый realtime tick. Он создаёт +постоянную связь интерфейса с Data Product; Foundry затем читает его +snapshot+patch contract. -```text -POST /internal/data-plane/v1/data-products/:dataProductId/publish -Authorization: Bearer -``` +### 7. Реальный Gelios acceptance-кейс -Body имеет schema `nodedc.data-product.publish/v1` и содержит только batch -identity и canonical facts. Data Plane одной транзакцией: +Первая версия provider package должна доказать путь: -1. проверяет idempotency; -2. обновляет current projection только более новым или изменившимся фактом; -3. применяет declarative history policy (`none`, `all`, `sampled`); -4. добавляет changed facts в durable patch outbox; -5. фиксирует монотонный cursor. +`Gelios safe read → все доступные units → map.moving_object facts → +fleet.positions.current.v1 → Foundry map` -Reader grant получает snapshot `nodedc.data-product.snapshot/v1`, затем -подключается к SSE stream с `after=` или `Last-Event-ID`. -Каждый outbox event имеет schema `nodedc.data-product.patch/v1`. Если cursor -уже удалён retention policy, stream отвечает `409 resync_required`, и consumer -повторяет snapshot. Browser не получает Data Plane token или scope: Foundry BFF -разрешает persisted binding и читает server-only reader grant. +Canonical product использует ontology revision +`ontology.map.moving_object.v1` и только объявленные snake_case поля. NDC L2 +не передаёт caller scope и не собирает собственный batch envelope вокруг +`NDC Data Product Publish`. -`snapshot+patch` в v1 является **bounded product contract**. Current projection -одного scoped Data Product не может содержать больше 5000 entity keys -`(sourceId, semanticType)`. Snapshot возвращает целую согласованную projection -и один barrier cursor из одной `REPEATABLE READ` transaction. Query-параметр -`limit` — только защитный потолок, а не размер страницы: если полная projection -не помещается, Data Plane отвечает `413 data_product_snapshot_limit_exceeded` -и consumer не начинает patch stream с неполной базой. +Acceptance выполняется по порядку: -Наивная pagination current snapshot по `sourceId`, offset или независимо -полученным page cursors запрещена: изменения между страницами могут быть -пропущены или продублированы относительно patch cursor. Product с ожидаемой -cardinality выше 5000 обязан **до включения realtime** выбрать один из двух -отдельно версионируемых контрактов: +1. Platform выполняет generic `ensure data-product publish grant`, выпускает + scoped EDP binding и атомарно сохраняет capability в native NDC L2 + Credentials; +2. NDC MCP видит только совместимый opaque writer credential reference/status; +3. применить подтверждённый graph patch без legacy intake; +4. validate/preflight; +5. один успешный manual run и проверка execution/trace/Data Product; +6. только затем отдельным изменением добавить Schedule Trigger; +7. после доказанного snapshot+patch подключить Foundry binding. -- stable partitioning в несколько Data Products, где partition key является - частью definition, а каждый partition имеет собственный полный snapshot и - независимый patch cursor; -- новый query delivery contract с явно определёнными snapshot barrier, - continuation cursor, query scope и правилами перехода к patch stream. +### 8. Capacity и topology -Такой query contract не входит в `nodedc.data-product.snapshot/v1` и не может -быть имитирован полем `nextPageCursor`. До его отдельного утверждения runtime и -`NDC Data Product Read` работают только с bounded products до 5000 сущностей. - -### 6. Порядок первой реализации - -1. Версионировать нейтральный intake/read contract и поднять External Data - Plane без provider-specific logic. -2. Пересобрать существующий L2 proof в native-node pipeline: safe read → - split/batch → semantic mapping → generic append → safe summary. Никаких - provider ID filters и provider gateway endpoint. -3. Выполнить manual run, проверить execution/trace, idempotency and current - projection; только затем включить Schedule Trigger. -4. Provision immutable writer binding, one-time place its token into an opaque - Engine credential, then switch the L2 to - `/data-products/:dataProductId/publish` and remove caller scope/headers and - the shared legacy credential. -5. Подключить Foundry к data product current positions. История, геозоны и - новые сущности добавляются отдельными L2 collection profiles and ontology - revisions. +Количество одновременно активных NDC L2 проектов ограничивается фактическими +ресурсами железа и профилем нагрузки. Пока capacity проверяется оператором и не +автоматизируется. Канон не объявляет отдельные worker/webhook generations или +high-load topology, которых ещё нет в принятом runtime. ## Последствия -- `services/-gateway` не является шаблоном для новых интеграций. - Существующий experimental code не расширяется и не деплоится как часть этого - решения. -- Data Plane может иметь собственную БД/Timescale/PostGIS implementation, но - физическая схема остаётся внутренней деталью neutral service, а не API для - L2 или Foundry. -- Inline raw запрещён в L2 → Data Plane v1: до отдельного raw-vault допускается - только restricted ref/hash либо отсутствие raw envelope. Retention reference - вычисляется по серверному acceptance time, а не по timestamp из batch; Data - Plane выполняет bounded retention sweeps после готовности сервиса и по таймеру. -- Обычный Codex Desktop получает ровно те L2 grants, которые выданы владельцем - connection/workflow, и может безопасно развивать adapter только в этой - границе. -- `POST /internal/data-plane/v1/intake` остаётся временным migration-only route: - он выключен по умолчанию (`EXTERNAL_DATA_PLANE_LEGACY_INTAKE_ENABLED=false`), - требует `NODEDC_INTERNAL_ACCESS_TOKEN`, canonical scoped batch и совпадающие - scope headers. Новый или migrated L2 не получает этот shared token и не - использует fallback в legacy route. После миграции всех writers он удаляется. +- `services/-gateway` не является частью канона и не создаётся для + новых provider integrations. Существующий self-hosted Gelios Gateway и его + Timescale volume остаются frozen compatibility contour, воспроизводятся из + source/deploy и не изменяются новым L2 pilot без отдельного решения. +- Новый account или provider-issued access/refresh pair — configuration change, + а не platform release. +- Новый provider или неподдержанная capability — versioned provider/Ontology + change с contract tests. +- Существующий legacy L1/SDK workflow является frozen compatibility boundary: + новый L2 pilot его не редактирует, не отзывает его credential и не ставит его + вывод условием acceptance. +- Legacy `/internal/data-plane/v1/intake` остаётся выключенным migration-only + route и не используется новыми NDC L2 workflows. +- Команды устройствам остаются отдельным red-domain контуром с явным + подтверждением, scope, idempotency и аудитом. +- Foundry развивается после доказанного Data Product path; provider API и + credentials в Foundry не попадают. diff --git a/docs/AI_WORKSPACE_ONTOLOGY_MCP.md b/docs/AI_WORKSPACE_ONTOLOGY_MCP.md index c34f621..4535326 100644 --- a/docs/AI_WORKSPACE_ONTOLOGY_MCP.md +++ b/docs/AI_WORKSPACE_ONTOLOGY_MCP.md @@ -28,7 +28,7 @@ The worker receives only a pairing-bound Hub URL. The Hub verifies that the pair It intentionally does not expose filesystem paths, evidence ledgers, credentials, raw payloads, live telemetry, databases, command dispatch, workflow mutation or Studio controls. -## Boundary for Gelios and future data services +## Boundary for external providers and runtime data Ontology Core describes the canonical meanings and constraints: @@ -36,15 +36,21 @@ Ontology Core describes the canonical meanings and constraints: gelios.unit -> gelios.telemetry_snapshot -> gelios.position_fix -> map.moving_object ``` -It does **not** serve the Gelios database or provider API. The future Gelios Gateway/Data API is a distinct capability with its own scope checks, data contract and transport. That capability may later be supplied to an AI Workspace run as another dynamic MCP server. Ontology then gives the assistant the names, relations, guardrails and allowed data-contract route; the data capability enforces access and returns live data. +It does **not** serve provider runtime data or call a provider API. Provider +access and mapping belong to the granted NDC L2 workflow; shared current and +history products belong to the provider-neutral External Data Plane. A scoped +Data Product read capability may later be supplied to an AI Workspace run as a +separate dynamic MCP server. Ontology supplies meanings, relations, guardrails +and the allowed contract route; the data capability enforces access and returns +live data. This preserves one-way responsibility: - Ontology Core: semantics, contracts, aliases, guardrails and context advice. -- Gelios Gateway/Data API: access-scoped telemetry and history reads. +- External Data Plane: access-scoped Data Product snapshots and updates. - Command Gateway: separate red-domain command route, explicit confirmation and audit. -- Engine L2: workflow orchestration using granted capabilities. -- Studio: later presentation consumer, outside this implementation. +- NDC L2 workflow: provider access, collection and mapping using granted capabilities. +- Foundry: presentation consumer, outside this implementation. ## Live catalog updates without AI Workspace rule redeploy diff --git a/infra/deploy-runner/README.md b/infra/deploy-runner/README.md index 3a89d11..5f422d1 100644 --- a/infra/deploy-runner/README.md +++ b/infra/deploy-runner/README.md @@ -97,8 +97,15 @@ Engine-owned activator may select it after exact MCP schema acceptance. The paired Engine activation is built by `build-engine-n8n-private-extension-artifact.mjs`. It deliberately does not -copy the dirty Engine `docker-compose.yml` and does not build an image. Instead -it installs a narrowly scoped Compose override plus a strict transition +copy from or write generated files into the dirty Engine worktree, and it does +not build an image. A fresh transition id is mandatory and previously issued +ids are rejected: + +```bash +node infra/deploy-runner/build-engine-n8n-private-extension-artifact.mjs 20260717-004 +``` + +The builder emits a narrowly scoped Compose override plus a strict transition descriptor. On apply, the runner validates the staged release again, verifies that the running n8n container and the NAS-local `2.3.2` tag resolve to the same immutable image ID, and extracts the package into the root-owned, @@ -110,10 +117,15 @@ read-only Engine release tree: The override sets `N8N_USER_FOLDER=/home/node`, which is required because the actual Engine service runs as root while the canonical community package path -is below `/home/node/.n8n`. It enables loading but disables reinstall, mounts -only the exact release read-only, and uses both Compose `pull_policy: never` -and `docker compose up --pull never`. No registry access, lifecycle script, -database `installed_packages` row or custom-extension loader is involved. +is below `/home/node/.n8n`. The live runtime contract remains the successfully +deployed generation-003 contract and does not set `NODE_PATH`. Only the +runner's isolated `node -e` package-loader probe temporarily initializes the +dependency tree bundled inside the exact n8n base image; this reproduces n8n's +own loader without redefining the live Compose state. The override enables +loading but disables reinstall, mounts only the exact release read-only, and +uses both Compose `pull_policy: never` and `docker compose up --pull never`. +No registry access, lifecycle script, database `installed_packages` row or +custom-extension loader is involved. The runner pins the exact Engine service topology observed in source and rejects an added worker/webhook generation. Only the single actual `n8n` @@ -140,20 +152,106 @@ PYTHONDONTWRITEBYTECODE=1 \ ``` For `platform` artifacts, the allowlist includes the versioned Ontology Core, -the legacy Gelios experiment and the provider-neutral External Data Plane -sources. An External Data Plane artifact builds only its image and starts -`external-data-plane-postgres` plus `external-data-plane`; it never contains a -provider credential, provider endpoint, collection schedule or command -transport. Database credentials remain root-owned live `.env.synology` +the frozen legacy Gelios compatibility service, and the provider-neutral +External Data Plane sources. The Gelios service remains reproducible only to +protect its existing database/workflow; it is not a template for a provider +integration. An External Data Plane artifact builds only its image and +force-recreates only `external-data-plane`; the already healthy +`external-data-plane-postgres` container and its Timescale volume are an +independent deploy prerequisite and are never selected by an EDP application +artifact. The reviewed Compose source pins the Timescale image, named volume +and target, internal database-only network, absence of database host ports, +healthy dependency, localhost-only EDP bind and the two read-only +provisioner/trust mounts in the reviewed Compose source. The runner does not +reinterpret version-dependent `docker compose config` JSON as a second deploy +schema. Its canonical enforcement remains the artifact/path allowlist plus +hard-coded build command, selected service set, runtime-secret preparation and +health acceptance. Post-apply acceptance also requires +`database=ready`; a first-rollout failure removes only the candidate EDP +container without volumes and restores the source overlay. It never contains a +provider credential, provider endpoint, +collection schedule or command +transport. Its contract payload is an exact provider-neutral runtime subset; +`providers/*`, mappings, fixtures and tests are excluded and do not trigger an +EDP rebuild. Database credentials remain root-owned live `.env.synology` configuration and must not reuse `NODEDC_INTERNAL_ACCESS_TOKEN`. -The External Data Plane writer-provisioner credential is different: on the -first relevant Platform apply, the root-owned runner creates +On the first relevant Platform apply, the root-owned runner creates `/volume1/docker/nodedc-platform/secrets/external-data-plane-provisioner/token` atomically in a dedicated UID/GID `11006` directory (directory `0500`, token -`0400`). It is never an `.env` value or an artifact member; Compose mounts it -read-only only into External Data Plane and, when implemented, its dedicated -Engine provisioner running under the same restricted identity. +`0400`). It is never an `.env` value or an artifact member and is mounted +read-only only into External Data Plane. + +Manual one-time binding issuance and digest-only managed writer ensure are +independently disabled by default. The target control-plane operation generates +and stores the capability inside native NDC L2 Credentials and sends only its +digest to EDP; users and MCP consumers receive only an opaque compatible +reference/status. The runner-owned bearer above authenticates only legacy +plaintext issuance and is intentionally rejected by managed ensure/revoke. +Managed requests use a deployment Ed25519 Engine service key; EDP mounts only +the public-key trust directory read-only. On every reviewed Engine apply and on +an EDP runtime apply, this runner creates or validates one matching Ed25519 pair: +the Engine-only private key is `root:root 0400`, while the EDP trust copy is +`root:11006 0440`. Public-only crash state, key mismatch, a non-Ed25519 key, +symlinks and permissive modes fail closed. `plan` discloses both paths without +printing key material. The private key must not be broadened into an L2 graph, +MCP surface, artifact or shared-token boundary. + +The reviewed Engine source candidate has dedicated server-derived MCP +plan/apply handling for the exact `ndcDataProductWriterApi` + Data Product +Publish tuple. It is separate from the generic HTTP safe-ref path and accepts no +caller-provided provider/scope/credential identity, capability, generation or +service URL. The production managed flag remains false during staging. In the +explicitly confirmed activation window it is set to true immediately before +the Platform EDP artifact; runner acceptance then requires EDP `/healthz` to +report managed provisioning `enabled`. At that point the old Engine still has +no signer mount, so the endpoint remains usable only after the separately +accepted Engine artifact. Root/UI transfer is emergency +self-hosted diagnostics only, not the user journey or acceptance +path. A provider-specific daemon remains forbidden. + +Build the narrow Engine source artifact with: + +```bash +node infra/deploy-runner/build-engine-data-product-publish-grant-artifact.mjs \ + engine-data-product-publish-grant-YYYYMMDD-NNN +``` + +The builder includes exactly the pinned provider-security catalog, the +Publish-grant service, Engine Agent scope/gateway wiring, the existing n8n +adapter and the reviewed additive backend runtime overlay. It deliberately +excludes base Compose, frontend/dist, runtime data, tests, native credentials, +the NDC L2 process and the legacy generic credential-sink route/core. The +runner fixes the runtime action to `nodedc-backend` with `--no-deps` and +`--pull never`; n8n, nginx app, database services and volumes are not selected. +Acceptance requires backend health, the active immutable backend identity and +the exact additive mount inventory. Any failure restores the touched source and +recreates only the previous verified backend runtime. + +Existing Engine Agents created before Publish-grant support store the original +nine scopes as the complete developer profile. They must not require a new +agent, setup command or device credential when the profile gains a server-owned +capability. Build the compatibility artifact that introduces the durable named +`full-developer` profile with: + +```bash +node infra/deploy-runner/build-engine-agent-full-grant-migration-artifact.mjs \ + engine-agent-full-grant-migration-YYYYMMDD-NNN +``` + +This follow-up slice contains exactly +`nodedc-source/server/engineAgents/store.js`. The runner pins its predecessor to +the successfully applied Publish generation, requires the installed Publish +overlay and active immutable backend, recreates only `nodedc-backend`, and +proves the exact candidate SHA, named profile and current expanded runtime +scope view. Store schema v1 is migrated atomically to v2 only when a grant +contains the complete legacy nine-scope developer bundle. From then on the +profile name is the authorization authority and its scope list is derived on +every read, so capabilities deliberately added to `full-developer` immediately +apply to existing full grants without token or store migrations. Partial grants +become `custom` and remain exact; they are never elevated. The artifact does not +touch UI, n8n, credentials, agent tokens, workflow graphs, databases or runtime +payloads. `module-foundry` is an independent, authenticated application component. Its artifact contains source and compose infrastructure only; its live @@ -190,12 +288,17 @@ verified; it does not alter NAS routes, VPN, DNS, or Tailscale. Install or update the root-owned live runner on Synology: ```bash +# First verify that no deploy process is active and state/deploy.lock is absent. sudo install -o root -g root -m 0755 \ /volume1/docker/nodedc-deploy/runner-install/nodedc-deploy \ /usr/local/sbin/nodedc-deploy sudo /usr/local/sbin/nodedc-deploy verify-install ``` +Runner promotion is a standalone admin step, never an app-overlay artifact and +never part of a running apply. A Python process already executing the old file +keeps the old code in memory; always invoke a fresh verified process afterward. + Normal service deploys must still use explicit artifacts: ```bash diff --git a/infra/synology/README.md b/infra/synology/README.md index 2609ef8..df344f1 100644 --- a/infra/synology/README.md +++ b/infra/synology/README.md @@ -202,7 +202,13 @@ TASKER_SYNC_SOURCE=1 ./infra/synology/deploy-current.sh Если emergency-fix был сделан прямо на Synology в этих env-файлах, перенести sanitized-значение в `.env.synology.example`/docs, а секрет оставить только в live env. -### Gelios: сбор всех units доверенного подключения +### Gelios: frozen legacy compatibility contour + +`gelios-postgres` и `gelios-gateway` — уже существующий self-hosted контур с +собственным persistent Timescale volume. Он остаётся полностью воспроизводимым +в source/deploy, но не развивается и не используется как шаблон для новых +провайдеров. Его удаление или миграция требуют отдельного подтверждённого плана; +обычный Platform/EDP deploy не удаляет volume и не изменяет старый workflow. `GELIOS_UNIT_SCOPE=all` означает **все текущие и будущие units только одной уже настроенной пары `GELIOS_TENANT_ID` + `GELIOS_CONNECTION_ID`**. Это не wildcard @@ -210,17 +216,71 @@ TASKER_SYNC_SOURCE=1 ./infra/synology/deploy-current.sh Пропавший из очередного ответа unit не удаляется: его stable provider ID и `last_seen_at` сохраняются; видимость на карте — отдельная логика витрины. -Перед каноническим `nodedc-deploy apply` включить политику на Synology (скрипт -создаёт backup и не выводит секреты): +Перед узким legacy apply включить политику на Synology (скрипт создаёт backup и +не выводит секреты): ```bash sudo bash /volume1/docker/nodedc-deploy/inbox/prepare-gelios-all-units-env.sh ``` -Затем применить узкий Platform-артефакт, собранный с `--gateway-only`, через -`nodedc-deploy`. В его plan должны быть только `gelios-postgres` и -`gelios-gateway`; общий `docker-compose` в такой архив не входит. После apply -Gateway начинает принимать весь состав units этого подключения. +В plan такого Platform-артефакта должны быть только `gelios-postgres` и +`gelios-gateway`; общий compose в узкий архив не входит. + +## EDP publish grant: platform-owned credential + +`NDC Data Product Writer API` — историческое имя native credential type для +внутреннего scoped EDP publish grant. Это не Gelios access/refresh token и не +право записи в Gelios. + +Целевой путь полностью служебный: control plane NDC по разрешённому connection +profile генерирует capability внутри native NDC L2 Credentials, передаёт EDP +только digest для scoped binding и возвращает MCP только opaque compatible +reference/status. +Пользователь не читает, не копирует и не вводит этот внутренний секрет. + +EDP уже имеет localhost-only digest-only managed endpoint. Managed endpoint и +legacy plaintext provisioning имеют раздельные флаги и по умолчанию выключены. +Runner-owned provisioner bearer относится только к legacy plaintext routes и +на managed ensure/revoke намеренно не действует. Managed caller contract — +Ed25519-signed Engine service request с exact audience, method, request target, +raw-body SHA-256, timestamp и одноразовым nonce; EDP хранит только public key и +fail-closed replay cache. Ни bearer, ни signing private key нельзя открывать +workflow, MCP или переиспользовать как shared credential. В source-кандидате +Engine добавлен отдельный server-derived plan/apply для exact пары +`ndcDataProductWriterApi` + custom Publish node; он не расширяет generic HTTP +safe-ref policy и не принимает provider/scope/credential identity, capability, +generation или service URL от MCP. До отдельного Engine deploy и runtime proof +текущий deployed MCP этого контракта не имеет. Поэтому отсутствие +совместимого reference — платформенный runtime gap, а не действие пользователя. +Ручной перенос capability из отдельно включаемого legacy endpoint через root/UI допустим только как аварийная +диагностика self-hosted контура; он не является acceptance-путём и не должен +закрепляться в пользовательской автоматизации. + +Compose монтирует read-only trust directory +`/volume1/docker/nodedc-platform/trust/engine-managed-provisioner` в EDP; в нём +ожидается один regular, non-symlink файл `public-key.pem` с Ed25519 SPKI public +key. Файл не должен быть group/world-writable и должен читаться runtime UID +`11006`. Private key в этот каталог, Platform artifact, `.env` или EDP +контейнер не попадает. Canonical deploy runner теперь атомарно создаёт или +проверяет matching Ed25519 pair перед relevant Engine/EDP Compose apply: private +key остаётся `root:root 0400`, public trust — `root:11006 0440`; public-only +crash state, mismatch, symlink и неверный тип ключа отклоняются до запуска. +`EXTERNAL_DATA_PLANE_MANAGED_PROVISIONING_ENABLED` остаётся `false` на staging. +В отдельно подтверждённом activation window его переводят в `true` +непосредственно перед Platform EDP artifact; runner принимает deploy только +если `/healthz` явно вернул managed status `enabled`. Старый Engine в этот +момент ещё не имеет signer mount, поэтому usable caller появляется лишь после +следующего отдельно принятого Engine artifact. +EDP application artifact пересобирает и force-recreate только +`external-data-plane`. Уже работающий `external-data-plane-postgres` и его +Timescale volume runner не выбирает и не перезапускает; healthy database — +обязательная deploy prerequisite. +Canonical service/key/audience заданы в `.env.synology.example`; wire headers и +canonical signing payload описаны в `packages/external-provider-contract/README.md`. + +До появления opaque compatible reference и успешного manual publish proof +Schedule Trigger не включать. Frozen legacy workflow и его credentials этот +процесс не изменяет. ## AI Hub relay-only deploy diff --git a/packages/external-provider-contract/README.md b/packages/external-provider-contract/README.md index ea98ced..da91af8 100644 --- a/packages/external-provider-contract/README.md +++ b/packages/external-provider-contract/README.md @@ -5,7 +5,7 @@ исполняемый connector code. `src/index.mjs` даёт dependency-free проверку минимальных v1 contracts; она запускается через `npm run check`. -Каждый L2 connector instance должен поставлять совместимые versioned +Каждый NDC L2 connector instance должен поставлять совместимые versioned артефакты: ```text @@ -21,16 +21,47 @@ command-catalog (metadata only until a red command gateway is approved) ``` `provider-manifest` — декларативный template-артефакт: provider ID, ontology -revision, L2 template version, capability catalog и data-product IDs. Он не +revision, NDC L2 template version, capability catalog и data-product IDs. Он не является tenant connection, не содержит endpoint/URL, credential reference, secret, ручной scope или executable provider code. Concrete non-secret -connection profile принадлежит и версионируется вместе с конкретным L2 -workflow; Engine только привязывает к нему opaque credential reference и grant. +connection profile принадлежит и версионируется вместе с конкретным NDC L2 +workflow; caller привязывает только opaque provider credential reference, а +внутренний workload grant разрешает control plane. + +## Versioned provider packages + +`providers//` — устанавливаемая contract/data единица, а не +runtime service и не custom node. Она объединяет manifest, auth-mode metadata, +capability catalog, field policies, collection profiles, Data Product +definitions, semantic mapping contracts, provider-neutral NDC L2 template +descriptor и synthetic fixtures. Package не содержит tenant/account state, +credential values или исполняемый provider daemon. + +`instantiateL2Connection` создаёт произвольное число connection instances из +одного immutable package. Каждый instance получает собственные tenant, +connection и opaque provider credential reference, но не требует изменения +Platform source. Внутренние workload bindings объявляются отдельно как +`control_plane_managed` и не являются caller input. Канонический collection +scope — `all_visible_to_credential`: выбранная +read-capability передаёт все entities, которые provider возвращает для +привязанного credential. Unit allowlist и provider-group filter на collection +boundary запрещены; сортировка и видимость принадлежат Data Product consumer и +Foundry. + +Первый production-shaped package — `providers/gelios/v1`. Он фиксирует реальный +Gelios REST `GET /api/v1/units` transport contract и точный output +`fleet.positions.current.v1@1.0.0` с revision +`ontology.map.moving_object.v1`. Все Data Product fields используют snake_case. +Его optional `tokenLifecycle` точно описывает два provider-issued artifacts — +`access` и `refresh`: request использует `access`, а refresh пока имеет режим +`operator_managed`. Это metadata без secret values и без заявления о +реализованном автоматическом refresh. ## Проверяемые v1 contracts -- `Connection` — provider instance, tenant scope и *ссылка* на credential в - Engine. Любые token/secret/password-like поля запрещены. +- `Connection` — provider instance, tenant scope и *ссылка* на provider + credential в NDC L2 Credentials. Любые token/secret/password-like поля + запрещены; publisher представлен system-managed declaration/status без ref. - `Collection Profile` — явная policy сбора. `manual` не может скрыто содержать polling interval; `realtime` требует interval не чаще одного раза в секунду. - `Data Product` — нормализованный versioned output с semantic types, полями и @@ -38,18 +69,18 @@ workflow; Engine только привязывает к нему opaque credenti - `Intake Batch` — canonical **scoped** record, который External Data Plane валидирует и сохраняет: source, contract revision, idempotency, restricted raw envelope и canonical facts. Его `source` содержит `providerId`, - `tenantId` и `connectionId`. Writer-bound L2 request намеренно не является + `tenantId` и `connectionId`. Writer-bound NDC L2 request намеренно не является готовым `Intake Batch`: Data Plane сначала materializes scope и лишь затем применяет этот contract. В canonical record нет provider field mapping, entity allowlist, token или renderer data. Inline `raw.payload` в v1 запрещён: если нужна provenance-ссылка, connector передаёт restricted `raw.ref` вместе с hash. Отдельный raw-vault может быть добавлен только - отдельным ADR и не становится частью L2 → Data Plane wire boundary. + отдельным ADR и не становится частью NDC L2 → Data Plane wire boundary. - `NDC Foundry Binding` — адресует data product только в конкретную цепочку `Foundry Application → Page → approved slot`; `templateId` можно сохранить как дополнительную типизацию, но он не заменяет `applicationId` и `pageId`. В binding запрещены provider transport, endpoint и credential reference. -- `Provider Manifest` — статическое описание L2 connector template, capability +- `Provider Manifest` — статическое описание NDC L2 connector template, capability catalog и ontology/data-product contracts. Оно не может содержать tenant, connection, credential, secret или provider transport. @@ -94,13 +125,14 @@ PostgreSQL `integer` `0..2147483647`. Эти ограничения нельзя Manifest, connection/collection profiles, data-product definition и Foundry binding используют fail-closed allowed-key schemas. Неизвестные поля, а также -secret-like имена или значения (включая `ndc_edpwb_`/`ndc_edprb_`) отклоняются +secret-like имена или значения (включая `ndc_edpwb_`, `ndc_edprb_` и +`ndc_edppr_`) отклоняются на общей границе. Все private custom nodes NODE.DC поставляются package `platform/packages/n8n-nodes-ndc`. Их display name обязан начинаться с `NDC `, а runtime type — с `n8n-nodes-ndc.`; package называется строго -`n8n-nodes-ndc`. Provider-specific adapters остаются L2 workflow logic и не +`n8n-nodes-ndc`. Provider-specific adapters остаются NDC L2 workflow logic и не становятся custom nodes или ветками Data Plane. Эти инварианты проверяются package test. @@ -110,76 +142,72 @@ declarative provider artifact и не runtime transport: команда соде application/page/binding/data-product projection и idempotency key, а право на операцию извлекается Foundry из отдельного opaque workload grant. -## Engine opaque credential sink +## Native NDC L2 Credentials -`src/engine-credential-sink.mjs` задаёт dependency-free server-to-server v1 -границу для доставки трёх workload capabilities в Engine Credentials: +Provider auth и внутренние workload capabilities используют один общий +секретный boundary — native Credentials NDC L2, — но являются разными +credential domains. После сохранения ядро владеет secret, а graph и MCP +используют только opaque reference. -- `external-data-plane.writer` → точная нода - `n8n-nodes-ndc.ndcDataProductPublish` / `ndcDataProductWriterApi`; -- `external-data-plane.reader` → точная нода - `n8n-nodes-ndc.ndcDataProductRead` / `ndcDataProductReaderApi`; -- `foundry.binding` → точная нода - `n8n-nodes-ndc.ndcFoundryBinding` / `ndcFoundryBindingApi`. +Gelios выдаёт ровно access token и refresh token. Текущий credential type +`httpBearerAuth` использует access token для HTTP request. Автоматический обмен +refresh → access в текущем runtime не доказан и поэтому не заявлен: package +фиксирует `refreshMode: operator_managed`. Название credential с текстом вроде +`read access` является лишь локальной меткой; read-классификацию задают +разрешённые endpoint/method в capability catalog и workflow policy, а не scope +самого access token. В deployed Engine exact method/path policy ещё не +подключена: текущая HTTP safe-ref граница проверяет host. Поэтому catalog +classification остаётся декларативной до capability-bound Engine/MCP proof. -Provision request фиксирует `workflowId`, `workflowRevision`, `nodeId`, runtime -node type, credential type, grant ID, expiry и issuer policy hash. Aggregate -`transaction.policyHash` детерминированно считается по всему secret-free -descriptor; подмена любой цели или policy ломает валидацию. Единственное поле, -которое переносит plaintext capability, — `bindings[].material.value`; request -нельзя писать в логи, traces, очередь или audit. +Для общего Data Product transport используются `ndcDataProductWriterApi`, +`ndcDataProductReaderApi` и `ndcFoundryBindingApi`. Это внутренние NDC +capabilities, не Gelios tokens и не часть Gelios token lifecycle. Их значения +не являются частью provider package и не передаются между workflow nodes как +данные. -Provision и rollback envelopes действуют не более 15 минут: sink отклоняет -истёкшие запросы и допускает максимум 60 секунд положительного clock skew. -Receipt повторно проверяет freshness относительно собственного `processedAt`, -чтобы старый запрос нельзя было применить или подтвердить через replay. +Connection caller передаёт только provider credential reference. Publisher +role остаётся credential binding для `NDC Data Product Publish`, но template +маркирует его `management: control_plane_managed`: ни writer secret, ни writer +reference не входят в connection parameters. Instantiation возвращает +декларативный `systemBindings.publisher` с desired state и unresolved status; +дальше его разрешает trusted control plane. -Каждый binding содержит `capabilityDigest = sha256(material.value)`: Engine -самостоятельно хеширует полученный plaintext и сравнивает digest. Aggregate -`policyHash` включает этот digest и issuer identity, а provision transaction -несёт обязательную Ed25519 attestation. Engine принимает её только по -allowlisted `serviceId:keyId`; заменить capability и пересчитать обычный hash -без приватного issuer key невозможно. +Canonical managed EDP writer capability генерируется и сохраняется внутри +Engine credential boundary, а EDP получает только digest. Старые manual +writer/reader и Foundry issuance paths могут возвращать capability один раз +trusted control-plane caller и остаются отдельно закрытыми compatibility +границами; MCP и пользователь получают только opaque reference/status. +Запрещены password input +как пользовательский acceptance-путь, graph parameters, connection/profile +files, env, Ops, logs и traces. -Sink обязан выполнять `rollback-all`: сначала проверить весь request и точное -состояние graph, затем создать credentials в staging, атомарно привязать весь -набор и только после commit вернуть opaque `credentialRef`. При любой ошибке -новые credentials удаляются, а прежние bindings остаются без изменений. -`rollback-failed` означает карантин и ручное восстановление, но никогда не -возвращает частичные credential refs. +Broad credential sink/resolver/daemon и provider-specific provisioning +запрещены. Требуется узкий generic `ensure data-product publish grant` contract +с server-derived scope, idempotency, CAS/crash recovery и audit. Текущий +deployed Engine MCP этой операции ещё не имеет; root/UI transfer допустим +только как emergency self-hosted diagnostics, не как user journey. -Receipt, rollback request/receipt и audit имеют отдельные strict schemas. Они -не способны вернуть capability material; audit хранит только hash opaque -credential reference. Explicit rollback адресует committed transaction через -`transactionId`, `policyHash` и hash committed receipt, поэтому не может -случайно откатить другой набор. Реализация sink принадлежит Engine и не даёт -Platform/Codex доступа к Engine core, runtime files или plaintext credential -storage. - -## Engine private-extension management +## NDC L2 private-extension management `src/engine-private-extension.mjs` задаёт строгую Platform-side границу для -Engine-owned активации проверенного `n8n-nodes-ndc` release. Контракт не -устанавливает package и не меняет Engine: он фиксирует async +NDC L2-owned активации проверенного `n8n-nodes-ndc` release. Контракт не +устанавливает package и не меняет NDC L2: он фиксирует async `plan -> apply receipt -> operation/status` protocol, где `apply` обязан быстро вернуть `queued` и `operationId`, а долгий recreate/acceptance отслеживается отдельно. Активация и rollback требуют отдельной глобальной capability -`engine.private-extension.manage`. Обычные L1/L2 grants её не дают. Чтение +`engine.private-extension.manage`. Обычные NDC L1/NDC L2 grants её не дают. Чтение состояния допускает `engine.private-extension.read` или manage-capability. Запрос выбирает только allowlisted package, digest-bound `releaseId` и `packageSha256`; caller не передаёт host path, package bytes, Compose service, shell command или credential material. Plan живёт не более 15 минут, является single-use и применяется только с тем же idempotency key и plan hash. -Runtime transition для n8n 2.3.2 зафиксирован как community-package loader по -`/home/node/.n8n/nodes/node_modules/n8n-nodes-ndc`, а не как -`N8N_CUSTOM_EXTENSIONS` или `CUSTOM.*` loader: +Runtime transition использует platform-managed community-package loader для +immutable `n8n-nodes-ndc` release; private runtime paths и environment settings +не являются частью публичного provider contract: -- `N8N_COMMUNITY_PACKAGES_ENABLED=true`, - `N8N_COMMUNITY_PACKAGES_PREVENT_LOADING=false`, - `N8N_REINSTALL_MISSING_PACKAGES=false`; - Deploy/Run quiesced and execution queue drained before the version switch; - read-only mount and atomic current/recovery state; - force-recreate main, every worker and every webhook instance as one version @@ -189,24 +217,25 @@ Runtime transition для n8n 2.3.2 зафиксирован как community-pa Любая ошибка после switch запускает automatic rollback и повторный force-recreate/acceptance. Ошибка самого rollback переводит runtime в -`quarantined`. Immutable release и существующие Engine Credentials +`quarantined`. Immutable release и существующие NDC L2 Credentials сохраняются. Для первой активации предыдущим проверенным состоянием является `n8n-nodes-ndc.inactive/v1`: rollback в этот baseline удаляет package из loader surface, но не удаляет credentials. Public Ops gateway уже умеет прозрачно передавать эти операции через -`/engine/mcp`, если Engine реализует соответствующие MCP tools. Так как +`/engine/mcp`, если NDC L2 реализует соответствующие MCP tools. Так как gateway имеет 30-second upstream timeout, side effect остаётся асинхронным; отдельный public REST proxy для management boundary не требуется. -Пример `examples/gelios-positions-current.v1.mjs` — provider-specific fixture -без customer, tenant identity или credential material. +`providers/gelios/v1/fixtures` содержит synthetic provider response и ожидаемый +publish contract без customer, tenant identity или credential material. ## Ownership - `platform/packages/external-provider-contract` — общий контракт и schemas. -- NDC Agent L2 — provider API adapter: fetch, pagination, batching, - semantic mapping, collection profile и ссылка на credential в Engine. +- NDC L2 — provider API adapter: fetch, pagination, batching, + semantic mapping, collection profile и ссылка на credential в NDC L2 + Credentials. - Platform External Data Plane — provider-neutral intake, raw retention, canonical facts, current/history projections и scoped read products. Он не знает provider fields, customer/business filters или renderer rules. @@ -222,46 +251,100 @@ gateway имеет 30-second upstream timeout, side effect остаётся ас ## Mandatory connection boundary `connection` принадлежит одному tenant/client context и содержит только -ссылку на секрет, утверждённый capability scope, collection profile, field -policy и retention policy. Во всех runtime records Data Plane сохраняет минимум +ссылку на provider secret, утверждённый capability scope, collection profile, +field policy и retention policy. Во всех runtime records Data Plane сохраняет минимум `tenant_id`, `connection_id`, `provider_id`, `observed_at`, `received_at` и provenance/version там, где это применимо. -Writer token — отдельный EDP runtime credential, а не поле `Connection`, -profile, provider manifest или L2 graph. +Provider credential reference текущего Gelios request адресует access token; +наличие provider-issued refresh token описывается `tokenLifecycle`, но его value +не входит в `Connection`. Writer capability — отдельный внутренний EDP runtime +credential, а не Gelios token и не caller-supplied поле `Connection`, profile, +provider manifest или NDC L2 graph. Connection artifact содержит только +system-managed publisher declaration/status без credential reference. ## Scoped writer binding Writer binding — EDP-owned runtime security state, а не versioned artifact provider manifest или connection profile. Он фиксирует `tenantId`, `connectionId`, `providerId`, `allowedDataProductIds`, active/revoked state и -`expiresAt`. L2 не может редактировать binding или задавать его scope; +`expiresAt`. NDC L2 не может редактировать binding или задавать его scope; изменение любого scope-поля либо TTL создаёт новый binding, а прежний binding можно только rotate/revoke. -Новый L2 вызывает `POST /internal/data-plane/v1/intake/writer-bound` с -`Authorization: Bearer ` и unscoped envelope. В `source` -разрешён только `providerId`; `tenantId`, `connectionId` и -`x-nodedc-*-id` headers запрещены. EDP проверяет token, binding, provider и -`contract.dataProductId`, затем materializes immutable canonical scope и -сохраняет обычный `Intake Batch`. Caller-provided scope отклоняется, а не -доверяется и не объединяется с binding. +Binding создаёт, сохраняет и привязывает только trusted control plane. Caller +connection instance не принимает `writerCredentialRef`; publisher role в L2 +template является обязательным `control_plane_managed` binding requirement. -Plaintext writer token возвращается только trusted provisioner при создании -или rotation. Provisioner помещает его непосредственно в opaque Engine -credential, доступный назначенному L2. EDP хранит только hash token и binding -metadata; token запрещён в connection/profile/manifest, L2 graph, logs/traces, -raw payload, Foundry и UI. +Новый NDC L2 вызывает +`POST /internal/data-plane/v1/data-products/:dataProductId/publish` с opaque +writer capability и envelope `nodedc.data-product.publish/v1`. В body есть +только batch identity и canonical facts; `source`, provider, tenant, +connection, ontology revision, product version и persistence policy запрещены. +EDP проверяет binding и path `dataProductId`, materializes immutable scope и +contract из server-owned registries, затем сохраняет canonical batch. +Caller-provided scope отклоняется, а не доверяется и не объединяется с +binding. -Provisioning routes принимают только отдельный secret file, созданный -root-owned deploy runner: -`/volume1/docker/nodedc-platform/secrets/external-data-plane-provisioner/token`. -Он read-only монтируется только в EDP и позднее — в dedicated Engine -provisioner, работающий под выделенным UID/GID `11006`; это не `.env` value, -не `NODEDC_INTERNAL_ACCESS_TOKEN` и не provider credential. Пока generic Engine -provisioner не развёрнут, `EXTERNAL_DATA_PLANE_PROVISIONING_ENABLED=false`, а -create/rotate/revoke routes отвечают `503` и не делают fallback к shared -internal bearer. +Plaintext writer capability запрещена в provider package, connection/profile, +NDC L2 graph, files, environment, logs/traces, raw payload, Foundry и MCP. +В canonical пути она генерируется и сохраняется внутри native Engine credential +boundary; EDP получает только SHA-256 digest, а workflow — только opaque +reference/status. Старые ручные EDP create/rotate endpoints с one-time +plaintext response остаются отдельно выключенным compatibility-механизмом и не +являются пользовательским или acceptance-путём. + +Legacy manual one-time issuance использует отдельный EDP-only runner-managed +bearer. Digest-only managed ensure этот bearer не принимает: Engine подписывает +каждый control-plane запрос отдельным Ed25519 service key, EDP хранит только +публичный ключ. Оба пути имеют независимые флаги и по умолчанию выключены. +Generic `ensure data-product publish grant` генерирует и сохраняет capability в +native Engine credential boundary, передаёт EDP её digest и возвращает только +opaque ref/status. Shared internal bearer и Gelios access/refresh token для +этого запрещены. Deployed Engine MCP умеет читать +opaque refs и bind только HTTP Request credentials; native +`ndcDataProductWriterApi` к custom Publish node этим policy не привязывается. +Поэтому ensure + exact private-node bind остаются реальным platform gap. +Root/UI перенос — только аварийная диагностика. + +Control-plane EDP boundary — idempotent +`PUT /internal/data-plane/v1/writer-bindings/by-key/:bindingKey`. Engine создаёт +capability внутри native credential boundary и отправляет только её SHA-256 +`capabilityDigest`, immutable scope, `generation` и expiry. Одинаковый +`bindingKey + generation + request hash` возвращает тот же binding без нового +secret; несовпадающий retry получает `409`. Rotation использует новую +generation/credential, поэтому старый binding можно оставить активным до +успешного node bind и затем явно revoke. +Managed revoke выполняется идемпотентно по точным `bindingKey + generation` +через managed-only route; для него не открывается legacy plaintext API. +Managed route включается только через +`EXTERNAL_DATA_PLANE_MANAGED_PROVISIONING_ENABLED`; legacy plaintext routes — +через отдельный `EXTERNAL_DATA_PLANE_PROVISIONING_ENABLED`. + +Managed auth wire contract — +`nodedc.external-data-plane.managed-provisioner-request/v1`. Engine отправляет +ровно по одному header: +`x-nodedc-engine-service-id`, `x-nodedc-engine-key-id`, +`x-nodedc-request-audience`, `x-nodedc-request-timestamp`, +`x-nodedc-request-nonce`, `x-nodedc-content-sha256` и +`x-nodedc-request-signature`. Timestamp — canonical UTC ISO с миллисекундами, +nonce и signature — unpadded canonical base64url, body hash — lowercase hex +SHA-256 от точных переданных bytes. Подписываются UTF-8 bytes результата +`JSON.stringify` объекта с полями строго в порядке `schemaVersion`, `audience`, +`serviceId`, `keyId`, `method`, `path`, `timestamp`, `nonce`, `bodySha256`. +`method` и request target `path` (включая query) должны совпадать побайтно. +Canonical identity: service `nodedc-engine`, key +`engine-edp-managed-provisioner-v1`, audience +`nodedc-external-data-plane.managed-provisioning.v1`. EDP проверяет bounded +clock skew и одноразовый nonce в bounded fail-closed replay cache; bearer +fallback для managed routes отсутствует, любой `Authorization` header на них +отклоняется. + +EDP импортирует только `@nodedc/external-provider-contract/data-plane`. +Artifact/image allowlist для этого subpath содержит contract version, intake, +publish/snapshot/patch validators и scrub policy; provider packages, mappings, +fixtures и tests физически не входят в EDP runtime и не являются причиной его +restart. ## Legacy intake migration @@ -271,22 +354,22 @@ existing writers. Он принимает только canonical scoped `Intake `x-nodedc-tenant-id` и `x-nodedc-connection-id`. Writer token этот route не заменяет. -Для миграции: создать writer binding, один раз записать выданный token в Engine -credential, переключить L2 на `/intake/writer-bound`, удалить tenant/connection -из body и headers, проверить успешный intake, затем удалить у L2 legacy -credential. Новый или migrated writer не должен fallback-иться на legacy route -после ошибки writer-bound intake. Когда migrated все writers, legacy route и -его shared-token access удаляются. +Для миграции: Platform обеспечивает scoped writer binding и native credential, +затем через Engine MCP новый workflow branch переключается на Data Product +publish route, удаляет tenant/connection из body и headers и проверяет +успешный publish. Существующий legacy workflow/credential остаётся неизменным, +пока владелец отдельно не решит его вывести; новый writer не должен +fallback-иться на legacy route после ошибки publish. ## Intake boundary -L2 отправляет в общий Data Plane `Intake Batch`, а не SQL-запрос в общие +NDC L2 отправляет в общий Data Plane publish request, а не SQL-запрос в общие таблицы. Platform проверяет boundary и сохраняет raw/history/current -projections; semantic mapping остаётся в L2. Список конкретных source IDs не +projections; semantic mapping остаётся в NDC L2. Список конкретных source IDs не является частью connection scope: scope выбирает read-capability API, а visibility решает data-product consumer. -Inline `raw.payload` запрещён: L2 не может записать в Data Plane полный ответ +Inline `raw.payload` запрещён: NDC L2 не может записать в Data Plane полный ответ provider-а или произвольную строку. Пока отдельный raw-vault не утверждён, batch либо не содержит `raw`, либо содержит только restricted `raw.ref` и hash. Secret-like keys и распознаваемые bearer/JWT/writer-token values в любом участке @@ -299,4 +382,6 @@ service делает sweep expired envelopes при старте и по рас Capabilities классифицируются как `read`, `metadata`, `write`, `destructive` или `unknown`. Только `read` и согласованные `metadata` могут попасть в collector. `write` и `destructive` остаются каталогизированными, но не имеют -transport route в read adapter. +transport route в read adapter. Эта классификация относится к разрешённому +method/endpoint workflow, а не утверждает, что provider access token имеет +read-only scope. diff --git a/packages/external-provider-contract/providers/gelios/v1/README.md b/packages/external-provider-contract/providers/gelios/v1/README.md new file mode 100644 index 0000000..468a27a --- /dev/null +++ b/packages/external-provider-contract/providers/gelios/v1/README.md @@ -0,0 +1,105 @@ +# Gelios provider package v1 + +Это versioned contract/data package для первого живого provider connection в +NODE.DC. Он не является runtime service, custom node, account configuration или +хранилищем telemetry. Provider-specific request и mapping исполняются внутри +изолированного NDC L2 workflow; Platform получает только provider-neutral Data +Product. + +Package фиксирует: + +- manifest `gelios.provider.v1@1.0.0`; +- точный provider token lifecycle: Gelios выдаёт `access` и `refresh`, а + HTTP request использует `access`; +- safe-read capability `gelios.units.current.read` + (`GET https://api.geliospro.com/api/v1/units` with an opaque bearer + credential); +- dynamic scope `all_visible_to_credential` без unit/group allowlist; +- realtime profile с интервалом 10 секунд и manual profile; +- fail-closed field policy: неизвестные и dynamic fields отбрасываются до + классификации, hardware IDs, phone/address, raw params и sensor payloads не + публикуются; +- mapping в `map.moving_object`; +- стабильный source ID `gelios-unit-` с точным сохранением + строкового provider ID; +- explicit derivation rules для `position_valid`, `operational_status` и + `quality_flags`, включая `no_position` и stale threshold 60 секунд; +- точный Data Product `fleet.positions.current.v1@1.0.0`, revision + `ontology.map.moving_object.v1`, с 13 разрешёнными snake_case fields; +- provider-neutral NDC L2 template descriptor из пяти boundary steps: + trigger → request → extract → map → `NDC Data Product Publish`; +- publish credential binding с `management: control_plane_managed`, без + caller-supplied writer reference; +- synthetic source/publish fixture, включая видимый объект без координат. + +## Authentication boundary + +Gelios выдаёт ровно два provider secret artifact: access token и refresh +token. Это не отдельные read/write tokens. Текущий NDC L2 HTTP binding типа +`httpBearerAuth` подставляет access token в `Authorization`; автоматический +refresh этим runtime пока не доказан, поэтому package явно фиксирует +`refreshMode: operator_managed`. Ни access, ни refresh value не попадает в +package, graph, fixture, MCP, Ops или trace. + +Название native credential, например `Gelios — Robot2B — read access`, является +только операторской меткой. `read` в `gelios.units.current.read` — +классификация разрешённого workflow endpoint `GET /api/v1/units`, а не scope +access token. Метка credential не создаёт отдельный «read token» и не сужает +права, которые фактически выдал Gelios. + +Unit без last message не исчезает: mapping использует collection receive time +как `observedAt` fallback и публикует subject без geometry с +`position_valid=false`/`operational_status=no_position`. + +Префикс source ID для v1 — строго `gelios-unit-`. Сокращённый `unit-` не +является каноническим: он создал бы второй entity key рядом с уже сохранённым +Gelios subject. Значение provider ID после префикса преобразуется только в +строку; ведущие нули и остальные значимые символы не нормализуются. + +`fleet.positions.current.v1` ограничен 5000 current entities. Если credential +видит больше, NDC L2 не должен отбрасывать «лишние» units: package/profile +требует остановить Deploy до новой partitioned Data Product revision. Полное +автоматическое enforcement этого правила остаётся gate будущего +package-to-graph compiler/runtime validator. + +Один package обслуживает любое число accounts. Для каждого account вызывается +`instantiateL2Connection` с новым `tenantId`, `connectionId`, provider +credential reference. Это единственная credential reference, которую передаёт +caller: она адресует Gelios access binding в native NDC L2 Credentials. +Внутреннюю capability для публикации в External Data Plane выдаёт и сохраняет +control plane; caller не передаёт writer secret или `writerCredentialRef`. +Результат instantiation содержит только provider ref и декларативный +`systemBindings.publisher` со статусом `unresolved`, который Platform должна +разрешить в native credential binding. Эта capability не выдаётся Gelios, не +является третьим Gelios token и не описывает права provider account. Добавление +account не меняет Platform source. + +Расширение Gelios происходит новой версией package: сначала подтверждаются +provider capability/fields, затем обновляются ontology, field policy, mapping, +fixtures и tests. Пользовательские workflow, которые уже покрываются +существующим catalog, не требуют участия разработчика Platform. + +Технический runtime type publish boundary остаётся +`n8n-nodes-ndc.ndcDataProductPublish`; это идентификатор общего NDC package, а +не Gelios-specific node. + +## Текущая готовность + +Package, ontology links и synthetic fixtures проверены в source. Production +Engine L2 target использует зарегистрированный Gelios Bearer credential на +точном safe-read endpoint и managed `ndcDataProductWriterApi` generation g3; +оба binding записаны без раскрытия secret values. Canonical graph содержит +provider read, extraction, mapping и native `NDC Data Product Publish`. + +Fresh production execution `881722` завершился успешно и опубликовал +`fleet.positions.current.v1`; managed writer g3 принят и имеет state `current`. +Это доказывает manual provider read → canonical Data Product path. Внутренняя +writer capability остаётся NODE.DC capability и не может быть заменена access +или refresh token Gelios. + +Не доказаны: автоматический refresh, Schedule Trigger 10 секунд, несколько +последовательных scheduled cycles, retry/backoff, sampled history/retention на +continuous path и Foundry snapshot/patch → moving pins E2E. Provider package +остаётся declarative: MCP package discovery и package/profile → L2 graph +compiler пока отсутствуют, поэтому первый graph собран через общие Engine MCP +graph tools. diff --git a/services/ontology-core/docs/GELIOS_DOMAIN_ONTOLOGY.md b/services/ontology-core/docs/GELIOS_DOMAIN_ONTOLOGY.md index a36c5b9..b467885 100644 --- a/services/ontology-core/docs/GELIOS_DOMAIN_ONTOLOGY.md +++ b/services/ontology-core/docs/GELIOS_DOMAIN_ONTOLOGY.md @@ -2,108 +2,202 @@ Package: `catalog/domain-packages/gelios` -Status: `v0.1.0` source-evidenced and product-required slice for the Robot2B client context and the Gelios provider. +Status: `v1.0.0`, source-evidenced and product-required. ## Purpose and boundary -The package describes the meaning of Gelios fleet, telemetry, sensor, spatial, report and command concepts. It is not a Gelios client, token store, telemetry database, Cesium implementation or Engine workflow. +The package describes Gelios fleet, telemetry, sensor, spatial, report and +command meanings. It is not a provider client, credential store, runtime +database, renderer implementation, NDC L1 agent or NDC L2 workflow. -The practical audit established a wide REST read surface: 107 currently visible units, 105 with `lastMsg`, a visible administrative account, unit groups, telemetry variants, sensor catalogues, command-template catalogues, geozone groups and report templates. The REST OpenAPI documents 295 operations. This ontology therefore separates three things that must not be conflated: +Gelios is the first live provider used to prove the common provider-package +model. Runtime transport, collection cadence and semantic mapping belong to an +isolated NDC L2 connection instance. External Data Plane persists and delivers +provider-neutral Data Products. Ontology Core describes meaning only. -1. documented provider capability; -2. actual runtime read evidence for the current account; -3. approved Robot2B product scope. +The matching contract/data artifact is +`platform/packages/external-provider-contract/providers/gelios/v1`. Adding a +second account creates another connection instance with different opaque +credential references; it does not create another ontology package, Platform +service or custom node. -The 95-unit legacy Engine snapshot is a temporary compatibility reference only. It is not the final business scope. The provider's current groups also do not resolve that scope automatically. A Robot2B owner must approve an allowlist or equivalent rule represented by `gelios.access_scope`. +## Provider authentication boundary + +Gelios issues exactly two provider secret artifacts: an access token and a +refresh token. The current NDC L2 `httpBearerAuth` request binding uses the +access token. Automatic refresh has not been proven in the deployed runtime, so +the matching provider package records refresh as `operator_managed`; neither +token value belongs in Ontology, a workflow graph, MCP, Ops or a trace. + +A credential label such as `read access` is operator metadata, not a Gelios +token scope. `gelios.units.current.read` is classified as read because the +approved workflow transport is `GET /api/v1/units`. The label does not create a +separate read token or constrain other rights that Gelios may have granted to +the same access token. The scoped Data Product writer credential later in the +runtime path is an internal NDC/External Data Plane capability, not a third +Gelios token. + +## Dynamic connection scope + +`gelios.access_scope` is derived from two inputs on every collection run: + +1. safe-read capabilities selected in the connection profile; +2. entities currently visible to the bound provider credential. + +For `gelios.units.current.read`, NDC L2 processes every valid unit returned by +`GET /api/v1/units`. Static unit IDs, owner allowlists and provider-group business +filters are forbidden at collection boundary. New units visible to the same +credential appear automatically. Foundry and other Data Product consumers own +layer selection, sorting, filtering and visibility. + +Technical limits such as a 5000-fact batch ceiling, retry policy and response +validation protect runtime health; they are not business entity filters. ## Canonical value contracts ### Unit and device -`gelios.unit` is the stable business subject. The durable key is a namespaced provider identity such as `gelios.unit:`, never a renderer entity ID or a display name. A unit can use a `gelios.tracker_device`, belong to multiple `gelios.unit_group` records and expose sensor, fuel, maintenance and custom-field configurations. +`gelios.unit` is a stable provider subject. The published v1 source ID is +`gelios-unit-` inside immutable `(providerId, connectionId)` +scope; a renderer entity ID or display name is never identity. The provider ID +suffix is preserved as an exact string (including leading zeros) and is never +parsed or renumbered. A unit can use a +`gelios.tracker_device`, belong to multiple `gelios.unit_group` records and +expose sensor, fuel, maintenance and custom-field configurations. -Hardware IDs, IMEI, phones and decrypt-related fields are restricted or secret-class data. They are not part of the default Studio contract. +Hardware IDs, IMEI, phones, address, decrypt-related fields, raw `params` and +unclassified sensor payloads are excluded by the v1 field policy. New provider +fields remain dropped until they are evidenced, classified and introduced by a +new package/ontology revision. -### Current telemetry +### Current telemetry and position -`gelios.telemetry_snapshot` is the normalized current state produced by the Gateway, with at least: +`gelios.telemetry_snapshot` is normalized by the versioned NDC L2 mapping. +`gelios.position_fix` is its time-qualified spatial portion; it is not a pin or +other renderer object. + +The first approved output is exactly: ```text -unitSubjectId -observedAt -receivedAt -position: { latitude, longitude, height?, course?, speed?, satellites? } -operationalStatus -sensorReadings[] -counterValues[] -qualityFlags[] -source: gelios-rest +Data Product: fleet.positions.current.v1 +version: 1.0.0 +ontology revision: ontology.map.moving_object.v1 +semantic type: map.moving_object +delivery: snapshot+patch +history: sampled, latest entity per 60-second bucket, 90 days ``` -The source supports a raw and a pure last-message variant. Pure data may include sensors, counters and accumulated fields. Raw messages and dynamic `params` remain behind restricted policy and may be retained separately for diagnostics only. `gelios.position_fix` is the time-qualified position portion used by spatial consumers; it is not itself a pin. +Allowed Data Product fields are: + +```text +course_degrees +display_name +elevation_meters +geometry +hdop +horizontal_accuracy_meters +object_kind +operational_status +position_source +position_valid +quality_flags +satellite_count +speed_kph +``` + +All attribute names are snake_case. GeoJSON `geometry` is omitted when valid +coordinates are unavailable, but the unit remains in the product with +`position_valid=false`, `operational_status=no_position` and appropriate +`quality_flags`. If the provider has no last-message timestamp, the collection +receive time becomes the explicit `observedAt` fallback so the credential-visible +unit is not silently dropped. ### Sensors and operational semantics -`gelios.sensor_definition`, `gelios.sensor_conversion` and `gelios.sensor_reading` separate stable device configuration, calibration and time-series readings. Fuel profile, maintenance plan and custom field are independent configuration concepts. No assumed sensor meaning should be added to analytics until its message parameter, measure and conversion semantics are approved. +`gelios.sensor_definition`, `gelios.sensor_conversion` and +`gelios.sensor_reading` separate device configuration, calibration and +time-series values. Fuel profile, maintenance plan and custom field are +independent concepts. A sensor meaning cannot enter a Data Product until its +message parameter, unit, conversion, classification and field policy are +approved. -### Spatial binding and Cesium pins +### Spatial binding -Gelios is a source domain; Cesium is a renderer adapter. The binding is deliberately provider-neutral: +Gelios is a source domain. The provider-neutral relation is: ```text -gelios.unit - -> map.moving_object (stable spatial subject) -gelios.position_fix - -> map.moving_object (current position update) -map.moving_object - -> map.pin + map.label + map.visibility_rule - -> map.renderer_adapter (Cesium today, replaceable later) +gelios.unit + gelios.position_fix + -> map.moving_object + -> fleet.positions.current.v1 + -> Foundry Data Product binding + -> map layers, selection and telemetry panel ``` -The future map payload selects a stable `subjectId`, position, normalized operational status, observed time and approved label fields. It must not contain Gelios credentials, raw payloads or Cesium transient entity references. This is the replacement boundary for the current M-map node-to-Cesium coupling. - -The Map Page resolves the selected Studio profile to the shared map.pin contract. The initial accepted presentation is an elevated spike — ground anchor, stem, outlined head and label — but that visual configuration remains on the Studio side. Gelios publishes data for a stable subject; it never publishes Cesium primitive settings, pin pixels or a renderer entity ID. - -The proposed renderer-neutral payload is in [`examples/gelios-map-moving-object.fixture.json`](../examples/gelios-map-moving-object.fixture.json). It uses synthetic coordinates only: it specifies the interface between Gateway and Map View, not a live trike position and not a Cesium entity. +The publish boundary contains stable facts only. It cannot contain provider +credentials, tenant/connection claims supplied by the workflow, raw payloads, +pixel values, styles or transient renderer identities. The synthetic contract +fixture is +[`examples/gelios-map-moving-object.fixture.json`](../examples/gelios-map-moving-object.fixture.json). ### Commands -`gelios.command_template` and `gelios.command_group` are read-only catalogue concepts. `gelios.command_dispatch`, `gelios.command_delivery` and `gelios.command_audit` are a separate red domain. +`gelios.command_template` and `gelios.command_group` are read-only catalogue +concepts. `gelios.command_dispatch`, `gelios.command_delivery` and +`gelios.command_audit` are a separate red domain. -No collection run, workflow, map click or autonomous agent may create a dispatch. A future dispatch requires an explicit human action, approved scope, selected unit/group and template/parameters, confirmation, an audit record and delivery-state reconciliation. This ontology package does not grant or test a write permission. +No collection run, workflow, map click or autonomous agent may create a +dispatch. A future command path requires explicit human intent, confirmation, +separate authorization, immutable audit and delivery reconciliation. This +ontology and the Gelios v1 provider package expose no command transport. -## Future data plane - -The intended flow is: +## Canonical runtime flow ```text -Gelios REST OAuth - -> server-side Gelios Gateway - -> scope filter + field policy + normalizer - -> durable telemetry/configuration storage - -> NDC level-2 workflow / internal data contract - -> Map View binding / Cesium renderer adapter +Gelios safe-read API + -> opaque Gelios access credential in NDC L2 Credentials + -> isolated NDC L2 connection workflow + -> versioned semantic mapping + -> NDC Data Product Publish through a native scoped writer credential + -> External Data Plane + -> scoped snapshot + patch stream + -> Foundry Data Product binding ``` -`gelios.collection_run` and `gelios.ingestion_cursor` define collection governance independently of the transport implementation. They support bounded incremental history ingestion later: one approved unit scope, limited time window, field whitelist, volume limit and durable checkpoint. They do not imply that all history should be collected now. - -Database technology is intentionally not chosen by Ontology Core. The proposed platform data-plane boundary and its validation gates are documented in [`platform/docs/ADR_GELIOS_DATA_PLANE.md`](../../../docs/ADR_GELIOS_DATA_PLANE.md). The ontology supplies the record boundaries needed by that ADR. +There is no Gelios-specific Platform service or database in this model. +Provider transport never enters External Data Plane or Foundry. `gelios.collection_run` +and `gelios.ingestion_cursor` describe audit and resumability independently of +physical storage. ## Guardrails -- Ontology Core never stores secrets, runtime snapshots, full raw telemetry or large geometry datasets. -- Current provider-account visibility is not the approved Robot2B scope; collection must enforce an owner-approved scope before every run. -- GET alone does not make a route safe: login-as, temporary tokens, configuration and WLN download are excluded from telemetry collection. -- Geozones require paging, filtering, volume controls and map LOD. -- Command send and mutation routes remain red even if the account has documented access. -- Map selection and pins target stable domain subjects, not provider payload IDs or renderer objects. +- Ontology Core never stores credentials, account state, runtime snapshots, + full raw telemetry or bulk geometry. +- Gelios authentication has exactly access and refresh artifacts; credential + labels and workflow classifications must not be reinterpreted as additional + provider token types or scopes. +- Every safe-read collection includes all valid entities returned for the + bound credential; unit allowlists and group filters are not collection + scope. +- A read method alone does not make a capability safe: impersonation, + temporary credential issuance, configuration mutation and downloads remain + excluded until separately classified. +- Dynamic provider fields are dropped until evidenced and classified. +- Geozones and history require bounded loading, paging/cursors, retention and + volume controls. +- Command send and mutation capabilities remain red even when provider access + exists. +- Foundry targets stable domain subjects and Data Product fields, not provider + payloads or renderer identities. -## Evidence and next implementation decisions +## Evidence and evolution -Evidence is limited to official REST OpenAPI, safe GET results, historical Engine donor inspection and the MAP package. The next decisions outside this ontology package are: +Evidence is limited to official Gelios API documentation, safe-read results, +historical NDC L2 donor inspection and provider-neutral map contracts. The +Robot2B pilot counts (107 credential-visible units and a 95-unit legacy +snapshot) are retained only as historical evidence of response shape and +cardinality. They do not define canonical IDs, collection allowlists or product +scope. -1. owner-approved Robot2B allowlist/rule; -2. field policy for Studio, analytics and raw retention; -3. data-plane ADR for realtime telemetry and history; -4. Gelios Gateway implementation boundary and deployment location; -5. fixture and renderer-adapter contract for Map View pins and labels. +Extension order is mandatory: establish safe provider evidence, extend +capability/field classification, update ontology and mapping revision, add +synthetic fixtures and contract tests, then deploy an updated NDC L2 workflow.