NODEDC_PLATFORM/packages/external-provider-contract
Codex 569b8762e6 feat(data-plane): add provider contracts and ontology delivery 2026-07-16 02:23:34 +03:00
..
examples feat(data-plane): add provider contracts and ontology delivery 2026-07-16 02:23:34 +03:00
src feat(data-plane): add provider contracts and ontology delivery 2026-07-16 02:23:34 +03:00
test feat(data-plane): add provider contracts and ontology delivery 2026-07-16 02:23:34 +03:00
README.md feat(data-plane): add provider contracts and ontology delivery 2026-07-16 02:23:34 +03:00
package.json feat(data-plane): add provider contracts and ontology delivery 2026-07-16 02:23:34 +03:00

README.md

External Provider Contract

Этот package — единственное общее место для формы внешних интеграций NODE.DC. Он не содержит provider secrets, customer records, runtime payloads или исполняемый connector code. src/index.mjs даёт dependency-free проверку минимальных v1 contracts; она запускается через npm run check.

Каждый L2 connector instance должен поставлять совместимые versioned артефакты:

provider-manifest
connection-profile
capability-catalog
field-catalog
collection-profile
retention-profile
semantic-mapping-contract
read-model-contract
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. Он не является tenant connection, не содержит endpoint/URL, credential reference, secret, ручной scope или executable provider code. Concrete non-secret connection profile принадлежит и версионируется вместе с конкретным L2 workflow; Engine только привязывает к нему opaque credential reference и grant.

Проверяемые v1 contracts

  • Connection — provider instance, tenant scope и ссылка на credential в Engine. Любые token/secret/password-like поля запрещены.
  • Collection Profile — явная policy сбора. manual не может скрыто содержать polling interval; realtime требует interval не чаще одного раза в секунду.
  • Data Product — нормализованный versioned output с semantic types, полями и внутренней аудиторией.
  • 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 намеренно не является готовым 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.
  • 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 catalog и ontology/data-product contracts. Оно не может содержать tenant, connection, credential, secret или provider transport.

Data Product delivery contracts

Provider-neutral runtime использует отдельные wire schemas:

  • nodedc.data-product.publish/v1 — unscoped publish request от NDC Data Product Publish; содержит только batch identity и canonical facts;
  • nodedc.data-product.snapshot/v1 — согласованный current snapshot с cursor;
  • nodedc.data-product.patch/v1 — committed upsert operations из durable outbox с previousCursor/cursor.

Publish request не может задавать provider, tenant, connection, endpoint, receivedAt, ontology revision, version или persistence policy. Data Plane materializes эти значения из opaque writer binding и зарегистрированного Data Product definition. Snapshot/stream читаются только по отдельному opaque reader binding; shared internal bearer и caller-provided scope headers являются legacy и не используются новыми nodes/Foundry runtime.

snapshot+patch v1 — bounded contract: один scoped Data Product содержит не более 5000 current entity keys (sourceId, semanticType). Snapshot обязан быть полным и привязанным к одному repeatable-read cursor. Параметр limit задаёт защитный ceiling, не page size: превышение возвращает 413 data_product_snapshot_limit_exceeded, а reader не имеет права начинать stream с усечённой базой. nextPageCursor зарезервирован для будущего отдельно версионируемого query contract и текущим bounded runtime не выдаётся.

Для большей cardinality definition заранее раскладывается по стабильным partition Data Products с независимыми snapshot/patch cursors либо использует будущий query contract с единым snapshot barrier и continuation semantics. Offset/source-ID pagination поверх меняющегося current snapshot запрещена: между страницами она способна потерять или задублировать изменения относительно patch cursor.

Для каждого canonical fact действует одинаковый hard ceiling: сериализованный attributes не больше 64 KiB как на publish/intake входе, так и в snapshot/patch выходе. GeoJSON Point принимает longitude только в [-180, 180], latitude в [-90, 90]; batch sequence ограничен диапазоном PostgreSQL integer 0..2147483647. Эти ограничения нельзя ослабить опциями конкретного caller-а.

Manifest, connection/collection profiles, data-product definition и Foundry binding используют fail-closed allowed-key schemas. Неизвестные поля, а также secret-like имена или значения (включая ndc_edpwb_/ndc_edprb_) отклоняются на общей границе.

Все 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 и не становятся custom nodes или ветками Data Plane. Эти инварианты проверяются package test.

Control-plane команда NDC Foundry Binding имеет отдельную replay-safe schema nodedc.foundry.binding-upsert/v1 (validateFoundryBindingUpsert). Это не declarative provider artifact и не runtime transport: команда содержит только application/page/binding/data-product projection и idempotency key, а право на операцию извлекается Foundry из отдельного opaque workload grant.

Engine opaque credential sink

src/engine-credential-sink.mjs задаёт dependency-free server-to-server v1 границу для доставки трёх workload capabilities в Engine Credentials:

  • 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.

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.

Provision и rollback envelopes действуют не более 15 минут: sink отклоняет истёкшие запросы и допускает максимум 60 секунд положительного clock skew. Receipt повторно проверяет freshness относительно собственного processedAt, чтобы старый запрос нельзя было применить или подтвердить через replay.

Каждый 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 невозможно.

Sink обязан выполнять rollback-all: сначала проверить весь request и точное состояние graph, затем создать credentials в staging, атомарно привязать весь набор и только после commit вернуть opaque credentialRef. При любой ошибке новые credentials удаляются, а прежние bindings остаются без изменений. rollback-failed означает карантин и ручное восстановление, но никогда не возвращает частичные credential refs.

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

src/engine-private-extension.mjs задаёт строгую Platform-side границу для Engine-owned активации проверенного n8n-nodes-ndc release. Контракт не устанавливает package и не меняет Engine: он фиксирует async plan -> apply receipt -> operation/status protocol, где apply обязан быстро вернуть queued и operationId, а долгий recreate/acceptance отслеживается отдельно.

Активация и rollback требуют отдельной глобальной capability engine.private-extension.manage. Обычные L1/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:

  • 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 barrier; hot reload запрещён;
  • acceptance требует единый generation и точный набор трёх package-qualified node schemas и трёх credential schemas.

Любая ошибка после switch запускает automatic rollback и повторный force-recreate/acceptance. Ошибка самого rollback переводит runtime в quarantined. Immutable release и существующие Engine Credentials сохраняются. Для первой активации предыдущим проверенным состоянием является n8n-nodes-ndc.inactive/v1: rollback в этот baseline удаляет package из loader surface, но не удаляет credentials.

Public Ops gateway уже умеет прозрачно передавать эти операции через /engine/mcp, если Engine реализует соответствующие 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.

Ownership

  • platform/packages/external-provider-contract — общий контракт и schemas.
  • NDC Agent L2 — provider API adapter: fetch, pagination, batching, semantic mapping, collection profile и ссылка на credential в Engine.
  • Platform External Data Plane — provider-neutral intake, raw retention, canonical facts, current/history projections и scoped read products. Он не знает provider fields, customer/business filters или renderer rules.
  • platform/services/ontology-core/catalog/domain-packages/<provider> — семантика, отношения и guardrails, но не runtime data.
  • Foundry/interface bindings — consumers scoped data product; они не получают provider transport, endpoint или credential reference.

services/<provider>-gateway — устаревший experimental path и не является шаблоном новых integration services. Канон описан в docs/ADR_L2_OWNED_EXTERNAL_CONNECTORS.md.

Mandatory connection boundary

connection принадлежит одному tenant/client context и содержит только ссылку на секрет, утверждённый 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.

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; изменение любого scope-поля либо TTL создаёт новый binding, а прежний binding можно только rotate/revoke.

Новый L2 вызывает POST /internal/data-plane/v1/intake/writer-bound с Authorization: Bearer <writer-token> и 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.

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.

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.

Legacy intake migration

POST /internal/data-plane/v1/intake — временный compatibility route для existing writers. Он принимает только canonical scoped Intake Batch под NODEDC_INTERNAL_ACCESS_TOKEN и требует совпадения body scope с 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 удаляются.

Intake boundary

L2 отправляет в общий Data Plane Intake Batch, а не SQL-запрос в общие таблицы. Platform проверяет boundary и сохраняет raw/history/current projections; semantic mapping остаётся в L2. Список конкретных source IDs не является частью connection scope: scope выбирает read-capability API, а visibility решает data-product consumer.

Inline raw.payload запрещён: L2 не может записать в Data Plane полный ответ provider-а или произвольную строку. Пока отдельный raw-vault не утверждён, batch либо не содержит raw, либо содержит только restricted raw.ref и hash. Secret-like keys и распознаваемые bearer/JWT/writer-token values в любом участке canonical batch, включая facts[].attributes, отклоняются. Retention raw reference вычисляется от server acceptance time, а не от batch.receivedAt; service делает sweep expired envelopes при старте и по расписанию.

Safety classification

Capabilities классифицируются как read, metadata, write, destructive или unknown. Только read и согласованные metadata могут попасть в collector. write и destructive остаются каталогизированными, но не имеют transport route в read adapter.