NODEDC_PLATFORM/packages/external-provider-contract/README.md

303 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
артефакты:
```text
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.