#!/usr/bin/env python3 import argparse import base64 import hashlib import json import os import re import secrets import shutil import stat import subprocess import sys import tarfile import tempfile import time import urllib.error import urllib.request from datetime import datetime, timezone from pathlib import Path, PurePosixPath EXPECTED_SELF = Path("/usr/local/sbin/nodedc-deploy") DEPLOY_ROOT = Path("/volume1/docker/nodedc-deploy") INBOX = DEPLOY_ROOT / "inbox" APPLIED_DIR = DEPLOY_ROOT / "applied" FAILED_DIR = DEPLOY_ROOT / "failed" BACKUPS_DIR = DEPLOY_ROOT / "backups" STATE_DIR = DEPLOY_ROOT / "state" TMP_DIR = DEPLOY_ROOT / ".tmp" RUNTIME_DIR = DEPLOY_ROOT / "runtime" STATE_FILE = STATE_DIR / "applied.jsonl" FAILED_STATE_FILE = STATE_DIR / "failed.jsonl" LOCK_DIR = STATE_DIR / "deploy.lock" DOCKER = Path("/usr/local/bin/docker") MAP_GATEWAY_SECRET_DIR = Path("/volume1/docker/nodedc-platform/secrets") MAP_GATEWAY_SECRET_FILE = MAP_GATEWAY_SECRET_DIR / "map-gateway-admin-secret" MAP_EGRESS_PROXY_SECRET_FILE = MAP_GATEWAY_SECRET_DIR / "map-egress-proxy-token" PROXY_CONTUR_ENV_FILE = Path("/volume1/docker/proxy-contur/.env") DC_AMD_PROXY_RUNTIME_DIR = Path("/volume1/docker/dc-amd-proxy/runtime") EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR = MAP_GATEWAY_SECRET_DIR / "external-data-plane-provisioner" EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR / "token" ENGINE_CREDENTIAL_PROVISIONER_PRIVATE_KEY_FILE = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR / "engine-credential-provisioner-ed25519.pem" ENGINE_CREDENTIAL_SINK_STATE_DIR = Path("/volume2/nodedc-demo/nodedc-data/engine-credential-sink") ENGINE_CREDENTIAL_SINK_PUBLIC_KEY_FILE = ENGINE_CREDENTIAL_SINK_STATE_DIR / "issuer-public-key.pem" ENGINE_CREDENTIAL_PROVISIONER_KEY_ID = "engine-credential-provisioner-20260716-001" ENGINE_CREDENTIAL_BACKEND_BASE_IMAGE = "node:20-alpine" ENGINE_CREDENTIAL_BACKEND_IMAGE = "nodedc/engine-backend:credential-sink-20260716-001" ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256 = "254f9086142eb469a0bcfcb393330a126e4d128a0df7455f0ac71d52b95e770b" ENGINE_CREDENTIAL_BACKEND_NODE_MODULES_DIR = Path("/volume2/nodedc-demo/nodedc-backend-node_modules") ENGINE_CREDENTIAL_BACKEND_OVERRIDE_TEMPLATE_REL = "nodedc-source/services/backend/credential-sink/docker-compose.immutable-runtime.yml" ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR = RUNTIME_DIR / "engine-credential-sink" ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE = ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR / "docker-compose.immutable-runtime.yml" ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE = ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR / "canonical-rootfs.tar" ENGINE_CREDENTIAL_BACKEND_METADATA_FILE = ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR / "runtime-metadata.json" ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE = ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR / "activation.ready" ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL = "nodedc-source/services/backend/data-product-publish-grant/docker-compose.immutable-runtime.yml" ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL = "nodedc-source/services/backend/data-product-read-grant/docker-compose.immutable-runtime.yml" ENGINE_AGENT_FULL_GRANT_MIGRATION_STORE_REL = "nodedc-source/server/engineAgents/store.js" ENGINE_AGENT_FULL_GRANT_MIGRATION_PREDECESSOR_SHA256 = "52daa43499d6d9a97fe7ffa891edb9212b7791e733f91dd3ca686d42739b7e9a" ENGINE_AGENT_FULL_GRANT_MIGRATION_TARGET_SHA256 = "2e62654c2dc12905efcc83a9dff45a818dd7b47924a10600160835c4416540e9" ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_COMPOSE_SHA256 = "258cebb64ff1943c939655cc55bdce00fc5c4dced67ec291d84d6df066ace50e" ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR = MAP_GATEWAY_SECRET_DIR / "engine-edp-managed-provisioner" ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE = ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR / "private-key.pem" ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR = Path("/volume1/docker/nodedc-platform/trust/engine-managed-provisioner") ENGINE_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE = ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR / "public-key.pem" FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR = MAP_GATEWAY_SECRET_DIR / "foundry-edp-managed-provisioner" FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE = FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR / "private-key.pem" FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR = Path("/volume1/docker/nodedc-platform/trust/foundry-managed-provisioner") FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR / "public-key.pem" EXTERNAL_DATA_PLANE_READER_GRANTS_DIR = MAP_GATEWAY_SECRET_DIR / "external-data-plane-reader-grants" FOUNDRY_BINDING_GRANTS_DIR = MAP_GATEWAY_SECRET_DIR / "foundry-binding-grants" N8N_PRIVATE_EXTENSION_RELEASES_ROOT = Path("/volume1/docker/nodedc-platform/n8n-private-extensions") ENGINE_N8N_SEALED_RELEASES_ROOT = Path("/volume2/nodedc-demo/n8n-private-extensions") ENGINE_N8N_TRANSITION_DESCRIPTOR_REL = "nodedc-source/services/n8n/private-extensions/ndc-activation.json" ENGINE_N8N_COMPOSE_OVERRIDE_REL = "nodedc-source/services/n8n/private-extensions/docker-compose.ndc-private-extension.yml" ENGINE_N8N_SCHEMA_ROOT_REL = "nodedc-source/server/assets/n8n/schema/v2.3.2" ENGINE_N8N_NODES_CATALOG_REL = f"{ENGINE_N8N_SCHEMA_ROOT_REL}/nodes.catalog.json" ENGINE_N8N_CREDENTIALS_CATALOG_REL = f"{ENGINE_N8N_SCHEMA_ROOT_REL}/credentials.catalog.json" ENGINE_N8N_SCHEMA_META_REL = f"{ENGINE_N8N_SCHEMA_ROOT_REL}/meta.json" ENGINE_N8N_ICON_REL = "nodedc-source/server/assets/n8n/icons/ndc.svg" ENGINE_N8N_DARK_ICON_REL = "nodedc-source/server/assets/n8n/icons/ndc.dark.svg" ENGINE_NODE_INTELLIGENCE_RELEASE_ID = "2.33.2-974a9fb3492f" ENGINE_NODE_INTELLIGENCE_UPSTREAM_COMMIT = "974a9fb3492fe2c4984ee0549085d531cdc6242a" ENGINE_NODE_INTELLIGENCE_IMAGE = ( f"nodedc/engine-node-intelligence:{ENGINE_NODE_INTELLIGENCE_RELEASE_ID}" ) ENGINE_NODE_INTELLIGENCE_SERVICE = "nodedc-node-intelligence" ENGINE_NODE_INTELLIGENCE_RUNTIME_UID = 11007 ENGINE_NODE_INTELLIGENCE_RUNTIME_GID = 11007 ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR = RUNTIME_DIR / "engine-node-intelligence" ENGINE_NODE_INTELLIGENCE_SECRET_FILE = ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR / "auth-token" ENGINE_NODE_INTELLIGENCE_RELEASES_DIR = ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR / "releases" ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH = ( "/run/nodedc-secrets/engine-node-intelligence-auth-token" ) ENGINE_NODE_INTELLIGENCE_SOURCE_REL = "nodedc-source/server/nodeIntelligence" ENGINE_NODE_INTELLIGENCE_GATEWAY_REL = "nodedc-source/server/routes/engineAgentGateway.js" ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL = "nodedc-source/services/node-intelligence" ENGINE_NODE_INTELLIGENCE_OVERRIDE_REL = ( f"{ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL}/docker-compose.immutable-runtime.yml" ) ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL = ( f"{ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL}/activation.json" ) ENGINE_NODE_INTELLIGENCE_IMAGE_ARCHIVE_REL = ( f"{ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL}/image/engine-node-intelligence.tar" ) ENGINE_NODE_INTELLIGENCE_PREDECESSOR_GATEWAY_SHA256 = ( "6b8c80fa997ef7c438d199a6ee942c5e37aebc4057667af417897fa636131db4" ) ENGINE_NODE_INTELLIGENCE_PREDECESSOR_COMPOSE_SHA256 = ( "258cebb64ff1943c939655cc55bdce00fc5c4dced67ec291d84d6df066ace50e" ) ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES = ( ENGINE_NODE_INTELLIGENCE_SOURCE_REL, ENGINE_NODE_INTELLIGENCE_GATEWAY_REL, ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL, ) ENGINE_NODE_INTELLIGENCE_ROLLBACK_ENTRIES = ( ENGINE_NODE_INTELLIGENCE_GATEWAY_REL, ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL, ) ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL = ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL ENGINE_MCP_CONTROL_PLANE_ARTIFACT_ENTRIES = ( "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs", "nodedc-source/server/assets/engine-agent-npm/package.json", "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.4.tgz", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js", "nodedc-source/server/dataProductPublishGrant/signedDataPlaneClient.js", "nodedc-source/server/dataProductReadGrant/acceptance.js", "nodedc-source/server/dataProductReadGrant/service.js", "nodedc-source/server/dataProductReadGrant/store.js", "nodedc-source/server/engineAgents/store.js", "nodedc-source/server/nodeIntelligence/upstreamProjection.js", ENGINE_NODE_INTELLIGENCE_GATEWAY_REL, "nodedc-source/server/routes/n8n.js", "nodedc-source/server/routes/ndcAgentMcp.js", ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL, "nodedc-source/server/dataProductPublishGrant/service.js", "nodedc-source/server/dataProductPublishGrant/store.js", ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL, ) ENGINE_MCP_CONTROL_PLANE_PREDECESSOR_SHA256 = { ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "604a75ad3b9b463cea5e578760c8a23592e2e00b8acfc0099730a347e27bf4e8", "nodedc-source/server/routes/ndcAgentMcp.js": "534e2c85e1faecc72a00da7ad32d0584ee09b6bd89eeec2221c3b23d80e1e962", "nodedc-source/server/routes/n8n.js": "de6e3c76740af86e2eaeedede140b5ee54eb526f03db270324d7a4fe513f6817", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "a96d3695fdb3ed7012b9041182f37b09e32d1dfa0dbb391d8db2d7d25a39d64d", "nodedc-source/server/dataProductPublishGrant/signedDataPlaneClient.js": "1a26728ac69c5ef3fdce1ce1154a6ce330f72286b6e07825796a246070d15bbf", "nodedc-source/server/engineAgents/store.js": "2e62654c2dc12905efcc83a9dff45a818dd7b47924a10600160835c4416540e9", "nodedc-source/server/nodeIntelligence/upstreamProjection.js": "946353ed8582c5d96e08361603425cc45eaf231f730ae46a75028d3bef352ee9", "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs": "7d3d7a9bc48614e6cd238199caa384073096b7ec8273703833681151b464ef17", "nodedc-source/server/assets/engine-agent-npm/package.json": "9f299b20db4855b1bb4a9bd2e1095dfd059cd90ec82e936884a380aa41249e28", "nodedc-source/server/dataProductPublishGrant/service.js": "85ded82d707e0ef5b1ad739b5a9be24c1432e2dd9e5051108739f09daf20f47c", "nodedc-source/server/dataProductPublishGrant/store.js": "ef2a5e6bfad9f0b1710db006a0d30d01cd9bcaf06524b0e9f975dd771efe3952", } ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256 = { ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "96c726dab5cf1341f74e6e1095d518058ca320e0dd5738e25bdbe75db1f4fc15", "nodedc-source/server/routes/ndcAgentMcp.js": "534e2c85e1faecc72a00da7ad32d0584ee09b6bd89eeec2221c3b23d80e1e962", "nodedc-source/server/routes/n8n.js": "f293a7794405badbabd2bf7ef088f96fe1167d9e249f05cbfc8af280a0d3e8f8", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "901b8fad80018ce177b34ced804b39cb140a47e831414057f484296b373c651d", "nodedc-source/server/dataProductPublishGrant/signedDataPlaneClient.js": "c2a13d5eb49937fe70ec6451d0465cac54c19c7fae44448db099079473ec02fc", "nodedc-source/server/dataProductReadGrant/acceptance.js": "202dbe575b2f2e1c584aa7e6e99e38fa84f12496feb454e3dbd3f98e2d0396dd", "nodedc-source/server/dataProductReadGrant/service.js": "65b8cdd6b603333bac1feb303e9791feb1e9da39dc9b5bfc3df3961273b0052e", "nodedc-source/server/dataProductReadGrant/store.js": "c3fcdc59a794f57d92e3d2ac713ee28341347cebd25364c4060beaa3dc2151de", "nodedc-source/server/engineAgents/store.js": "debd351fe0b8c72b33b8c79909b8f339cbaf061b970576f3ae72df52ebaa211f", "nodedc-source/server/nodeIntelligence/upstreamProjection.js": "761a874b102a938bc6018159ddacdaac71ad6ae08e9f0f8d7f3b58a0165a5131", "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs": "521098de69fe288a56bd4158c849c1055ba24be08e19f7a4111618d0e8138445", "nodedc-source/server/assets/engine-agent-npm/package.json": "cbe113e9b10bb84b9ccbffa3e261908e58a8430c11abe2cf4fd5304741b04597", "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.4.tgz": "e74c0136d346f904b42589d75cb11a952b4d2f21153f1b057fba28e9acf4f95f", ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL: "952df0cb2ac477e644f5f3a9d64b4872ce1da33356eeab0bec17dcb2931cf653", "nodedc-source/server/dataProductPublishGrant/service.js": "ded3832c9677345a988448ae8e69cef254fd9bf39d2de20cffa295c1feedcd7c", "nodedc-source/server/dataProductPublishGrant/store.js": "a92303b2732e21f68c1ac732fa26e4983cbadf3515741cee983b916a283d754c", } ENGINE_MCP_CONTROL_PLANE_NEW_PATHS = ( "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.4.tgz", "nodedc-source/server/dataProductReadGrant/acceptance.js", "nodedc-source/server/dataProductReadGrant/service.js", "nodedc-source/server/dataProductReadGrant/store.js", ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL, ) ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES = ( "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs", "nodedc-source/server/assets/engine-agent-npm/package.json", "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.5.tgz", "nodedc-source/server/assets/provider-packages/v1/catalog.json", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js", "nodedc-source/server/engineAgents/store.js", ENGINE_NODE_INTELLIGENCE_GATEWAY_REL, ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL, ) ENGINE_MCP_ONTOLOGY_SDK_PREDECESSOR_SHA256 = { "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs": "521098de69fe288a56bd4158c849c1055ba24be08e19f7a4111618d0e8138445", "nodedc-source/server/assets/engine-agent-npm/package.json": "cbe113e9b10bb84b9ccbffa3e261908e58a8430c11abe2cf4fd5304741b04597", "nodedc-source/server/assets/provider-packages/v1/catalog.json": "30f89052b6557a2444550bd6e7eed30747b2d9b7ff3a22d079d9905fe0a04702", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "901b8fad80018ce177b34ced804b39cb140a47e831414057f484296b373c651d", "nodedc-source/server/engineAgents/store.js": "debd351fe0b8c72b33b8c79909b8f339cbaf061b970576f3ae72df52ebaa211f", ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "96c726dab5cf1341f74e6e1095d518058ca320e0dd5738e25bdbe75db1f4fc15", } ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256 = { "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs": "adff3e474c914680f7d36b204d1dc03e69dc8065fff1f80b6713526cfc970137", "nodedc-source/server/assets/engine-agent-npm/package.json": "0741647e4f7f58f69f3021d367484609a8e1eb7b8727d6ad9639bd9906b7f455", "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.5.tgz": "72a1b2d41a12a298eaae63ba3334c7c66b6ca53de6a3f03a48607d4ff6da42fb", "nodedc-source/server/assets/provider-packages/v1/catalog.json": "4800e1a1c2af5e4893b1a403c04e91f55689383457caff833edc9e35e8e7e37a", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "d5511a8bd3b4238af88a89537661c9ca4c0126bca7b5ad225985e27ccdb2c64c", "nodedc-source/server/engineAgents/store.js": "4cd4bdd5958cfafee184e98a04fe12aa0c1cbe884326beaec63c99f9fff61285", ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "25fa013ddbfd7d0c7ece8c792daec5f345062757c85eb56cfbff45bf1e812152", } ENGINE_MCP_ONTOLOGY_SDK_NEW_PATHS = ( "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.5.tgz", ) ENGINE_MCP_AUTONOMY_PROVIDER_V5_ARTIFACT_ENTRIES = ( "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs", "nodedc-source/server/assets/engine-agent-npm/package.json", "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.6.tgz", "nodedc-source/server/assets/provider-packages/v1/catalog.json", "nodedc-source/server/engineAgents/store.js", ENGINE_NODE_INTELLIGENCE_GATEWAY_REL, ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL, ) ENGINE_MCP_AUTONOMY_PROVIDER_V5_PREDECESSOR_SHA256 = { "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs": "521098de69fe288a56bd4158c849c1055ba24be08e19f7a4111618d0e8138445", "nodedc-source/server/assets/engine-agent-npm/package.json": "cbe113e9b10bb84b9ccbffa3e261908e58a8430c11abe2cf4fd5304741b04597", "nodedc-source/server/assets/provider-packages/v1/catalog.json": "17f3e368f3264cbbd708965c9e1fd735aa974f15a1383bf88cd6d14a43dbf32d", "nodedc-source/server/engineAgents/store.js": "debd351fe0b8c72b33b8c79909b8f339cbaf061b970576f3ae72df52ebaa211f", ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "96c726dab5cf1341f74e6e1095d518058ca320e0dd5738e25bdbe75db1f4fc15", } ENGINE_MCP_AUTONOMY_PROVIDER_V5_TARGET_SHA256 = { "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs": "c15c9da4f90f44a4e9e12f3683127e906d614fb98e562faa0c939505c973e074", "nodedc-source/server/assets/engine-agent-npm/package.json": "2ca8dcab0fa04bb1b21add1f75a9be61d5aa97753443ee1917302b4e0da5780a", "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.6.tgz": "d007a81cb4e4af569b3c54d3869b0f60b5597e3b531bff232145fa1851d8572a", "nodedc-source/server/assets/provider-packages/v1/catalog.json": "63e0741646197f0b1b3c64a4095e1bc8fb3a95ee6caf20b0293f89d869c9e620", "nodedc-source/server/engineAgents/store.js": "4cd4bdd5958cfafee184e98a04fe12aa0c1cbe884326beaec63c99f9fff61285", ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "5331f6dc8dc306f641370a2968eb025ee3e278e27ae9a217e4cfc2901fec369c", } ENGINE_MCP_AUTONOMY_PROVIDER_V5_NEW_PATHS = ( "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.6.tgz", ) ENGINE_PROVIDER_SECURITY_CATALOG_REL = ( "nodedc-source/server/assets/provider-packages/v1/catalog.json" ) ENGINE_PROVIDER_SECURITY_CATALOG_ARTIFACT_ENTRIES = ( ENGINE_PROVIDER_SECURITY_CATALOG_REL, ) ENGINE_PROVIDER_SECURITY_CATALOG_PREDECESSOR_SHA256 = ( "dd67d8b01091072a4e7be3820b9639c12f2f897ccbbd79208123564d10c1cd81" ) ENGINE_PROVIDER_SECURITY_CATALOG_TARGET_SHA256 = ( "773335bb616a5c03eb2108c4f53ef092c02e75ee75f014511201bc12e2956b27" ) ENGINE_CONTROL_PLANE_STATE_REL = "nodedc-control-plane" ENGINE_PUBLISH_GRANT_STATE_REL = f"{ENGINE_CONTROL_PLANE_STATE_REL}/publish-grants" ENGINE_READ_GRANT_STATE_REL = f"{ENGINE_CONTROL_PLANE_STATE_REL}/read-grants" ENGINE_CONTROL_PLANE_STATE_PATH = Path("/volume2/nodedc-demo/nodedc-control-plane") ENGINE_PUBLISH_GRANT_STATE_PATH = ENGINE_CONTROL_PLANE_STATE_PATH / "publish-grants" ENGINE_READ_GRANT_STATE_PATH = ENGINE_CONTROL_PLANE_STATE_PATH / "read-grants" ENGINE_EDP_PRIVATE_KEY_CONTAINER_PATH = "/run/nodedc-secrets/engine-edp-managed-provisioner-private.pem" ENGINE_CONTROL_PLANE_CONTAINER_PATH = "/var/lib/nodedc-control-plane" ENGINE_PUBLISH_GRANT_CONTAINER_PATH = "/var/lib/nodedc-control-plane/publish-grants" ENGINE_READ_GRANT_CONTAINER_PATH = "/var/lib/nodedc-control-plane/read-grants" EXTERNAL_DATA_PLANE_MANAGED_TRUST_CONTAINER_PATH = "/run/nodedc-trust/engine-managed-provisioner" EXTERNAL_DATA_PLANE_INTERNAL_URL = "http://external-data-plane:18106" PLATFORM_EXTERNAL_DATA_PLANE_COMPOSE_REL = "platform/docker-compose.external-data-plane.yml" EXTERNAL_DATA_PLANE_DATABASE_SERVICE = "external-data-plane-postgres" EXTERNAL_DATA_PLANE_SERVICE = "external-data-plane" EXTERNAL_DATA_PLANE_IMAGE = "nodedc/external-data-plane:local" ENGINE_N8N_VERSION = "2.3.2" ENGINE_N8N_BASE_IMAGE = "docker.n8n.io/n8nio/n8n:2.3.2" ENGINE_N8N_BASE_ARCHITECTURE = "amd64" ENGINE_N8N_RUNTIME_PACKAGE_PATH = "/home/node/.n8n/nodes/node_modules/n8n-nodes-ndc" ENGINE_N8N_NODE_MODULES_PATH = "/usr/local/lib/node_modules/n8n/node_modules" ENGINE_N8N_ICON_SHA256 = "1c928fc996d8b82121e8f8e327d74b1dd21556c106de99c5e8d317d926c0ba17" ENGINE_N8N_DARK_ICON_SHA256 = "ef3b8551da4ce04527736405afa9a220a2c76d047bd18f6f7124b9adb4216b0c" ENGINE_N8N_RELEASE_CATALOG_JSON_SHA256 = { "0.1.2-05e4b38b14b4a019": { "nodes": "230f712230f7ee8debaa4b44a4358cc19c205bc43305d8ed89d5e3daac466506", "credentials": "a26824ffc0e15db857c792153de3417febc0dbba4880380d6c8cd544b3c80f4d", "meta": "caf7635420c61333e65f68350f5567217aed96fb51354b38764bcac2c24e7966", }, "0.1.3-3354149b5245e39a": { "nodes": "b0a5215699a6e691b8cfc505ab457d5632ef6d83adb3537520d0b60760ba16b2", "credentials": "a26824ffc0e15db857c792153de3417febc0dbba4880380d6c8cd544b3c80f4d", "meta": "b8aa60c0d919573626fa0694a1e4ae9ede015325a5d312e3e05d5ac293084aa7", }, "0.1.4-59dc9f7882721d6a": { "nodes": "b0a5215699a6e691b8cfc505ab457d5632ef6d83adb3537520d0b60760ba16b2", "credentials": "af8ada839070a4c24b9b10981a751c2b2db651dced1392333d7a4ef13de4564e", "meta": "8fadbc594c5d14b1d53ab69b677f00823949410b9a37f4e43ecc321e89d68983", }, "0.1.5-3c8ae53f010d7c88": { "nodes": "fb2ef4d38b9c071fae117a27381714658a1a19334b82fc79d64d13f27555fe5a", "credentials": "af8ada839070a4c24b9b10981a751c2b2db651dced1392333d7a4ef13de4564e", "meta": "b324592c38d518af8320d8540c7061e4684056ae88a676cf5e50c6eef03c0dd7", }, "0.1.6-25cc2d7a52a0d0ee": { "nodes": "fb2ef4d38b9c071fae117a27381714658a1a19334b82fc79d64d13f27555fe5a", "credentials": "af8ada839070a4c24b9b10981a751c2b2db651dced1392333d7a4ef13de4564e", "meta": "58adcb182e5f231a4798636a99887a1f214e3c6e1eb4c0dc2d9b2f3aa80a99ef", }, } ENGINE_N8N_INACTIVE_NODES_CATALOG_JSON_SHA256 = "b70d9d8130d498c55de46a5d0758c844f1242b70803457b2291ab3a9de8056f2" ENGINE_N8N_INACTIVE_CREDENTIALS_CATALOG_JSON_SHA256 = "680e9f52aac791efbd38e3bd99bd51ef5cded9756d867897c6c2755850e87b50" ENGINE_N8N_INACTIVE_META_JSON_SHA256 = "0ac555d7ab31cb0ca042db4f474e83cfefbf20b5bbb2e1509fead27ed21e8acb" MAP_GATEWAY_RUNTIME_GID = 1000 EXTERNAL_DATA_PLANE_RUNTIME_UID = 11006 EXTERNAL_DATA_PLANE_RUNTIME_GID = 11006 MAX_ARTIFACT_BYTES = 512 * 1024 * 1024 MAX_MEMBER_COUNT = 20000 MAX_PAYLOAD_BYTES = 1024 * 1024 * 1024 MAX_FILE_BYTES = 256 * 1024 * 1024 PATCH_ID_RE = re.compile(r"^[A-Za-z0-9._-]{1,96}$") MANIFEST_KEYS = {"id", "component", "type"} MAP_GATEWAY_SECRET_RE = re.compile(r"^[A-Za-z0-9_-]{48,256}$") EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_RE = re.compile(r"^[A-Za-z0-9_-]{48,256}$") ENGINE_NODE_INTELLIGENCE_SECRET_RE = re.compile(r"^[A-Za-z0-9_-]{48,256}$") N8N_PRIVATE_EXTENSION_RELEASE_RE = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+-[a-f0-9]{16}$") N8N_PRIVATE_EXTENSION_NODES = ( "dist/nodes/NdcDataProductPublish/NdcDataProductPublish.node.js", "dist/nodes/NdcDataProductRead/NdcDataProductRead.node.js", "dist/nodes/NdcFoundryBinding/NdcFoundryBinding.node.js", ) N8N_PRIVATE_EXTENSION_CREDENTIALS = ( "dist/credentials/NdcDataProductWriterApi.credentials.js", "dist/credentials/NdcDataProductReaderApi.credentials.js", "dist/credentials/NdcFoundryBindingApi.credentials.js", "dist/credentials/NdcProviderRotatingAccessApi.credentials.js", ) ENGINE_N8N_NODE_TYPES = ( "n8n-nodes-ndc.ndcDataProductPublish", "n8n-nodes-ndc.ndcDataProductRead", "n8n-nodes-ndc.ndcFoundryBinding", ) ENGINE_N8N_CREDENTIAL_TYPES = ( "ndcDataProductWriterApi", "ndcDataProductReaderApi", "ndcFoundryBindingApi", "ndcProviderRotatingAccessApi", ) ENGINE_N8N_CREDENTIAL_TYPES_BY_RELEASE = { "0.1.2-05e4b38b14b4a019": ENGINE_N8N_CREDENTIAL_TYPES[:3], "0.1.3-3354149b5245e39a": ENGINE_N8N_CREDENTIAL_TYPES[:3], "0.1.4-59dc9f7882721d6a": ENGINE_N8N_CREDENTIAL_TYPES, "0.1.5-3c8ae53f010d7c88": ENGINE_N8N_CREDENTIAL_TYPES, "0.1.6-25cc2d7a52a0d0ee": ENGINE_N8N_CREDENTIAL_TYPES, } ENGINE_CREDENTIAL_SINK_ARTIFACT_ENTRIES = ( "nodedc-source/server/credentialPolicies/ndcPrivateNode.js", "nodedc-source/server/credentialSink", "nodedc-source/server/index.js", "nodedc-source/server/routes/engineAgentGateway.js", "nodedc-source/server/routes/engineCredentialSink.js", "nodedc-source/server/routes/n8n.js", "nodedc-source/server/routes/ndcAgentMcp.js", ENGINE_CREDENTIAL_BACKEND_OVERRIDE_TEMPLATE_REL, ) ENGINE_DATA_PRODUCT_PUBLISH_GRANT_ARTIFACT_ENTRIES = ( "nodedc-source/server/assets/provider-packages/v1/catalog.json", "nodedc-source/server/dataProductPublishGrant", "nodedc-source/server/engineAgents/store.js", "nodedc-source/server/routes/engineAgentGateway.js", "nodedc-source/server/routes/n8n.js", ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL, ) ENGINE_DATA_PRODUCT_PUBLISH_GRANT_SOURCE_PREFIX = ( "nodedc-source/server/dataProductPublishGrant/" ) ENGINE_DATA_PRODUCT_PUBLISH_GRANT_SOURCE_FILES = ( "acceptance.js", "providerCatalog.js", "service.js", "signedDataPlaneClient.js", "store.js", ) ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_PREDECESSOR_SHA256 = ( "992159fc457ec76ce1f45aad337604c8a72b29252d44fbccda515f0fd6ea6428" ) ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_TARGET_SHA256 = ( "9c931f9abfcadb5b34a8a854c2efb8fd79913e000eecf0967d1b7c500bf9a56a" ) ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CHANGED_PATHS = ( "nodedc-source/server/assets/provider-packages/v1/catalog.json", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js", "nodedc-source/server/dataProductPublishGrant/service.js", "nodedc-source/server/dataProductPublishGrant/store.js", ) ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES = ( "nodedc-source/server/assets/provider-packages/v1/catalog.json", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js", "nodedc-source/server/dataProductPublishGrant/service.js", "nodedc-source/server/dataProductPublishGrant/store.js", ) ENGINE_COMPOSITE_PROVIDER_V4_PREDECESSOR_SHA256 = { "nodedc-source/server/assets/provider-packages/v1/catalog.json": "992159fc457ec76ce1f45aad337604c8a72b29252d44fbccda515f0fd6ea6428", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "901b8fad80018ce177b34ced804b39cb140a47e831414057f484296b373c651d", # Publish Grant 20260718-004 was applied after MCP Control Plane # 20260718-003 and is therefore the authoritative installed predecessor. "nodedc-source/server/dataProductPublishGrant/service.js": "6a417b25b080c05b40c771df4e2dca163491af2c9083ff73dc7e54ad3b360241", "nodedc-source/server/dataProductPublishGrant/store.js": "a92303b2732e21f68c1ac732fa26e4983cbadf3515741cee983b916a283d754c", } ENGINE_COMPOSITE_PROVIDER_V4_TARGET_SHA256 = { "nodedc-source/server/assets/provider-packages/v1/catalog.json": "9c931f9abfcadb5b34a8a854c2efb8fd79913e000eecf0967d1b7c500bf9a56a", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "689c6fbf695e582d983159973a21142787d1b19bb1d343da4c62c03092ac291f", "nodedc-source/server/dataProductPublishGrant/service.js": "83f045f4e0f332644310172ed51bb652d808bf04155b11c02e46bdffc95f7220", "nodedc-source/server/dataProductPublishGrant/store.js": "98662da6acd0489a9cae4b726eb61ce89d9b2c788b2ea95433e9c4f02930c095", } ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES = ( "nodedc-source/server/assets/provider-packages/v1/catalog.json", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js", ) ENGINE_PROVIDER_ROTATING_SLOT_PREDECESSOR_SHA256 = { "nodedc-source/server/assets/provider-packages/v1/catalog.json": "9c931f9abfcadb5b34a8a854c2efb8fd79913e000eecf0967d1b7c500bf9a56a", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "689c6fbf695e582d983159973a21142787d1b19bb1d343da4c62c03092ac291f", } ENGINE_PROVIDER_ROTATING_SLOT_TARGET_SHA256 = { "nodedc-source/server/assets/provider-packages/v1/catalog.json": "17f3e368f3264cbbd708965c9e1fd735aa974f15a1383bf88cd6d14a43dbf32d", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "f6cf5f4de9e57f87fb904e2136a9f34d494e1ffc289aec3ed9ed788b5583a062", } ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES = ( "nodedc-source/server/nodeIntelligence/upstreamProjection.js", ) ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_PREDECESSOR_SHA256 = { "nodedc-source/server/nodeIntelligence/upstreamProjection.js": "761a874b102a938bc6018159ddacdaac71ad6ae08e9f0f8d7f3b58a0165a5131", } ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256 = { "nodedc-source/server/nodeIntelligence/upstreamProjection.js": "2dfa6b4f37d9bfa8b92a8109d4060d02dd2634ceb8f7924504b83ccf3fdd1523", } ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES = ( "nodedc-source/server/assets/provider-packages/v1/catalog.json", "nodedc-source/server/assets/provider-packages/v1/depttrans-zone-authority-v1.json", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js", "nodedc-source/server/dataProductPublishGrant/service.js", "nodedc-source/server/dataProductPublishGrant/store.js", ) ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_PREDECESSOR_SHA256 = { "nodedc-source/server/assets/provider-packages/v1/catalog.json": "63e0741646197f0b1b3c64a4095e1bc8fb3a95ee6caf20b0293f89d869c9e620", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "1a14299167ebe17efd677fa3c59b84c80e12d6846bac6f40f9bcae0729ab22c6", "nodedc-source/server/dataProductPublishGrant/service.js": "83f045f4e0f332644310172ed51bb652d808bf04155b11c02e46bdffc95f7220", "nodedc-source/server/dataProductPublishGrant/store.js": "98662da6acd0489a9cae4b726eb61ce89d9b2c788b2ea95433e9c4f02930c095", } ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_TARGET_SHA256 = { "nodedc-source/server/assets/provider-packages/v1/catalog.json": "1aac712a05e55137d69eedaf363969460ffe866288e8e18e35711967ab232f39", "nodedc-source/server/assets/provider-packages/v1/depttrans-zone-authority-v1.json": "1482032178b4816e599df570face9b1dfa8ae1fa2943ac8f941c561e8cb9aa9d", "nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "e4c216b0affd89ddbd48abcec86febd48eb12a4c56507851daeb47f2ccd60c9a", "nodedc-source/server/dataProductPublishGrant/service.js": "408836564e9a422eb5e648cc24d764f4bfdf7f83d93306742e8615fa8186a642", "nodedc-source/server/dataProductPublishGrant/store.js": "ace5971d0772e9a9499f0849e6b754e3677cc2ca3080e573a12e26acf5a6c0c0", } ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_NEW_PATHS = ( "nodedc-source/server/assets/provider-packages/v1/depttrans-zone-authority-v1.json", ) ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES = ( "nodedc-source/server/routes/n8n.js", ) ENGINE_PROVIDER_TARGET_HOST_POLICY_PREDECESSOR_SHA256 = { "nodedc-source/server/routes/n8n.js": "f293a7794405badbabd2bf7ef088f96fe1167d9e249f05cbfc8af280a0d3e8f8", } ENGINE_PROVIDER_TARGET_HOST_POLICY_TARGET_SHA256 = { "nodedc-source/server/routes/n8n.js": "9bc3638e271102abec91bb80413329befb89d60c0e4dc0548f0dd11e93220d0a", } ENGINE_AGENT_FULL_GRANT_MIGRATION_ARTIFACT_ENTRIES = ( ENGINE_AGENT_FULL_GRANT_MIGRATION_STORE_REL, ) ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_SHA256 = { "docker-compose.yml": ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_COMPOSE_SHA256, "nodedc-source/server/credentialPolicies/ndcPrivateNode.js": "723874a02dc7b8a68b22ff2304431cfe64f28933cedf5f1e6cda79b2e1cf704a", "nodedc-source/server/credentialSink/core.js": "9f0facc41fd398fcd955cffdd486abb126cdcd756c87ffde667fcbe00e2c41d3", "nodedc-source/server/credentialSink/requestAuth.js": "f8c9237c3e6f4219dee0d4f956d6e97f76f5bd7ba8a6fd0b4fb21aceffa38138", "nodedc-source/server/credentialSink/store.js": "c78dc285a973b6acd8a2330f0310935ad08720b2905454492f292d235abf12c0", "nodedc-source/server/credentialSink/vendor/engine-credential-sink.mjs": "b4800eead9bf94793ff1280d06e34aad6b37ef055a9d7f8d83d7fb5f9bd66d8b", "nodedc-source/server/index.js": "b2b790b02839570d967a2ca68b00e2724485a99389ac9b3a589a1b22302a36b8", "nodedc-source/server/routes/engineAgentGateway.js": "604a75ad3b9b463cea5e578760c8a23592e2e00b8acfc0099730a347e27bf4e8", "nodedc-source/server/routes/engineCredentialSink.js": "9cbb69dbc8cbe6181cd5b0170fe9c4d717b0a173866ca98cba3e3766c0bab94e", "nodedc-source/server/routes/n8n.js": "783d822e2457d82e890f43bc00c7e33822077dc2841f0511a89ecb210fd36d48", "nodedc-source/server/routes/ndcAgentMcp.js": "534e2c85e1faecc72a00da7ad32d0584ee09b6bd89eeec2221c3b23d80e1e962", ENGINE_CREDENTIAL_BACKEND_OVERRIDE_TEMPLATE_REL: "944fa64b08255eb8207b93fd327aebb98ecd9400d37d25fcfa8e3a040ee44afe", } ENGINE_DATA_PRODUCT_PUBLISH_GRANT_INSTALLED_FOUNDATION_SHA256 = { **ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_SHA256, # This exact successor was installed by the later restart-safe-auth # transition; initial Publish-grant installation still uses the original # predecessor map above. "nodedc-source/server/index.js": "0ac408e0e9a7bc5c8e13a00afc957b982b065e2bc414919839e0b0a11aa05ba4", } ENGINE_CREDENTIAL_SINK_CONTRACT_SHA256 = "b4800eead9bf94793ff1280d06e34aad6b37ef055a9d7f8d83d7fb5f9bd66d8b" ENGINE_BASE_COMPOSE_SERVICES = ( "postgresql", "authentik-server", "authentik-worker", "n8n-postgres", "ops-postgres", "n8n", "nodedc-backend", "nodedc-frontend-build", "app", ) COMPONENTS = { "engine": { "payload_root": Path("/volume2/nodedc-demo"), "compose_root": Path("/volume2/nodedc-demo"), "services": ("nodedc-backend", "app"), "compose_no_deps": True, "publish_dist": True, "healthchecks": ( "http://127.0.0.1:8080/", "http://127.0.0.1:3001/health", ), }, "launcher": { "payload_root": Path("/volume1/docker/nodedc-platform/launcher/source"), "build_root": Path("/volume1/docker/nodedc-platform/launcher/source"), "build": ("build", "--no-cache", "-t", "nodedc/launcher:local", "."), "compose_root": Path("/volume1/docker/nodedc-platform/platform"), "compose_env_file": Path("/volume1/docker/nodedc-platform/platform/.env.synology"), "compose_files": ( Path("/volume1/docker/nodedc-platform/platform/docker-compose.platform-http.yml"), ), "compose_no_deps": True, "services": ("launcher",), "healthchecks": ( { "url": "http://127.0.0.1:18080/healthz", "headers": {"Host": "hub.nodedc.ru"}, }, ), }, "platform": { "payload_root": Path("/volume1/docker/nodedc-platform"), "compose_root": Path("/volume1/docker/nodedc-platform/platform"), "compose_env_file": Path("/volume1/docker/nodedc-platform/platform/.env.synology"), "compose_files": ( Path("/volume1/docker/nodedc-platform/platform/docker-compose.platform-http.yml"), Path("/volume1/docker/nodedc-platform/platform/docker-compose.external-data-plane.yml"), ), "compose_no_deps": True, "services": ("notification-postgres", "notification-core", "ai-workspace-hub", "launcher", "reverse-proxy", "external-data-plane-postgres", "external-data-plane", "map-gateway"), "healthchecks": ( "http://127.0.0.1:5185/healthz", "http://127.0.0.1:18081/healthz", { "url": "http://127.0.0.1:18080/healthz", "headers": {"Host": "hub.nodedc.ru"}, }, { "url": "http://127.0.0.1:18080/", "headers": {"Host": "id.nodedc.ru"}, }, ), }, "tasker": { "payload_root": Path("/volume1/docker/nodedc-platform/tasker"), "compose_root": Path("/volume1/docker/nodedc-platform/tasker/plane-app"), "compose_project": "nodedc-tasker", "compose_env_file": Path("/volume1/docker/nodedc-platform/tasker/plane-app/.env.synology"), "compose_files": ( Path("/volume1/docker/nodedc-platform/tasker/plane-app/docker-compose.yaml"), Path("/volume1/docker/nodedc-platform/tasker/plane-app/docker-compose.synology.override.yml"), ), "services": ("api", "worker", "beat-worker", "web"), "healthchecks": ( { "url": "http://127.0.0.1:18090/", "headers": {"Host": "ops.nodedc.ru"}, }, ), }, "ops-agents": { "payload_root": Path("/volume1/docker/nodedc-platform/ops-agents"), "compose_root": Path("/volume1/docker/nodedc-platform/ops-agents"), "compose_env_file": Path("/volume1/docker/nodedc-platform/ops-agents/.env"), "compose_files": ( Path("/volume1/docker/nodedc-platform/ops-agents/docker-compose.synology.yml"), ), "compose_build": True, "compose_no_deps": True, "services": ("agent-gateway",), "healthchecks": ( "http://172.22.0.222:18190/readyz", ), }, "bim-viewer": { "payload_root": Path("/volume1/docker/nodedc-platform/bim-viewer/source"), "build_root": Path("/volume1/docker/nodedc-platform/bim-viewer/source/converter"), "build": ("build", "--no-cache", "-t", "nodedc/bim-converter:local", "."), "compose_root": Path("/volume1/docker/nodedc-platform/bim-viewer/source"), "compose_env_file": Path("/volume1/docker/nodedc-platform/bim-viewer/source/.env"), "compose_files": ( Path("/volume1/docker/nodedc-platform/bim-viewer/source/docker-compose.beam.yml"), ), "compose_no_deps": True, "services": ("ndc-beam-viewer", "nodedc-bim-converter"), "healthchecks": ( "http://127.0.0.1:18100/api/auth/session", ), }, "n8n-private-extension": { # This component only stages a verified, immutable offline release in # Platform-owned storage. It deliberately has no Compose file, service, # container mutation or Engine activation side effect. "payload_root": N8N_PRIVATE_EXTENSION_RELEASES_ROOT, "bootstrap_root": True, "artifact_only": True, "immutable_payload": True, "services": (), }, "module-foundry": { "payload_root": Path("/volume1/docker/nodedc-platform/module-foundry/source"), "compose_root": Path("/volume1/docker/nodedc-platform/module-foundry/source/infra"), "compose_project": "nodedc-module-foundry", "compose_env_file": Path("/volume1/docker/nodedc-platform/module-foundry/source/.env"), "compose_files": ( Path("/volume1/docker/nodedc-platform/module-foundry/source/infra/docker-compose.module-foundry.yml"), ), "bootstrap_root": True, "compose_build": True, "compose_no_deps": True, "services": ("nodedc-module-foundry",), "healthchecks": ( "http://172.22.0.222:9920/healthz", ), }, "proxy-contur": { "payload_root": Path("/volume1/docker/proxy-contur"), "compose_root": Path("/volume1/docker/proxy-contur"), "compose_project": "proxy-contur", "compose_env_file": Path("/volume1/docker/proxy-contur/.env"), "compose_files": ( Path("/volume1/docker/proxy-contur/docker-compose.yml"), ), "compose_build": True, "compose_no_deps": True, "services": ("proxy-contur",), "health_container": "proxy-contur", }, "dc-amd-proxy": { "payload_root": Path("/volume1/docker/dc-amd-proxy"), "compose_root": Path("/volume1/docker/dc-amd-proxy"), "compose_project": "dc-amd-proxy", "bootstrap_root": True, "compose_build": True, "compose_no_deps": True, "services": ("dc-amd-proxy",), "health_container": "dc-amd-proxy", }, "dc-cms": { "payload_root": Path("/volume1/docker/dc-cms/source"), "compose_root": Path("/volume1/docker/dc-cms/source/infra"), "compose_env_file": Path("/volume1/docker/dc-cms/source/infra/.env.synology"), "compose_files": ( Path("/volume1/docker/dc-cms/source/infra/docker-compose.yml"), ), "bootstrap_root": True, "compose_build": True, "services": ("postgresql-authentik", "authentik-server", "authentik-worker", "authentik-bootstrap", "cms-app", "reverse-proxy"), "healthchecks": ( { "url": "http://172.22.0.222:9918/auth/login?returnTo=%2F", "headers": {"Host": "cms.dcserve.ru"}, }, { "url": "http://172.22.0.222:9919/", "headers": {"Host": "auth.dcserve.ru"}, }, ), }, "dc-cms-site-nodedc": { "payload_root": Path("/volume1/docker/dc-cms/sites/nodedc"), "compose_root": Path("/volume1/docker/dc-cms/source/infra"), "compose_env_file": Path("/volume1/docker/dc-cms/source/infra/.env.synology"), "compose_files": ( Path("/volume1/docker/dc-cms/source/infra/docker-compose.yml"), ), "compose_no_deps": True, "services": ("cms-app", "reverse-proxy"), "healthchecks": ( { "url": "http://172.22.0.222:9918/auth/login?returnTo=%2F", "headers": {"Host": "cms.dcserve.ru"}, }, ), }, } class NoRedirectHandler(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None NO_REDIRECT_OPENER = urllib.request.build_opener(NoRedirectHandler) class DeployError(Exception): pass class ReconciliationRequired(DeployError): pass def die(message): raise DeployError(message) def utc_now(): return datetime.now(timezone.utc).replace(microsecond=0).isoformat() def stamp(): return datetime.now().strftime("%Y%m%d-%H%M%S") def sha256_file(path): import hashlib h = hashlib.sha256() with path.open("rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): h.update(chunk) return h.hexdigest() def sha256_json_value(value): import hashlib canonical = json.dumps( value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), ).encode("utf-8") return hashlib.sha256(canonical).hexdigest() def safe_name(value): return re.sub(r"[^A-Za-z0-9._-]", "_", value) def is_relative_to(child, parent): try: child.relative_to(parent) return True except ValueError: return False def ensure_layout(): for path in (INBOX, APPLIED_DIR, FAILED_DIR, BACKUPS_DIR, STATE_DIR, TMP_DIR, RUNTIME_DIR): path.mkdir(parents=True, exist_ok=True) for path in (STATE_DIR, BACKUPS_DIR, TMP_DIR, RUNTIME_DIR): os.chown(path, 0, 0) path.chmod(0o700) for path in (APPLIED_DIR, FAILED_DIR): os.chown(path, 0, 0) path.chmod(0o755) def ensure_platform_runtime_secret(secret_file, secret_re, label): # This is Platform infrastructure state, not a product setting. The # root-owned runner creates it once and only explicitly bound service mounts # can read it. It must never be copied into an artifact or a shared .env. secret_dir = secret_file.parent try: directory_stat = secret_dir.lstat() except FileNotFoundError: secret_dir.mkdir(parents=True, exist_ok=False) directory_stat = secret_dir.lstat() if stat.S_ISLNK(directory_stat.st_mode) or not stat.S_ISDIR(directory_stat.st_mode): die(f"{label} secret directory is unsafe: {secret_dir}") os.chown(secret_dir, 0, MAP_GATEWAY_RUNTIME_GID) secret_dir.chmod(0o710) try: secret_stat = secret_file.lstat() except FileNotFoundError: secret_value = secrets.token_urlsafe(48) temporary = secret_dir / f".{secret_file.name}.{os.getpid()}.{time.time_ns()}.tmp" descriptor = None try: descriptor = os.open(str(temporary), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o640) os.write(descriptor, f"{secret_value}\n".encode("ascii")) os.fsync(descriptor) os.fchown(descriptor, 0, MAP_GATEWAY_RUNTIME_GID) os.fchmod(descriptor, 0o640) os.close(descriptor) descriptor = None os.replace(temporary, secret_file) fsync_directory(secret_dir) finally: if descriptor is not None: os.close(descriptor) if temporary.exists(): temporary.unlink() return "created" if stat.S_ISLNK(secret_stat.st_mode) or not stat.S_ISREG(secret_stat.st_mode): die(f"{label} secret file is unsafe: {secret_file}") if secret_stat.st_uid != 0 or secret_stat.st_mode & (stat.S_IWGRP | stat.S_IWOTH): die(f"{label} secret file has unsafe ownership or permissions: {secret_file}") if secret_stat.st_size > 512: die(f"{label} secret file is too large: {secret_file}") try: secret_value = secret_file.read_text(encoding="ascii").strip() except UnicodeDecodeError: die(f"{label} secret file is not ascii: {secret_file}") if not secret_re.fullmatch(secret_value): die(f"{label} secret file has invalid format: {secret_file}") os.chown(secret_file, 0, MAP_GATEWAY_RUNTIME_GID) secret_file.chmod(0o640) return "reused" def ensure_map_gateway_admin_secret(): return ensure_platform_runtime_secret( MAP_GATEWAY_SECRET_FILE, MAP_GATEWAY_SECRET_RE, "map gateway", ) def read_proxy_contur_token(): try: source_stat = PROXY_CONTUR_ENV_FILE.lstat() except FileNotFoundError: die("proxy-contur env file is required for Map egress") if stat.S_ISLNK(source_stat.st_mode) or not stat.S_ISREG(source_stat.st_mode): die("proxy-contur env file is unsafe") if source_stat.st_size > 64 * 1024 or source_stat.st_mode & (stat.S_IWGRP | stat.S_IWOTH): die("proxy-contur env file has unsafe permissions") try: lines = PROXY_CONTUR_ENV_FILE.read_text(encoding="utf-8").splitlines() except UnicodeDecodeError: die("proxy-contur env file is not utf-8") value = None for raw in lines: line = raw.strip() if not line or line.startswith("#"): continue if line.startswith("export "): line = line[7:].lstrip() if not line.startswith("PROXY_TOKEN="): continue value = line.partition("=")[2].strip() if len(value) >= 2 and value[0] in ("'", '"') and value[-1] == value[0]: value = value[1:-1] break if not value or len(value) > 4096 or any(ord(char) < 33 or ord(char) == 127 for char in value): die("proxy-contur PROXY_TOKEN is missing or invalid") return value def sync_map_egress_proxy_secret(): # The Map Gateway gets a read-only file copy of the existing proxy token. # The value is never printed, placed in an artifact, or exposed to Foundry. token = read_proxy_contur_token() try: parent_stat = MAP_GATEWAY_SECRET_DIR.lstat() except FileNotFoundError: MAP_GATEWAY_SECRET_DIR.mkdir(parents=True, exist_ok=False) parent_stat = MAP_GATEWAY_SECRET_DIR.lstat() if stat.S_ISLNK(parent_stat.st_mode) or not stat.S_ISDIR(parent_stat.st_mode): die(f"map egress parent secret directory is unsafe: {MAP_GATEWAY_SECRET_DIR}") os.chown(MAP_GATEWAY_SECRET_DIR, 0, MAP_GATEWAY_RUNTIME_GID) MAP_GATEWAY_SECRET_DIR.chmod(0o710) if MAP_EGRESS_PROXY_SECRET_FILE.exists() or MAP_EGRESS_PROXY_SECRET_FILE.is_symlink(): target_stat = MAP_EGRESS_PROXY_SECRET_FILE.lstat() if stat.S_ISLNK(target_stat.st_mode) or not stat.S_ISREG(target_stat.st_mode): die("map egress proxy secret file is unsafe") temporary = MAP_GATEWAY_SECRET_DIR / f".{MAP_EGRESS_PROXY_SECRET_FILE.name}.{os.getpid()}.{time.time_ns()}.tmp" descriptor = None try: descriptor = os.open(str(temporary), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o640) os.write(descriptor, f"{token}\n".encode("utf-8")) os.fsync(descriptor) os.fchown(descriptor, 0, MAP_GATEWAY_RUNTIME_GID) os.fchmod(descriptor, 0o640) os.close(descriptor) descriptor = None os.replace(temporary, MAP_EGRESS_PROXY_SECRET_FILE) fsync_directory(MAP_GATEWAY_SECRET_DIR) finally: if descriptor is not None: os.close(descriptor) if temporary.exists(): temporary.unlink() return "synced" def ensure_root_owned_grant_directory(path, label): try: parent_stat = MAP_GATEWAY_SECRET_DIR.lstat() except FileNotFoundError: MAP_GATEWAY_SECRET_DIR.mkdir(parents=True, exist_ok=False) parent_stat = MAP_GATEWAY_SECRET_DIR.lstat() if stat.S_ISLNK(parent_stat.st_mode) or not stat.S_ISDIR(parent_stat.st_mode): die(f"{label} parent directory is unsafe: {MAP_GATEWAY_SECRET_DIR}") os.chown(MAP_GATEWAY_SECRET_DIR, 0, MAP_GATEWAY_RUNTIME_GID) MAP_GATEWAY_SECRET_DIR.chmod(0o710) try: directory_stat = path.lstat() except FileNotFoundError: path.mkdir(parents=False, exist_ok=False) directory_stat = path.lstat() if stat.S_ISLNK(directory_stat.st_mode) or not stat.S_ISDIR(directory_stat.st_mode): die(f"{label} directory is unsafe: {path}") os.chown(path, 0, 0) path.chmod(0o500) for entry in path.iterdir(): entry_stat = entry.lstat() if (not re.fullmatch(r"[a-f0-9]{64}", entry.name) or stat.S_ISLNK(entry_stat.st_mode) or not stat.S_ISREG(entry_stat.st_mode) or entry_stat.st_uid != 0 or entry_stat.st_gid != 0 or stat.S_IMODE(entry_stat.st_mode) != 0o400 or entry_stat.st_size < 2 or entry_stat.st_size > 128 * 1024): die(f"{label} record is unsafe: {entry}") def ensure_external_data_plane_provisioner_secret(): # Unlike the Map Gateway key, this capability can mint scoped writers. # Keep it in an isolated child directory readable only by the dedicated # EDP/provisioner uid, never by the shared gid 1000. try: parent_stat = MAP_GATEWAY_SECRET_DIR.lstat() except FileNotFoundError: MAP_GATEWAY_SECRET_DIR.mkdir(parents=True, exist_ok=False) parent_stat = MAP_GATEWAY_SECRET_DIR.lstat() if stat.S_ISLNK(parent_stat.st_mode) or not stat.S_ISDIR(parent_stat.st_mode): die(f"external data plane parent secret directory is unsafe: {MAP_GATEWAY_SECRET_DIR}") os.chown(MAP_GATEWAY_SECRET_DIR, 0, MAP_GATEWAY_RUNTIME_GID) MAP_GATEWAY_SECRET_DIR.chmod(0o710) try: directory_stat = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR.lstat() except FileNotFoundError: EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR.mkdir(parents=False, exist_ok=False) directory_stat = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR.lstat() if stat.S_ISLNK(directory_stat.st_mode) or not stat.S_ISDIR(directory_stat.st_mode): die(f"external data plane provisioner secret directory is unsafe: {EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR}") os.chown(EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR, EXTERNAL_DATA_PLANE_RUNTIME_UID, EXTERNAL_DATA_PLANE_RUNTIME_GID) EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR.chmod(0o500) try: secret_stat = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE.lstat() except FileNotFoundError: secret_value = secrets.token_urlsafe(48) temporary = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR / f".{EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE.name}.{os.getpid()}.{time.time_ns()}.tmp" descriptor = None try: descriptor = os.open(str(temporary), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o400) os.write(descriptor, f"{secret_value}\n".encode("ascii")) os.fsync(descriptor) os.fchown(descriptor, EXTERNAL_DATA_PLANE_RUNTIME_UID, EXTERNAL_DATA_PLANE_RUNTIME_GID) os.fchmod(descriptor, 0o400) os.close(descriptor) descriptor = None os.replace(temporary, EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE) fsync_directory(EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR) finally: if descriptor is not None: os.close(descriptor) if temporary.exists(): temporary.unlink() return "created" if stat.S_ISLNK(secret_stat.st_mode) or not stat.S_ISREG(secret_stat.st_mode): die(f"external data plane provisioner secret file is unsafe: {EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}") if (secret_stat.st_uid != EXTERNAL_DATA_PLANE_RUNTIME_UID or secret_stat.st_gid != EXTERNAL_DATA_PLANE_RUNTIME_GID or stat.S_IMODE(secret_stat.st_mode) != 0o400): die(f"external data plane provisioner secret file has unsafe ownership or permissions: {EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}") if secret_stat.st_size > 512: die(f"external data plane provisioner secret file is too large: {EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}") try: secret_value = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE.read_text(encoding="ascii").strip() except UnicodeDecodeError: die(f"external data plane provisioner secret file is not ascii: {EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}") if not EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_RE.fullmatch(secret_value): die(f"external data plane provisioner secret file has invalid format: {EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}") return "reused" def resolve_openssl_binary(): candidate = shutil.which("openssl") if not candidate: die("openssl is required to provision the Engine credential issuer") path = Path(candidate).resolve() try: path_stat = path.lstat() except FileNotFoundError: die("resolved openssl binary is missing") if (not stat.S_ISREG(path_stat.st_mode) or path_stat.st_uid != 0 or path_stat.st_mode & (stat.S_IWGRP | stat.S_IWOTH)): die(f"openssl binary is unsafe: {path}") return path def run_openssl(arguments, label): result = subprocess.run( [str(resolve_openssl_binary()), *arguments], check=False, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30, ) if result.returncode != 0: die(f"openssl {label} failed") def validate_ed25519_public_key_file(path): try: value = path.read_text(encoding="ascii") except (OSError, UnicodeDecodeError): die(f"Engine credential issuer public key is unreadable: {path}") match = re.fullmatch( r"-----BEGIN PUBLIC KEY-----\n([A-Za-z0-9+/=\n]+)-----END PUBLIC KEY-----\n?", value, ) if not match: die("Engine credential issuer public key PEM format mismatch") try: der = base64.b64decode(match.group(1).replace("\n", ""), validate=True) except ValueError: die("Engine credential issuer public key base64 is invalid") # SubjectPublicKeyInfo for Ed25519 is the fixed RFC 8410 algorithm header # plus one 32-byte public key. This rejects accidental RSA/EC key drift # without printing either private or public key material. if len(der) != 44 or not der.startswith(bytes.fromhex("302a300506032b6570032100")): die("Engine credential issuer key is not Ed25519") def fsync_file(path): descriptor = os.open(str(path), os.O_RDONLY) try: os.fsync(descriptor) finally: os.close(descriptor) def ensure_engine_credential_issuer_keypair(): # The private issuer never enters an artifact, Compose environment or # runner output. The Engine receives only its matching public key through # the already persistent nodedc-data mount. ensure_external_data_plane_provisioner_secret() state_dir = ENGINE_CREDENTIAL_SINK_STATE_DIR try: state_stat = state_dir.lstat() except FileNotFoundError: state_dir.mkdir(parents=True, exist_ok=False) state_stat = state_dir.lstat() if stat.S_ISLNK(state_stat.st_mode) or not stat.S_ISDIR(state_stat.st_mode): die(f"Engine credential sink state directory is unsafe: {state_dir}") os.chown(state_dir, 0, 0) state_dir.chmod(0o700) private_key = ENGINE_CREDENTIAL_PROVISIONER_PRIVATE_KEY_FILE public_key = ENGINE_CREDENTIAL_SINK_PUBLIC_KEY_FILE private_exists = private_key.exists() or private_key.is_symlink() public_exists = public_key.exists() or public_key.is_symlink() if not private_exists and public_exists: die("Engine credential issuer public key exists without its private key") if private_exists: private_stat = private_key.lstat() if (stat.S_ISLNK(private_stat.st_mode) or not stat.S_ISREG(private_stat.st_mode) or private_stat.st_uid != EXTERNAL_DATA_PLANE_RUNTIME_UID or private_stat.st_gid != EXTERNAL_DATA_PLANE_RUNTIME_GID or stat.S_IMODE(private_stat.st_mode) != 0o400 or private_stat.st_size < 64 or private_stat.st_size > 1024): die(f"Engine credential issuer private key is unsafe: {private_key}") run_openssl(["pkey", "-in", str(private_key), "-noout"], "private key validation") else: private_tmp = private_key.with_name(f".{private_key.name}.{os.getpid()}.{time.time_ns()}.tmp") descriptor = None try: descriptor = os.open(str(private_tmp), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) os.close(descriptor) descriptor = None run_openssl(["genpkey", "-algorithm", "ED25519", "-out", str(private_tmp)], "Ed25519 key generation") os.chown(private_tmp, EXTERNAL_DATA_PLANE_RUNTIME_UID, EXTERNAL_DATA_PLANE_RUNTIME_GID) private_tmp.chmod(0o400) fsync_file(private_tmp) os.replace(private_tmp, private_key) fsync_directory(private_key.parent) finally: if descriptor is not None: os.close(descriptor) if private_tmp.exists(): private_tmp.unlink() derived_public = state_dir / f".{public_key.name}.{os.getpid()}.{time.time_ns()}.tmp" descriptor = None try: descriptor = os.open(str(derived_public), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) os.close(descriptor) descriptor = None run_openssl(["pkey", "-in", str(private_key), "-pubout", "-out", str(derived_public)], "public key derivation") validate_ed25519_public_key_file(derived_public) if public_exists: public_stat = public_key.lstat() if (stat.S_ISLNK(public_stat.st_mode) or not stat.S_ISREG(public_stat.st_mode) or public_stat.st_uid != 0 or public_stat.st_gid != 0 or stat.S_IMODE(public_stat.st_mode) != 0o444 or public_stat.st_size < 64 or public_stat.st_size > 1024): die(f"Engine credential issuer public key is unsafe: {public_key}") validate_ed25519_public_key_file(public_key) if derived_public.read_bytes() != public_key.read_bytes(): die("Engine credential issuer keypair mismatch") else: os.chown(derived_public, 0, 0) derived_public.chmod(0o444) fsync_file(derived_public) os.replace(derived_public, public_key) fsync_directory(state_dir) finally: if descriptor is not None: os.close(descriptor) if derived_public.exists(): derived_public.unlink() return "reused" if private_exists else "created" def ensure_engine_edp_managed_provisioner_keypair(): # Engine is the sole holder of the signing key. EDP receives only the # matching public key from a separate trust directory. Neither file is # copied into an artifact, shared .env, n8n container, or L2 graph. openssl = shutil.which("openssl") if not openssl: die("openssl is required to manage the Engine EDP signing key") try: secret_parent_stat = MAP_GATEWAY_SECRET_DIR.lstat() except FileNotFoundError: MAP_GATEWAY_SECRET_DIR.mkdir(parents=True, exist_ok=False) secret_parent_stat = MAP_GATEWAY_SECRET_DIR.lstat() if stat.S_ISLNK(secret_parent_stat.st_mode) or not stat.S_ISDIR(secret_parent_stat.st_mode): die("Engine EDP signing key parent directory is unsafe") os.chown(MAP_GATEWAY_SECRET_DIR, 0, MAP_GATEWAY_RUNTIME_GID) MAP_GATEWAY_SECRET_DIR.chmod(0o710) try: private_dir_stat = ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR.lstat() except FileNotFoundError: ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR.mkdir(parents=False, exist_ok=False) private_dir_stat = ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR.lstat() if stat.S_ISLNK(private_dir_stat.st_mode) or not stat.S_ISDIR(private_dir_stat.st_mode): die("Engine EDP signing key directory is unsafe") os.chown(ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR, 0, 0) ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR.chmod(0o700) trust_parent = ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR.parent try: trust_parent_stat = trust_parent.lstat() except FileNotFoundError: trust_parent.mkdir(parents=True, exist_ok=False) trust_parent_stat = trust_parent.lstat() if stat.S_ISLNK(trust_parent_stat.st_mode) or not stat.S_ISDIR(trust_parent_stat.st_mode): die("Engine EDP trust parent directory is unsafe") os.chown(trust_parent, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID) trust_parent.chmod(0o710) try: trust_dir_stat = ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR.lstat() except FileNotFoundError: ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR.mkdir(parents=False, exist_ok=False) trust_dir_stat = ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR.lstat() if stat.S_ISLNK(trust_dir_stat.st_mode) or not stat.S_ISDIR(trust_dir_stat.st_mode): die("Engine EDP trust directory is unsafe") os.chown(ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID) ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR.chmod(0o550) try: private_stat = ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE.lstat() except FileNotFoundError: private_stat = None try: public_stat = ENGINE_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE.lstat() except FileNotFoundError: public_stat = None # A public-only state is ambiguous: replacing its missing private half # would silently rotate trust. Fail before creating anything so recovery is # an explicit operator decision. Private-only is recoverable by derivation. if private_stat is None and public_stat is not None: die("Engine EDP public key exists without its private key") private_created = False if private_stat is None: temporary_private = ( ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR / f".private-key.{os.getpid()}.{time.time_ns()}.tmp" ) try: subprocess.run( [openssl, "genpkey", "-algorithm", "ED25519", "-out", str(temporary_private)], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True, ) temporary_stat = temporary_private.lstat() if (stat.S_ISLNK(temporary_stat.st_mode) or not stat.S_ISREG(temporary_stat.st_mode) or temporary_stat.st_size < 80 or temporary_stat.st_size > 8192): die("generated Engine EDP private key is invalid") os.chown(temporary_private, 0, 0) temporary_private.chmod(0o400) with temporary_private.open("rb") as handle: os.fsync(handle.fileno()) os.replace(temporary_private, ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE) fsync_directory(ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR) private_created = True except (OSError, subprocess.CalledProcessError) as error: die(f"failed to generate Engine EDP signing key: {type(error).__name__}") finally: if temporary_private.exists(): temporary_private.unlink() private_stat = ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE.lstat() if (stat.S_ISLNK(private_stat.st_mode) or not stat.S_ISREG(private_stat.st_mode) or private_stat.st_uid != 0 or private_stat.st_gid != 0 or stat.S_IMODE(private_stat.st_mode) != 0o400 or private_stat.st_size < 80 or private_stat.st_size > 8192): die("Engine EDP private key has unsafe ownership, mode, or size") try: derived = subprocess.run( [openssl, "pkey", "-in", str(ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE), "-pubout"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ).stdout except (OSError, subprocess.CalledProcessError) as error: die(f"Engine EDP private key validation failed: {type(error).__name__}") if (not derived.startswith(b"-----BEGIN PUBLIC KEY-----\n") or not derived.rstrip().endswith(b"-----END PUBLIC KEY-----") or len(derived) > 8192): die("derived Engine EDP public key is invalid") try: derived_der = subprocess.run( [ openssl, "pkey", "-in", str(ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE), "-pubout", "-outform", "DER", ], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ).stdout except (OSError, subprocess.CalledProcessError) as error: die(f"Engine EDP private key type validation failed: {type(error).__name__}") # RFC 8410 Ed25519 SubjectPublicKeyInfo is exactly 44 bytes and carries the # OID 1.3.101.112. Reject RSA/EC keys before either runtime can mount them. if (len(derived_der) != 44 or not derived_der.startswith(bytes.fromhex("302a300506032b6570032100"))): die("Engine EDP signing key must be Ed25519") if public_stat is not None: if (stat.S_ISLNK(public_stat.st_mode) or not stat.S_ISREG(public_stat.st_mode) or public_stat.st_uid != 0 or public_stat.st_gid != EXTERNAL_DATA_PLANE_RUNTIME_GID or stat.S_IMODE(public_stat.st_mode) != 0o440 or public_stat.st_size < 80 or public_stat.st_size > 8192): die("Engine EDP public key has unsafe ownership, mode, or size") installed = ENGINE_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE.read_bytes() if installed != derived: die("Engine EDP public key does not match the installed private key") return "created" if private_created else "reused" temporary_public = ( ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR / f".public-key.{os.getpid()}.{time.time_ns()}.tmp" ) descriptor = None try: descriptor = os.open(str(temporary_public), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o440) os.write(descriptor, derived) os.fsync(descriptor) os.fchown(descriptor, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID) os.fchmod(descriptor, 0o440) os.close(descriptor) descriptor = None os.replace(temporary_public, ENGINE_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE) fsync_directory(ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR) finally: if descriptor is not None: os.close(descriptor) if temporary_public.exists(): temporary_public.unlink() return "created" return "created" def ensure_foundry_edp_managed_provisioner_keypair(): # Foundry receives its own signing identity. It must never reuse or read # the Engine private key; EDP trusts the matching public key separately. openssl = shutil.which("openssl") if not openssl: die("openssl is required to manage the Foundry EDP signing key") try: secret_parent_stat = MAP_GATEWAY_SECRET_DIR.lstat() except FileNotFoundError: MAP_GATEWAY_SECRET_DIR.mkdir(parents=True, exist_ok=False) secret_parent_stat = MAP_GATEWAY_SECRET_DIR.lstat() if stat.S_ISLNK(secret_parent_stat.st_mode) or not stat.S_ISDIR(secret_parent_stat.st_mode): die("Foundry EDP signing key parent directory is unsafe") os.chown(MAP_GATEWAY_SECRET_DIR, 0, MAP_GATEWAY_RUNTIME_GID) MAP_GATEWAY_SECRET_DIR.chmod(0o710) try: private_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.lstat() except FileNotFoundError: FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.mkdir(parents=False, exist_ok=False) private_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.lstat() if stat.S_ISLNK(private_dir_stat.st_mode) or not stat.S_ISDIR(private_dir_stat.st_mode): die("Foundry EDP signing key directory is unsafe") os.chown(FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR, 0, 0) FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.chmod(0o700) trust_parent = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.parent try: trust_parent_stat = trust_parent.lstat() except FileNotFoundError: trust_parent.mkdir(parents=True, exist_ok=False) trust_parent_stat = trust_parent.lstat() if stat.S_ISLNK(trust_parent_stat.st_mode) or not stat.S_ISDIR(trust_parent_stat.st_mode): die("Foundry EDP trust parent directory is unsafe") os.chown(trust_parent, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID) trust_parent.chmod(0o710) try: trust_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.lstat() except FileNotFoundError: FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.mkdir(parents=False, exist_ok=False) trust_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.lstat() if stat.S_ISLNK(trust_dir_stat.st_mode) or not stat.S_ISDIR(trust_dir_stat.st_mode): die("Foundry EDP trust directory is unsafe") os.chown(FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID) FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.chmod(0o550) try: private_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE.lstat() except FileNotFoundError: private_stat = None try: public_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE.lstat() except FileNotFoundError: public_stat = None if private_stat is None and public_stat is not None: die("Foundry EDP public key exists without its private key") private_created = False if private_stat is None: temporary_private = ( FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR / f".private-key.{os.getpid()}.{time.time_ns()}.tmp" ) try: subprocess.run( [openssl, "genpkey", "-algorithm", "ED25519", "-out", str(temporary_private)], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True, ) temporary_stat = temporary_private.lstat() if (stat.S_ISLNK(temporary_stat.st_mode) or not stat.S_ISREG(temporary_stat.st_mode) or temporary_stat.st_size < 80 or temporary_stat.st_size > 8192): die("generated Foundry EDP private key is invalid") os.chown(temporary_private, 0, 0) temporary_private.chmod(0o400) with temporary_private.open("rb") as handle: os.fsync(handle.fileno()) os.replace(temporary_private, FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE) fsync_directory(FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR) private_created = True except (OSError, subprocess.CalledProcessError) as error: die(f"failed to generate Foundry EDP signing key: {type(error).__name__}") finally: if temporary_private.exists(): temporary_private.unlink() private_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE.lstat() if (stat.S_ISLNK(private_stat.st_mode) or not stat.S_ISREG(private_stat.st_mode) or private_stat.st_uid != 0 or private_stat.st_gid != 0 or stat.S_IMODE(private_stat.st_mode) != 0o400 or private_stat.st_size < 80 or private_stat.st_size > 8192): die("Foundry EDP private key has unsafe ownership, mode, or size") try: derived = subprocess.run( [openssl, "pkey", "-in", str(FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE), "-pubout"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ).stdout derived_der = subprocess.run( [ openssl, "pkey", "-in", str(FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE), "-pubout", "-outform", "DER", ], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ).stdout except (OSError, subprocess.CalledProcessError) as error: die(f"Foundry EDP private key validation failed: {type(error).__name__}") if (not derived.startswith(b"-----BEGIN PUBLIC KEY-----\n") or not derived.rstrip().endswith(b"-----END PUBLIC KEY-----") or len(derived) > 8192 or len(derived_der) != 44 or not derived_der.startswith(bytes.fromhex("302a300506032b6570032100"))): die("Foundry EDP signing key must be Ed25519") if public_stat is not None: if (stat.S_ISLNK(public_stat.st_mode) or not stat.S_ISREG(public_stat.st_mode) or public_stat.st_uid != 0 or public_stat.st_gid != EXTERNAL_DATA_PLANE_RUNTIME_GID or stat.S_IMODE(public_stat.st_mode) != 0o440 or public_stat.st_size < 80 or public_stat.st_size > 8192): die("Foundry EDP public key has unsafe ownership, mode, or size") if FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE.read_bytes() != derived: die("Foundry EDP public key does not match the installed private key") return "created" if private_created else "reused" temporary_public = ( FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR / f".public-key.{os.getpid()}.{time.time_ns()}.tmp" ) descriptor = None try: descriptor = os.open(str(temporary_public), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o440) os.write(descriptor, derived) os.fsync(descriptor) os.fchown(descriptor, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID) os.fchmod(descriptor, 0o440) os.close(descriptor) descriptor = None os.replace(temporary_public, FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE) fsync_directory(FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR) finally: if descriptor is not None: os.close(descriptor) if temporary_public.exists(): temporary_public.unlink() return "created" def ensure_engine_data_product_grant_private_state(include_reader=False): # Compose must never create this bind source on our behalf: Docker's normal # 0755 directory would either expose control-plane metadata or be rejected # by the Engine's fail-closed PublishGrantStore. engine_root = component_root("engine") try: engine_root_stat = engine_root.lstat() except FileNotFoundError: die("Engine root is missing before private state preparation") if stat.S_ISLNK(engine_root_stat.st_mode) or not stat.S_ISDIR(engine_root_stat.st_mode): die("Engine root is unsafe before private state preparation") state_paths = [ (engine_root / ENGINE_CONTROL_PLANE_STATE_REL, "Engine control-plane state"), (engine_root / ENGINE_PUBLISH_GRANT_STATE_REL, "Engine Publish grant state"), ] if include_reader: state_paths.append(( engine_root / ENGINE_READ_GRANT_STATE_REL, "Engine Read grant state", )) for state_path, label in state_paths: try: state_stat = state_path.lstat() except FileNotFoundError: state_path.mkdir(parents=False, exist_ok=False, mode=0o700) state_stat = state_path.lstat() if stat.S_ISLNK(state_stat.st_mode) or not stat.S_ISDIR(state_stat.st_mode): die(f"{label} directory is unsafe: {state_path}") os.chown(state_path, 0, 0) state_path.chmod(0o700) state_stat = state_path.lstat() if (state_stat.st_uid != 0 or state_stat.st_gid != 0 or stat.S_IMODE(state_stat.st_mode) != 0o700): die(f"{label} ownership or mode is unsafe: {state_path}") def ensure_engine_publish_grant_private_state(): # Frozen compatibility seam for the established Publish transition. return ensure_engine_data_product_grant_private_state(include_reader=False) def fsync_directory(path): descriptor = os.open(str(path), os.O_RDONLY | os.O_DIRECTORY) try: os.fsync(descriptor) finally: os.close(descriptor) def require_root(): if os.geteuid() != 0: die("run with sudo") def verify_file_mode(path, require_root_owner): try: st = path.lstat() except FileNotFoundError: die(f"missing path: {path}") if stat.S_ISLNK(st.st_mode): die(f"path must not be a symlink: {path}") if require_root_owner and (st.st_uid != 0 or st.st_gid != 0): die(f"path must be root:root: {path}") if st.st_mode & stat.S_IWGRP: die(f"path must not be group-writable: {path}") if st.st_mode & stat.S_IWOTH: die(f"path must not be world-writable: {path}") def verify_install(): invoked = Path(sys.argv[0]).absolute() if invoked != EXPECTED_SELF: die(f"runner must be invoked as {EXPECTED_SELF}, current: {invoked}") verify_file_mode(EXPECTED_SELF, require_root_owner=True) verify_file_mode(Path("/usr/local"), require_root_owner=True) verify_file_mode(Path("/usr/local/sbin"), require_root_owner=True) print(f"path={EXPECTED_SELF}") print(f"sha256={sha256_file(EXPECTED_SELF)}") print(f"python={sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}") print("verify-install-ok") def validate_artifact_location(path): if path.suffix != ".tgz": die("artifact must have .tgz extension") try: st = path.lstat() except FileNotFoundError: die(f"artifact not found: {path}") if stat.S_ISLNK(st.st_mode): die(f"artifact must not be a symlink: {path}") if not stat.S_ISREG(st.st_mode): die(f"artifact must be a regular file: {path}") if st.st_size > MAX_ARTIFACT_BYTES: die(f"artifact too large: {st.st_size} bytes") inbox_real = INBOX.resolve() parent_real = path.parent.resolve() if parent_real != inbox_real: die(f"artifact must be inside {INBOX}") name = path.name if "/" in name or ".." in name or name.startswith("."): die(f"unsafe artifact filename: {name}") def validate_posix_path(value): if not value: die("empty path") if "\\" in value: die(f"backslash is not allowed in path: {value}") pure = PurePosixPath(value) if pure.is_absolute(): die(f"absolute path rejected: {value}") if any(part in ("", ".", "..") for part in pure.parts): die(f"path escape rejected: {value}") if any(ord(ch) < 32 for ch in value): die(f"control character rejected in path: {value!r}") return pure def tar_name_to_path(work_dir, name): pure = validate_posix_path(name) target = work_dir.joinpath(*pure.parts) resolved = target.resolve(strict=False) if not is_relative_to(resolved, work_dir.resolve()): die(f"tar path escaped work dir: {name}") return target def validate_tar_member(member): name = member.name validate_posix_path(name) if name not in ("manifest.env", "files.txt", "payload") and not name.startswith("payload/"): die(f"unexpected tar member: {name}") if not (member.isfile() or member.isdir()): die(f"unsupported tar member type: {name}") if member.mode & stat.S_ISUID or member.mode & stat.S_ISGID: die(f"setuid/setgid tar member rejected: {name}") if member.isfile() and member.size > MAX_FILE_BYTES: die(f"file too large in artifact: {name}") def scan_tar(artifact): member_count = 0 payload_bytes = 0 names = set() with tarfile.open(artifact, "r:gz") as tar: for member in tar: member_count += 1 if member_count > MAX_MEMBER_COUNT: die("too many files in artifact") validate_tar_member(member) if member.name in names: die(f"duplicate tar member rejected: {member.name}") names.add(member.name) if member.isfile() and member.name.startswith("payload/"): payload_bytes += member.size if payload_bytes > MAX_PAYLOAD_BYTES: die("payload is too large") if "manifest.env" not in names: die("manifest.env missing") if "files.txt" not in names: die("files.txt missing") if not any(name == "payload" or name.startswith("payload/") for name in names): die("payload missing") def safe_extract(artifact, work_dir): scan_tar(artifact) with tarfile.open(artifact, "r:gz") as tar: for member in tar: validate_tar_member(member) target = tar_name_to_path(work_dir, member.name) if member.isdir(): target.mkdir(parents=True, exist_ok=True) target.chmod(0o755) continue if target.exists() and target.is_dir(): die(f"file target already exists as directory: {member.name}") target.parent.mkdir(parents=True, exist_ok=True) source = tar.extractfile(member) if source is None: die(f"cannot read tar member: {member.name}") tmp = target.with_name(f"{target.name}.extracting") with source, tmp.open("wb") as out: shutil.copyfileobj(source, out, 1024 * 1024) tmp.chmod(0o644) os.replace(tmp, target) def parse_manifest(path): data = {} with path.open("r", encoding="utf-8") as f: for lineno, line in enumerate(f, 1): line = line.rstrip("\n") if not line: continue if "=" not in line: die(f"manifest line {lineno} must be key=value") key, value = line.split("=", 1) if key not in MANIFEST_KEYS: die(f"unsupported manifest key: {key}") if key in data: die(f"duplicate manifest key: {key}") if any(ord(ch) < 32 for ch in value): die(f"control character in manifest value: {key}") data[key] = value missing = sorted(MANIFEST_KEYS - set(data)) if missing: die(f"manifest keys missing: {', '.join(missing)}") if not PATCH_ID_RE.match(data["id"]): die("manifest id contains unsafe characters") if data["type"] != "app-overlay": die(f"unsupported artifact type: {data['type']}") if data["component"] not in COMPONENTS: die(f"unsupported component: {data['component']}") return data def parse_files_list(path): entries = [] seen = set() with path.open("r", encoding="utf-8") as f: for lineno, raw in enumerate(f, 1): rel = raw.rstrip("\n") if not rel: continue if rel.strip() != rel: die(f"files.txt line {lineno} has leading/trailing whitespace") validate_posix_path(rel) if rel in seen: die(f"duplicate files.txt entry: {rel}") seen.add(rel) entries.append(rel) if not entries: die("files.txt is empty") return entries def denied_payload_path(component, rel): lower = rel.lower() parts = lower.split("/") base = parts[-1] if component == "engine" and rel in ( "docker-compose.yml", "nodedc-source/services/n8n/private-extensions/n8n-nodes-ndc/Dockerfile", ): pass elif component == "platform" and rel in ( "platform/docker-compose.platform-http.yml", "platform/notification-core/Dockerfile", "platform/ai-workspace-hub/Dockerfile", "platform/ai-workspace-assistant/Dockerfile", "platform/ontology-core/Dockerfile", "platform/gelios-gateway/Dockerfile", "platform/services/map-gateway/Dockerfile", "platform/services/external-data-plane/Dockerfile", ): pass elif component == "bim-viewer" and rel in ( "converter/Dockerfile", ): pass elif component == "dc-cms" and rel in ( "Dockerfile", "infra/docker-compose.yml", ): pass elif component == "module-foundry" and rel in ( "Dockerfile", "infra/docker-compose.module-foundry.yml", ): pass elif component == "proxy-contur" and rel in ( "Dockerfile", "docker-compose.yml", ): pass elif component == "dc-amd-proxy" and rel in ( "Dockerfile", "docker-compose.yml", ): pass elif base in (".env", "dockerfile", "docker-compose.yml", "compose.yml"): return "sensitive or infrastructure filename" if base.endswith(".env") or base.endswith((".pem", ".crt", ".key", ".p12", ".pfx")): return "secret-like file extension" if base.endswith((".sh", ".bash", ".zsh")): return "shell files are not allowed in app-overlay artifacts" if base.endswith(".zip"): return "zip backup files are not deployable artifacts" if any(part in (".git", "node_modules") for part in parts): return "repository/build dependency directory" # `packages/tokens` is a versioned UI design-token package, not a secret # store. Keep this narrowly scoped exception so the general filename guard # remains in force for every other Module Foundry payload path. if not (component == "module-foundry" and lower.startswith("packages/tokens/")) and any( token in lower for token in ("secret", "token", "password") ): return "secret-like path" runtime_prefixes = () if component == "engine": runtime_prefixes = ( "nodedc-source/server/data", "nodedc-source/server/storage", "nodedc-source/server/logs", "nodedc-source/public/storage", "nodedc-source/dist/storage", ) if rel == "nodedc-source/server/.env": return "server env file" elif component == "launcher": runtime_prefixes = ( "server/data", "server/storage", "server/logs", "public/storage", "dist/storage", ) if rel == "server/.env": return "server env file" elif component == "platform": runtime_prefixes = ( "backups", "launcher", "ops-agents", "tasker", "platform/.env.synology", "platform/authentik", "platform/docs", "authentik/data", "authentik/certs", "authentik/media", "authentik/postgresql", ) if rel.startswith("platform/.env"): return "platform env file" if rel.startswith(("platform/Caddyfile.http.bak", "platform/docker-compose.platform-http.yml.bak")): return "platform backup file" elif component == "tasker": runtime_prefixes = ( "plane-app/.local-web-root", "plane-app/archive", "plane-app/backup", "plane-app/plane.env", "plane-app/plane.env.example", "plane-app/plane.env.staging.example", "plane-app/plane.env.synology", "plane-app/plane.env.synology.base", "plane-src/.cache", "plane-src/.next", "plane-src/.pnpm-store", "plane-src/.react-router", "plane-src/.turbo", "plane-src/build", "plane-src/coverage", "plane-src/dist", "plane-src/playwright-report", "plane-src/test-results", ) if rel.startswith(("plane-app/docker-compose.yaml.bak", "plane-app/docker-compose.synology.override.yml.bak")): return "tasker backup file" elif component == "ops-agents": runtime_prefixes = ( "dist", ) if rel.startswith(".env"): return "ops-agents env file" elif component == "bim-viewer": runtime_prefixes = ( "dist", "frontend/dist", "server/data", "server/logs", "server/storage", "server/tmp", ) if rel == ".env" or rel.startswith(".env."): if rel != ".env.synology.example": return "bim-viewer env file" if rel.startswith(("docker-compose.beam.yml.bak",)): return "bim-viewer backup file" elif component == "module-foundry": runtime_prefixes = ( "runtime-data", "node_modules", "apps/catalog/dist", "server/data", "server/logs", "server/storage", ) if rel == ".env" or rel.startswith(".env."): if rel != ".env.example": return "module-foundry env file" if rel.startswith(("Dockerfile.bak", "infra/docker-compose.module-foundry.yml.bak")): return "module-foundry backup file" elif component == "n8n-private-extension": # An extension release is inert data at this boundary. Activation is # Engine-owned and must never be smuggled into the artifact as a script, # Compose file, environment file, pointer or mount configuration. extension_parts = PurePosixPath(rel).parts is_release_directory = ( len(extension_parts) == 3 and extension_parts[0] == "releases" and extension_parts[1] == "n8n-nodes-ndc" and N8N_PRIVATE_EXTENSION_RELEASE_RE.fullmatch(extension_parts[2]) ) if not is_release_directory and base not in ("package.tgz", "release.json", "rollback.json"): return "unexpected private extension release member" elif component == "proxy-contur": runtime_prefixes = ( "node_modules", ) if rel == ".env" or rel.startswith(".env."): return "proxy-contur env file" if rel.startswith(("Dockerfile.bak", "docker-compose.yml.bak")): return "proxy-contur backup file" elif component == "dc-amd-proxy": runtime_prefixes = ( "node_modules", "runtime", ) if rel == ".env" or rel.startswith(".env."): return "dc-amd-proxy env file" if rel.startswith(("Dockerfile.bak", "docker-compose.yml.bak")): return "dc-amd-proxy backup file" elif component == "dc-cms": runtime_prefixes = ( "admin/uploads", "infra/authentik/certs", "infra/authentik/data", "infra/authentik/media", "infra/authentik/postgresql", "server/data", "server/logs", "server/storage", "sites", ) if rel == ".env" or rel.startswith(".env."): return "dc-cms root env file" if rel in ("infra/.env", "infra/.env.synology"): return "dc-cms env file" if rel.startswith("infra/.env.") and rel not in ( "infra/.env.example", "infra/.env.synology.example", ): return "dc-cms env file" if rel.startswith(("Dockerfile.bak", "infra/docker-compose.yml.bak")): return "dc-cms backup file" elif component == "dc-cms-site-nodedc": runtime_prefixes = ( "assets/.trash", "assets/uploads/.trash", "node_modules", ) if any(rel == prefix or rel.startswith(prefix + "/") for prefix in runtime_prefixes): return "runtime data path" return None def allowed_payload_path(component, rel): validate_posix_path(rel) reason = denied_payload_path(component, rel) if reason: die(f"path rejected: {rel}: {reason}") if component == "engine": if rel in ("nodedc-source/package.json", "nodedc-source/package-lock.json"): return True if rel == "docker-compose.yml": return True if rel.startswith(( "nodedc-source/server/", "nodedc-source/src/", "nodedc-source/dist/", "nodedc-source/public/", "nodedc-source/workers/ai-workspace-bridge/", )): return True if rel == "nodedc-source/services/n8n/private-extensions" or rel.startswith( "nodedc-source/services/n8n/private-extensions/" ): return True if rel == ENGINE_CREDENTIAL_BACKEND_OVERRIDE_TEMPLATE_REL: return True if rel == ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL: return True if rel == ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL: return True if rel == ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL or rel.startswith( ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL + "/" ): return True if rel == "nodedc-source/dist": return True if component == "launcher": if rel in ( "index.html", "package.json", "package-lock.json", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json", "vite.config.ts", ): return True if rel.startswith(("server/", "src/", "public/", "scripts/", "dc-ui-guideline/")): return True if component == "platform": if rel in ( "platform/Caddyfile.http", "platform/docker-compose.platform-http.yml", "platform/docker-compose.external-data-plane.yml", ): return True if rel == "platform/notification-core" or rel.startswith("platform/notification-core/"): return True if rel == "platform/ai-workspace-hub" or rel.startswith("platform/ai-workspace-hub/"): return True if rel == "platform/ai-workspace-assistant" or rel.startswith("platform/ai-workspace-assistant/"): return True if rel == "platform/ontology-core" or rel.startswith("platform/ontology-core/"): return True if rel == "platform/gelios-gateway" or rel.startswith("platform/gelios-gateway/"): return True if rel == "platform/services/map-gateway" or rel.startswith("platform/services/map-gateway/"): return True if rel == "platform/services/external-data-plane" or rel.startswith("platform/services/external-data-plane/"): return True if rel == "platform/packages/external-provider-contract" or rel.startswith("platform/packages/external-provider-contract/"): return True if rel == "authentik/custom-templates" or rel.startswith("authentik/custom-templates/"): return True if component == "tasker": if rel in ( "plane-app/docker-compose.yaml", "plane-app/docker-compose.synology.override.yml", ): return True if rel.startswith(( "plane-src/apps/api/", "plane-src/apps/web/", "plane-src/packages/", )): return True if component == "ops-agents": if rel in ( ".dockerignore", ".env.synology.example", ".gitignore", "Dockerfile", "README.md", "docker-compose.local.yml", "docker-compose.synology.yml", "package.json", "package-lock.json", "tsconfig.json", ): return True if rel.startswith(( "docs/", "migrations/", "src/", )): return True if component == "bim-viewer": if rel in ( "converter", "embeddedDemos", "frontend", "images", "locales", "processor", "report", "src", ): return True if rel in ( ".env.synology.example", ".esdoc.json", ".gitattributes", ".gitignore", ".release-please-manifest.json", "CHANGELOG.md", "CODE_OF_CONDUCT.md", "LICENSE", "README.md", "SECURITY.md", "_config.yml", "changelog-template.hbs", "docker-compose.beam.yml", "embeddedViewer.html", "index.js", "package.json", "package-lock.json", "release-please-config.json", "rollup.config.js", "rollup.dev.config.js", "webComponentExample.html", "xeokit-bim-viewer.css", ): return True if rel.startswith(( "converter/", "embeddedDemos/", "frontend/", "images/", "locales/", "processor/", "report/", "server/", "src/", )): return True if component == "module-foundry": if rel in ( ".dockerignore", ".env.example", ".gitignore", "Dockerfile", "package.json", "package-lock.json", "tsconfig.base.json", "infra/docker-compose.module-foundry.yml", ): return True if rel in ("apps", "packages", "registry", "runtime-seed", "scripts", "server"): return True if rel.startswith(( "apps/", "packages/", "registry/", "runtime-seed/", "scripts/", "server/", )): return True if component == "n8n-private-extension": parts = PurePosixPath(rel).parts if ( len(parts) == 3 and parts[0] == "releases" and parts[1] == "n8n-nodes-ndc" and N8N_PRIVATE_EXTENSION_RELEASE_RE.fullmatch(parts[2]) ): return True if ( len(parts) == 4 and parts[0] == "releases" and parts[1] == "n8n-nodes-ndc" and N8N_PRIVATE_EXTENSION_RELEASE_RE.fullmatch(parts[2]) and parts[3] in ("package.tgz", "release.json", "rollback.json") ): return True if component == "proxy-contur": if rel in ( "Dockerfile", "README.md", "docker-compose.yml", "package.json", "package-lock.json", "server.js", ): return True if component == "dc-amd-proxy": if rel in ( "Dockerfile", "README.md", "docker-compose.yml", "package.json", "server.mjs", ): return True if component == "dc-cms": if rel in ( ".dockerignore", ".gitignore", "Dockerfile", "README.md", "package.json", "package-lock.json", "infra/.env.example", "infra/.env.synology.example", "infra/docker-compose.yml", "infra/authentik/bootstrap-cms.py", ): return True if rel.startswith(( "admin/", "infra/authentik/custom-templates/", "infra/reverse-proxy/", "projects/", "server/", )): return True if component == "dc-cms-site-nodedc": if rel in ( ".gitignore", "README.md", "asset-manifest.json", "index.html", "package.json", "package-lock.json", "robots.txt", "sitemap.xml", ): return True if rel.startswith(( "assets/", "content/", "knowledge/", "templates/", "tools/", )): return True die(f"path allowlist rejected: {rel}") def path_is_covered_by_files_list(rel, entries): return any(rel == entry or rel.startswith(entry.rstrip("/") + "/") for entry in entries) def validate_payload_tree(component, payload_dir, entries): root = payload_dir.resolve() for path in payload_dir.rglob("*"): resolved = path.resolve(strict=False) if not is_relative_to(resolved, root): die(f"payload path escaped payload dir: {path}") rel = path.relative_to(payload_dir).as_posix() if path.is_symlink(): die(f"payload symlink rejected: {rel}") if not path.is_file() and not path.is_dir(): die(f"payload special file rejected: {rel}") if path.is_dir(): continue allowed_payload_path(component, rel) if not path_is_covered_by_files_list(rel, entries): die(f"payload file is not covered by files.txt: {rel}") def read_strict_json(path, label, max_bytes=64 * 1024): try: file_stat = path.lstat() except FileNotFoundError: die(f"{label} missing: {path}") if stat.S_ISLNK(file_stat.st_mode) or not stat.S_ISREG(file_stat.st_mode): die(f"{label} must be a regular file") if file_stat.st_size < 2 or file_stat.st_size > max_bytes: die(f"{label} has invalid size") def reject_duplicate_keys(pairs): result = {} for key, value in pairs: if key in result: die(f"{label} has duplicate key: {key}") result[key] = value return result try: return json.loads( path.read_text(encoding="utf-8"), object_pairs_hook=reject_duplicate_keys, ) except UnicodeDecodeError: die(f"{label} is not utf-8") except json.JSONDecodeError as exc: die(f"{label} is invalid json: {exc.msg}") def require_exact_json_keys(value, expected, label): if not isinstance(value, dict): die(f"{label} must be an object") actual = set(value) if actual != set(expected): die(f"{label} keys mismatch: expected={sorted(expected)} actual={sorted(actual)}") def validate_n8n_package_tarball(package_path, release): package_stat = package_path.lstat() if stat.S_ISLNK(package_stat.st_mode) or not stat.S_ISREG(package_stat.st_mode): die("private extension package must be a regular file") if package_stat.st_size < 1024 or package_stat.st_size > 32 * 1024 * 1024: die("private extension package has invalid size") if sha256_file(package_path) != release["package"]["sha256"]: die("private extension package sha256 mismatch") if package_stat.st_size != release["package"]["bytes"]: die("private extension package byte count mismatch") names = set() package_bytes = 0 package_json_bytes = None packed_node_sources = {} with tarfile.open(package_path, "r:gz") as archive: for member_count, member in enumerate(archive, 1): if member_count > 512: die("private extension package has too many members") validate_posix_path(member.name) if member.name in names: die(f"duplicate private extension package member: {member.name}") names.add(member.name) if member.name != "package" and not member.name.startswith("package/"): die(f"private extension package member escaped package root: {member.name}") if not (member.isfile() or member.isdir()): die(f"unsupported private extension package member: {member.name}") if member.name == "package": if not member.isdir(): die("private extension package root must be a directory") else: package_rel = PurePosixPath(member.name).relative_to("package") if any(part.startswith(".") or part.startswith("._") for part in package_rel.parts): die(f"private extension package hidden member rejected: {member.name}") if not ( package_rel.as_posix() in ("README.md", "package.json") or package_rel.as_posix().startswith("dist/") ): die(f"private extension package member is not allowlisted: {member.name}") expected_mode = 0o644 if member.isfile() else 0o755 if stat.S_IMODE(member.mode) != expected_mode: die(f"unsafe private extension package mode: {member.name}") if member.isfile(): package_bytes += member.size if member.size > 4 * 1024 * 1024 or package_bytes > 64 * 1024 * 1024: die("private extension package payload is too large") if member.name == "package/package.json": source = archive.extractfile(member) if source is None: die("private extension package.json cannot be read") package_json_bytes = source.read(64 * 1024 + 1) if member.name.startswith("package/dist/nodes/") and member.name.endswith(".node.js"): source = archive.extractfile(member) if source is None: die(f"private extension packed node cannot be read: {member.name}") packed_node_sources[member.name[len("package/"):]] = source.read(4 * 1024 * 1024 + 1) if package_json_bytes is None or len(package_json_bytes) > 64 * 1024: die("private extension package.json missing or too large") try: package_json = json.loads(package_json_bytes.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError): die("private extension package.json is invalid") if package_json.get("name") != "n8n-nodes-ndc": die("private extension package name mismatch") if package_json.get("version") != release["package"]["version"]: die("private extension package version mismatch") if package_json.get("private") is not True: die("private extension package must remain private") if package_json.get("dependencies") not in (None, {}): die("private extension runtime dependencies are forbidden") scripts = package_json.get("scripts") or {} if not isinstance(scripts, dict): die("private extension package scripts must be an object") for lifecycle in ("preinstall", "install", "postinstall", "prepack", "prepare", "postpack"): if lifecycle in scripts: die(f"private extension lifecycle script is forbidden: {lifecycle}") n8n = package_json.get("n8n") if not isinstance(n8n, dict): die("private extension n8n registration missing") registered = [] expected_registrations = { "nodes": N8N_PRIVATE_EXTENSION_NODES, "credentials": N8N_PRIVATE_EXTENSION_CREDENTIALS, } for field, expected in expected_registrations.items(): values = n8n.get(field) if not isinstance(values, list) or not values: die(f"private extension n8n.{field} registration missing") if values != list(expected): die(f"private extension n8n.{field} registration mismatch") for value in values: if not isinstance(value, str) or not value.startswith("dist/") or value not in names and f"package/{value}" not in names: die(f"private extension n8n.{field} member missing: {value}") registered.append(value) if len(registered) != len(set(registered)): die("private extension n8n registration contains duplicates") packed_nodes = sorted( name[len("package/"):] for name in names if name.startswith("package/dist/nodes/") and name.endswith(".node.js") ) packed_credentials = sorted( name[len("package/"):] for name in names if name.startswith("package/dist/credentials/") and name.endswith(".credentials.js") ) if packed_nodes != sorted(N8N_PRIVATE_EXTENSION_NODES): die("private extension packed node set mismatch") if packed_credentials != sorted(N8N_PRIVATE_EXTENSION_CREDENTIALS): die("private extension packed credential set mismatch") for node_path in N8N_PRIVATE_EXTENSION_NODES: source = packed_node_sources.get(node_path) if source is None or len(source) > 4 * 1024 * 1024: die(f"private extension packed node source missing or too large: {node_path}") if b"usableAsTool" in source: die(f"private extension packed node would generate a tool variant: {node_path}") def validate_n8n_private_extension_release(payload_dir, entries): if len(entries) != 1: die("private extension artifact must contain exactly one immutable release entry") release_rel = entries[0] parts = PurePosixPath(release_rel).parts if ( len(parts) != 3 or parts[0] != "releases" or parts[1] != "n8n-nodes-ndc" or not N8N_PRIVATE_EXTENSION_RELEASE_RE.fullmatch(parts[2]) ): die("private extension files.txt entry must be a digest-bound release directory") release_dir = payload_dir / release_rel expected_files = {"package.tgz", "release.json", "rollback.json"} actual_files = {path.name for path in release_dir.iterdir() if path.is_file()} actual_dirs = [path.name for path in release_dir.iterdir() if path.is_dir()] if actual_dirs or actual_files != expected_files: die("private extension release must contain only package.tgz, release.json and rollback.json") release = read_strict_json(release_dir / "release.json", "private extension release manifest") require_exact_json_keys(release, ("schemaVersion", "releaseId", "package", "storage", "activation"), "private extension release manifest") require_exact_json_keys(release["package"], ("name", "version", "sha256", "bytes", "runtimeTypePrefix"), "private extension package manifest") require_exact_json_keys(release["storage"], ("relativePath", "immutable"), "private extension storage manifest") require_exact_json_keys( release["activation"], ( "owner", "status", "requiredCommunityPackagePath", "requiresAtomicReleaseSwitch", "requiresAllN8nProcessesRestart", "requiresMcpSchemaAcceptance", "rollbackBaselinePolicy", ), "private extension activation manifest", ) require_exact_json_keys( release["activation"]["rollbackBaselinePolicy"], ("allowed", "firstActivation", "requiresPreActivationVerification"), "private extension rollback baseline policy", ) release_id = parts[2] package = release["package"] activation = release["activation"] if release["schemaVersion"] != "nodedc.n8n-private-extension-release/v2": die("private extension release schema mismatch") if release["releaseId"] != release_id: die("private extension release id mismatch") if package["name"] != "n8n-nodes-ndc" or package["runtimeTypePrefix"] != "n8n-nodes-ndc.": die("private extension package identity mismatch") if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", str(package["version"])): die("private extension package version is invalid") if not re.fullmatch(r"[a-f0-9]{64}", str(package["sha256"])): die("private extension package sha256 is invalid") if not isinstance(package["bytes"], int) or isinstance(package["bytes"], bool): die("private extension package byte count is invalid") if release_id != f"{package['version']}-{package['sha256'][:16]}": die("private extension release id is not digest-bound") if release["storage"] != {"relativePath": release_rel, "immutable": True}: die("private extension storage manifest mismatch") rollback_baseline_policy = { "allowed": [ "previous_verified_immutable_release", "verified_inactive", ], "firstActivation": "verified_inactive", "requiresPreActivationVerification": True, } if activation != { "owner": "engine", "status": "blocked_pending_engine_owned_mount", "requiredCommunityPackagePath": "/home/node/.n8n/nodes/node_modules/n8n-nodes-ndc", "requiresAtomicReleaseSwitch": True, "requiresAllN8nProcessesRestart": True, "requiresMcpSchemaAcceptance": True, "rollbackBaselinePolicy": rollback_baseline_policy, }: die("private extension activation boundary mismatch") rollback = read_strict_json(release_dir / "rollback.json", "private extension rollback manifest") require_exact_json_keys( rollback, ("schemaVersion", "releaseId", "packageSha256", "mode", "baselinePolicy", "steps", "forbidden"), "private extension rollback manifest", ) require_exact_json_keys( rollback["baselinePolicy"], ("allowed", "firstActivation", "requiresPreActivationVerification"), "private extension rollback manifest baseline policy", ) if rollback != { "schemaVersion": "nodedc.n8n-private-extension-rollback/v2", "releaseId": release_id, "packageSha256": package["sha256"], "mode": "engine-owned-atomic-release-switch", "baselinePolicy": rollback_baseline_policy, "steps": [ "select_verified_previous_release_or_preverified_inactive_baseline", "switch_engine_owned_mount_atomically", "restart_all_n8n_processes", "verify_mcp_schema_state_matches_selected_baseline", ], "forbidden": [ "delete_active_release", "mutate_engine_core", "live_npm_install", ], }: die("private extension rollback manifest mismatch") validate_n8n_package_tarball(release_dir / "package.tgz", release) def is_engine_n8n_transition(component, entries): return component == "engine" and entries is not None and ENGINE_N8N_TRANSITION_DESCRIPTOR_REL in entries def engine_n8n_credential_types_for_release(release_id): expected = ENGINE_N8N_CREDENTIAL_TYPES_BY_RELEASE.get(release_id) if expected is None: die("Engine n8n credential catalog release is not registered") return expected def read_engine_n8n_transition_descriptor(path, label="Engine n8n transition descriptor"): descriptor = read_strict_json(path, label) require_exact_json_keys( descriptor, ( "schemaVersion", "action", "releaseId", "packageVersion", "packageSha256", "n8nVersion", "baseImage", "baseImageArchitecture", "baseImageIdentityPolicy", "sealedReleaseRelativePath", "composeOverride", "runtimePackagePath", "topologyServices", "expectedCurrent", "expectedNodeTypes", "expectedCredentialTypes", "rollbackBaseline", ), label, ) action = descriptor.get("action") if descriptor.get("schemaVersion") != "nodedc.engine-n8n-private-extension-transition/v1": die("Engine n8n transition schema mismatch") if action not in ("activate", "rollback-inactive"): die("Engine n8n transition action mismatch") release_id = descriptor.get("releaseId") package_version = descriptor.get("packageVersion") package_sha256 = descriptor.get("packageSha256") if not isinstance(release_id, str) or not N8N_PRIVATE_EXTENSION_RELEASE_RE.fullmatch(release_id): die("Engine n8n transition release id is invalid") if not isinstance(package_version, str) or not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", package_version): die("Engine n8n transition package version is invalid") if not isinstance(package_sha256, str) or not re.fullmatch(r"[a-f0-9]{64}", package_sha256): die("Engine n8n transition package sha256 is invalid") if release_id != f"{package_version}-{package_sha256[:16]}": die("Engine n8n transition release identity mismatch") expected_sealed_path = f"n8n-private-extensions/releases/n8n-nodes-ndc/{release_id}/package" exact_common = { "n8nVersion": ENGINE_N8N_VERSION, "baseImage": ENGINE_N8N_BASE_IMAGE, "baseImageArchitecture": ENGINE_N8N_BASE_ARCHITECTURE, "baseImageIdentityPolicy": "running-container-and-local-tag-must-match", "sealedReleaseRelativePath": expected_sealed_path, "composeOverride": ENGINE_N8N_COMPOSE_OVERRIDE_REL, "runtimePackagePath": ENGINE_N8N_RUNTIME_PACKAGE_PATH, "topologyServices": ["n8n"], } for key, expected in exact_common.items(): if descriptor.get(key) != expected: die(f"Engine n8n transition {key} mismatch") if action == "activate": expected_current = descriptor.get("expectedCurrent") if ( expected_current != "verified_inactive" and expected_current not in ENGINE_N8N_RELEASE_CATALOG_JSON_SHA256 ): die("Engine n8n activation expected-current release is not registered") if expected_current == release_id: die("Engine n8n activation target already equals expected current") if descriptor.get("rollbackBaseline") != expected_current: die("Engine n8n activation rollback baseline mismatch") if descriptor.get("expectedNodeTypes") != list(ENGINE_N8N_NODE_TYPES): die("Engine n8n activation node type set mismatch") if descriptor.get("expectedCredentialTypes") != list( engine_n8n_credential_types_for_release(release_id) ): die("Engine n8n activation credential type set mismatch") else: if descriptor.get("expectedCurrent") != release_id: die("Engine n8n rollback expected-current release mismatch") if descriptor.get("rollbackBaseline") != "verified_inactive": die("Engine n8n inactive rollback baseline mismatch") if descriptor.get("expectedNodeTypes") != [] or descriptor.get("expectedCredentialTypes") != []: die("Engine n8n rollback must select the inactive catalog") return descriptor def expected_engine_n8n_compose_override(descriptor): health_script = ( "const http=require('http');const req=http.get('http://127.0.0.1:5678/healthz/readiness'," "r=>{r.resume();process.exit(r.statusCode===200?0:1)});req.on('error',()=>process.exit(1));" "req.setTimeout(4000,()=>{req.destroy();process.exit(1)});" ) health_test = json.dumps(["CMD", "node", "-e", health_script], separators=(",", ":")) mount_source = f"/volume2/nodedc-demo/{descriptor['sealedReleaseRelativePath']}" return "\n".join(( "services:", " n8n:", f" image: {ENGINE_N8N_BASE_IMAGE}", " platform: linux/amd64", " pull_policy: never", " environment:", " N8N_USER_FOLDER: /home/node", ' N8N_COMMUNITY_PACKAGES_ENABLED: "true"', ' N8N_COMMUNITY_PACKAGES_PREVENT_LOADING: "false"', ' N8N_REINSTALL_MISSING_PACKAGES: "false"', " volumes:", f" - {mount_source}:{ENGINE_N8N_RUNTIME_PACKAGE_PATH}:ro", " healthcheck:", f" test: {health_test}", " interval: 10s", " timeout: 5s", " retries: 30", " start_period: 30s", " labels:", f" nodedc.n8n-private-extension.release: {descriptor['releaseId']}", f" nodedc.n8n-private-extension.package-sha256: {descriptor['packageSha256']}", "", )) def validate_engine_n8n_catalog_payload(payload_dir, descriptor): action = descriptor["action"] nodes_path = payload_dir / ENGINE_N8N_NODES_CATALOG_REL credentials_path = payload_dir / ENGINE_N8N_CREDENTIALS_CATALOG_REL meta_path = payload_dir / ENGINE_N8N_SCHEMA_META_REL nodes = read_strict_json( nodes_path, "Engine n8n node catalog", max_bytes=32 * 1024 * 1024, ) credentials = read_strict_json( credentials_path, "Engine n8n credential catalog", max_bytes=4 * 1024 * 1024, ) meta = read_strict_json(meta_path, "Engine n8n schema metadata") if not isinstance(nodes, list) or not isinstance(credentials, list): die("Engine n8n schema catalogs must be arrays") private_nodes = [ item for item in nodes if isinstance(item, dict) and str(item.get("name") or "").startswith("n8n-nodes-ndc.") ] private_credentials = [ item for item in credentials if isinstance(item, dict) and str(item.get("name") or "") in ENGINE_N8N_CREDENTIAL_TYPES ] expected_node_types = list(ENGINE_N8N_NODE_TYPES) if action == "activate" else [] expected_credential_types = ( list(engine_n8n_credential_types_for_release(descriptor["releaseId"])) if action == "activate" else [] ) if [item.get("name") for item in private_nodes] != expected_node_types: die("Engine n8n pinned node catalog exact set mismatch") if [item.get("name") for item in private_credentials] != expected_credential_types: die("Engine n8n pinned credential catalog exact set mismatch") if any("usableAsTool" in item for item in private_nodes): die("Engine n8n pinned node catalog would generate tool variants") if action == "activate": if len(nodes) != 437 or len(credentials) != 385 + len(expected_credential_types): die("Engine n8n activation catalog count mismatch") if any(not str(item.get("displayName") or "").startswith("NDC ") for item in private_nodes): die("Engine n8n private node visible name mismatch") if any(not str(item.get("displayName") or "").startswith("NDC ") for item in private_credentials): die("Engine n8n private credential visible name mismatch") private_catalog_text = json.dumps( {"nodes": private_nodes, "credentials": private_credentials}, ensure_ascii=False, ).lower() if "gelios" in private_catalog_text or "robot2b" in private_catalog_text: die("provider business identity is forbidden in the Engine n8n extension catalog") else: if len(nodes) != 434 or len(credentials) != 385: die("Engine n8n inactive baseline catalog count mismatch") if not isinstance(meta, dict): die("Engine n8n schema metadata must be an object") if meta.get("n8nVersion") != ENGINE_N8N_VERSION: die("Engine n8n schema metadata version mismatch") if meta.get("nodeCount") != len(nodes) or meta.get("credentialCount") != len(credentials): die("Engine n8n schema metadata count mismatch") if action == "activate": if meta.get("source") != f"n8n-core+n8n-nodes-ndc@{descriptor['packageVersion']}": die("Engine n8n schema metadata source mismatch") if sha256_file(payload_dir / ENGINE_N8N_ICON_REL) != ENGINE_N8N_ICON_SHA256: die("Engine n8n light icon sha256 mismatch") if sha256_file(payload_dir / ENGINE_N8N_DARK_ICON_REL) != ENGINE_N8N_DARK_ICON_SHA256: die("Engine n8n dark icon sha256 mismatch") release_hashes = ENGINE_N8N_RELEASE_CATALOG_JSON_SHA256.get(descriptor["releaseId"]) if release_hashes is None: die("Engine n8n activation release catalog is not registered") expected_catalog_hashes = ( (nodes, release_hashes["nodes"], "node"), (credentials, release_hashes["credentials"], "credential"), (meta, release_hashes["meta"], "metadata"), ) else: expected_catalog_hashes = ( (nodes, ENGINE_N8N_INACTIVE_NODES_CATALOG_JSON_SHA256, "node"), (credentials, ENGINE_N8N_INACTIVE_CREDENTIALS_CATALOG_JSON_SHA256, "credential"), (meta, ENGINE_N8N_INACTIVE_META_JSON_SHA256, "metadata"), ) for catalog, expected_sha256, label in expected_catalog_hashes: if sha256_json_value(catalog) != expected_sha256: die(f"Engine n8n {action} {label} catalog sha256 mismatch") def validate_engine_n8n_transition(payload_dir, entries): descriptor = read_engine_n8n_transition_descriptor(payload_dir / ENGINE_N8N_TRANSITION_DESCRIPTOR_REL) activation_entries = [ ENGINE_N8N_TRANSITION_DESCRIPTOR_REL, ENGINE_N8N_COMPOSE_OVERRIDE_REL, ENGINE_N8N_NODES_CATALOG_REL, ENGINE_N8N_CREDENTIALS_CATALOG_REL, ENGINE_N8N_SCHEMA_META_REL, ENGINE_N8N_ICON_REL, ENGINE_N8N_DARK_ICON_REL, ] rollback_entries = [ ENGINE_N8N_TRANSITION_DESCRIPTOR_REL, ENGINE_N8N_NODES_CATALOG_REL, ENGINE_N8N_CREDENTIALS_CATALOG_REL, ENGINE_N8N_SCHEMA_META_REL, ] expected_entries = activation_entries if descriptor["action"] == "activate" else rollback_entries if entries != expected_entries: die("Engine n8n transition files.txt exact set/order mismatch") if descriptor["action"] == "activate": override = payload_dir / ENGINE_N8N_COMPOSE_OVERRIDE_REL try: override_text = override.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): die("Engine n8n Compose override cannot be read") if override_text != expected_engine_n8n_compose_override(descriptor): die("Engine n8n Compose override mismatch") if "N8N_CUSTOM_EXTENSIONS" in override_text or "CUSTOM." in override_text: die("Engine n8n custom-extension loader is forbidden") validate_engine_n8n_catalog_payload(payload_dir, descriptor) return descriptor def is_engine_node_intelligence_transition(component, entries): if component != "engine" or entries is None: return False return tuple(entries) in ( ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES, ENGINE_NODE_INTELLIGENCE_ROLLBACK_ENTRIES, ) def expected_engine_node_intelligence_compose_override(): return "\n".join(( "services:", f" {ENGINE_NODE_INTELLIGENCE_SERVICE}:", f" image: {ENGINE_NODE_INTELLIGENCE_IMAGE}", " pull_policy: never", " restart: unless-stopped", f' user: "{ENGINE_NODE_INTELLIGENCE_RUNTIME_UID}:{ENGINE_NODE_INTELLIGENCE_RUNTIME_GID}"', " read_only: true", " environment:", " HOME: /tmp", " NODE_ENV: production", " MCP_MODE: http", ' PORT: "3000"', f" AUTH_TOKEN_FILE: {ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH}", " NODE_DB_PATH: /app/data/nodes.db", ' REBUILD_ON_START: "false"', ' N8N_MCP_TELEMETRY_DISABLED: "true"', " LOG_LEVEL: warn", " volumes:", " - type: bind", f" source: {ENGINE_NODE_INTELLIGENCE_SECRET_FILE}", f" target: {ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH}", " read_only: true", " expose:", ' - "3000"', " tmpfs:", " - /tmp:mode=1777", " - /app/logs:mode=0755", " cap_drop:", " - ALL", " security_opt:", " - no-new-privileges:true", " healthcheck:", " test:", " - CMD-SHELL", " - curl -fsS http://127.0.0.1:3000/health >/dev/null", " interval: 15s", " timeout: 5s", " retries: 10", " start_period: 30s", "", " nodedc-backend:", " depends_on:", f" {ENGINE_NODE_INTELLIGENCE_SERVICE}:", " condition: service_healthy", " environment:", f" ENGINE_NODE_INTELLIGENCE_MCP_URL: http://{ENGINE_NODE_INTELLIGENCE_SERVICE}:3000/mcp", f" ENGINE_NODE_INTELLIGENCE_AUTH_TOKEN_FILE: {ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH}", f" ENGINE_NODE_INTELLIGENCE_ASSET_ID: nodedc-node-intelligence@{ENGINE_NODE_INTELLIGENCE_RELEASE_ID.replace('-', '+', 1)}", " volumes:", " - type: bind", f" source: {ENGINE_NODE_INTELLIGENCE_SECRET_FILE}", f" target: {ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH}", " read_only: true", "", )) def read_engine_node_intelligence_descriptor(path, label="Engine node-intelligence descriptor"): descriptor = read_strict_json(path, label) require_exact_json_keys( descriptor, ( "schemaVersion", "action", "releaseId", "expectedCurrent", "upstream", "image", "source", "predecessor", ), label, ) if descriptor.get("schemaVersion") != "nodedc.engine-node-intelligence-transition/v1": die("Engine node-intelligence transition schema mismatch") action = descriptor.get("action") if action not in ("activate", "rollback-inactive"): die("Engine node-intelligence transition action mismatch") if descriptor.get("releaseId") != ENGINE_NODE_INTELLIGENCE_RELEASE_ID: die("Engine node-intelligence release mismatch") require_exact_json_keys( descriptor.get("upstream"), ("package", "version", "commit"), f"{label} upstream", ) if descriptor["upstream"] != { "package": "n8n-mcp", "version": "2.33.2", "commit": ENGINE_NODE_INTELLIGENCE_UPSTREAM_COMMIT, }: die("Engine node-intelligence upstream pin mismatch") require_exact_json_keys( descriptor.get("predecessor"), ("gatewaySha256", "composeSha256", "backendRuntime"), f"{label} predecessor", ) predecessor = descriptor["predecessor"] if predecessor.get("composeSha256") != ENGINE_NODE_INTELLIGENCE_PREDECESSOR_COMPOSE_SHA256: die("Engine node-intelligence predecessor Compose mismatch") if predecessor.get("backendRuntime") != "verified-derived-retry": die("Engine node-intelligence backend predecessor mismatch") if action == "activate": if descriptor.get("expectedCurrent") != "inactive": die("Engine node-intelligence activation expected-current mismatch") if predecessor.get("gatewaySha256") != ENGINE_NODE_INTELLIGENCE_PREDECESSOR_GATEWAY_SHA256: die("Engine node-intelligence activation gateway predecessor mismatch") require_exact_json_keys( descriptor.get("image"), ( "tag", "archiveRelativePath", "archiveSha256", "configSha256", "architecture", "os", ), f"{label} image", ) image = descriptor["image"] if ( image.get("tag") != ENGINE_NODE_INTELLIGENCE_IMAGE or image.get("archiveRelativePath") != ENGINE_NODE_INTELLIGENCE_IMAGE_ARCHIVE_REL or not re.fullmatch(r"[a-f0-9]{64}", str(image.get("archiveSha256") or "")) or not re.fullmatch(r"[a-f0-9]{64}", str(image.get("configSha256") or "")) or image.get("architecture") != "amd64" or image.get("os") != "linux" ): die("Engine node-intelligence image descriptor mismatch") require_exact_json_keys( descriptor.get("source"), ( "gatewaySha256", "catalogSha256", "upstreamClientSha256", "upstreamProjectionSha256", "composeOverrideSha256", "readmeSha256", ), f"{label} source", ) for value in descriptor["source"].values(): if not re.fullmatch(r"[a-f0-9]{64}", str(value or "")): die("Engine node-intelligence source digest is invalid") else: if descriptor.get("expectedCurrent") != ENGINE_NODE_INTELLIGENCE_RELEASE_ID: die("Engine node-intelligence rollback expected-current mismatch") require_exact_json_keys( descriptor.get("image"), ("tag", "configSha256", "architecture", "os"), f"{label} image", ) image = descriptor["image"] if ( image.get("tag") != ENGINE_NODE_INTELLIGENCE_IMAGE or not re.fullmatch(r"[a-f0-9]{64}", str(image.get("configSha256") or "")) or image.get("architecture") != "amd64" or image.get("os") != "linux" ): die("Engine node-intelligence rollback image descriptor mismatch") require_exact_json_keys( descriptor.get("source"), ("gatewaySha256", "readmeSha256"), f"{label} source", ) if descriptor["source"].get("gatewaySha256") != ENGINE_NODE_INTELLIGENCE_PREDECESSOR_GATEWAY_SHA256: die("Engine node-intelligence rollback gateway target mismatch") if any( not re.fullmatch(r"[a-f0-9]{64}", str(value or "")) for value in descriptor["source"].values() ): die("Engine node-intelligence rollback source digest is invalid") if not re.fullmatch(r"[a-f0-9]{64}", str(predecessor.get("gatewaySha256") or "")): die("Engine node-intelligence rollback predecessor gateway is invalid") return descriptor def validate_engine_node_intelligence_image_archive(path, descriptor): image = descriptor["image"] try: archive_stat = path.lstat() except FileNotFoundError: die("Engine node-intelligence image archive is missing") if stat.S_ISLNK(archive_stat.st_mode) or not stat.S_ISREG(archive_stat.st_mode): die("Engine node-intelligence image archive is unsafe") if archive_stat.st_size < 1024 or archive_stat.st_size > MAX_ARTIFACT_BYTES: die("Engine node-intelligence image archive size is invalid") if sha256_file(path) != image["archiveSha256"]: die("Engine node-intelligence image archive sha256 mismatch") names = set() manifest_raw = None config_raw = None total = 0 try: with tarfile.open(path, "r:") as archive: members = archive.getmembers() if not members or len(members) > MAX_MEMBER_COUNT: die("Engine node-intelligence image archive member count is invalid") for member in members: validate_posix_path(member.name.rstrip("/")) if member.name in names: die(f"Engine node-intelligence image archive duplicate member: {member.name}") names.add(member.name) if not (member.isfile() or member.isdir()): die(f"Engine node-intelligence image archive special member: {member.name}") if member.isdir(): continue if member.size > MAX_FILE_BYTES: die(f"Engine node-intelligence image archive member too large: {member.name}") total += member.size if total > MAX_ARTIFACT_BYTES: die("Engine node-intelligence image archive expanded size is too large") source = archive.extractfile(member) if source is None: die(f"Engine node-intelligence image archive member unreadable: {member.name}") data = source.read(MAX_FILE_BYTES + 1) if len(data) != member.size: die(f"Engine node-intelligence image archive member size mismatch: {member.name}") if member.name.startswith("blobs/sha256/"): digest = member.name.rsplit("/", 1)[-1] if not re.fullmatch(r"[a-f0-9]{64}", digest): die("Engine node-intelligence image blob name is invalid") if hashlib.sha256(data).hexdigest() != digest: die("Engine node-intelligence image blob digest mismatch") elif member.name not in ("index.json", "manifest.json", "oci-layout"): die(f"Engine node-intelligence image archive unexpected member: {member.name}") if member.name == "manifest.json": manifest_raw = data if manifest_raw is None: die("Engine node-intelligence image manifest is missing") try: manifest = json.loads(manifest_raw.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError): die("Engine node-intelligence image manifest is invalid") if not isinstance(manifest, list) or len(manifest) != 1: die("Engine node-intelligence image manifest exact set mismatch") record = manifest[0] if not isinstance(record, dict) or set(record) != {"Config", "RepoTags", "Layers"}: die("Engine node-intelligence image manifest record mismatch") if record.get("RepoTags") != [ENGINE_NODE_INTELLIGENCE_IMAGE]: die("Engine node-intelligence image tag mismatch") config_name = record.get("Config") expected_config_name = f"blobs/sha256/{image['configSha256']}" if config_name != expected_config_name: die("Engine node-intelligence image config digest mismatch") try: config_member = archive.getmember(config_name) config_file = archive.extractfile(config_member) config_raw = config_file.read(MAX_FILE_BYTES + 1) if config_file else None except KeyError: config_raw = None except (tarfile.TarError, OSError): die("Engine node-intelligence image archive cannot be read") if config_raw is None: die("Engine node-intelligence image config is missing") try: config = json.loads(config_raw.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError): die("Engine node-intelligence image config is invalid") if config.get("architecture") != "amd64" or config.get("os") != "linux": die("Engine node-intelligence image platform mismatch") runtime_config = config.get("config") or {} labels = runtime_config.get("Labels") or {} if labels.get("org.opencontainers.image.revision") != ENGINE_NODE_INTELLIGENCE_UPSTREAM_COMMIT: die("Engine node-intelligence image revision label mismatch") if runtime_config.get("Entrypoint") != ["/usr/local/bin/docker-entrypoint.sh"]: die("Engine node-intelligence image entrypoint mismatch") if runtime_config.get("Cmd") != ["node", "dist/mcp/index.js"]: die("Engine node-intelligence image command mismatch") env = runtime_config.get("Env") or [] if any( str(value).startswith(("AUTH_TOKEN=", "N8N_API_URL=", "N8N_API_KEY=")) for value in env ): die("Engine node-intelligence image contains runtime authority") return image["configSha256"] def validate_engine_node_intelligence_transition(payload_dir, entries): service_root = payload_dir / ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL descriptor = read_engine_node_intelligence_descriptor( service_root / "activation.json" ) expected_entries = ( ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES if descriptor["action"] == "activate" else ENGINE_NODE_INTELLIGENCE_ROLLBACK_ENTRIES ) if tuple(entries) != expected_entries: die("Engine node-intelligence files.txt exact set/order mismatch") gateway = payload_dir / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL if sha256_file(gateway) != descriptor["source"]["gatewaySha256"]: die("Engine node-intelligence gateway sha256 mismatch") readme = service_root / "README.md" if sha256_file(readme) != descriptor["source"]["readmeSha256"]: die("Engine node-intelligence README sha256 mismatch") if descriptor["action"] == "activate": expected_service_files = { "README.md", "activation.json", "docker-compose.immutable-runtime.yml", "image/engine-node-intelligence.tar", } actual_service_files = { child.relative_to(service_root).as_posix() for child in service_root.rglob("*") if child.is_file() } if actual_service_files != expected_service_files: die("Engine node-intelligence service payload file set mismatch") source_root = payload_dir / ENGINE_NODE_INTELLIGENCE_SOURCE_REL expected_source_files = { "catalog.js", "upstreamMcpClient.js", "upstreamProjection.js", } actual_source_files = { child.relative_to(source_root).as_posix() for child in source_root.rglob("*") if child.is_file() } if actual_source_files != expected_source_files: die("Engine node-intelligence source file set mismatch") source_digests = { "catalogSha256": sha256_file(source_root / "catalog.js"), "upstreamClientSha256": sha256_file(source_root / "upstreamMcpClient.js"), "upstreamProjectionSha256": sha256_file(source_root / "upstreamProjection.js"), } if any(descriptor["source"][key] != value for key, value in source_digests.items()): die("Engine node-intelligence source digest mismatch") override = service_root / "docker-compose.immutable-runtime.yml" try: override_text = override.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): die("Engine node-intelligence Compose override is unreadable") if override_text != expected_engine_node_intelligence_compose_override(): die("Engine node-intelligence Compose override mismatch") if sha256_file(override) != descriptor["source"]["composeOverrideSha256"]: die("Engine node-intelligence Compose override sha256 mismatch") gateway_text = gateway.read_text(encoding="utf-8") for required in ( "engine_get_node_intelligence_status", "engine_get_node_guidance", "engine_validate_node_configuration", "engine_validate_l2_deep", ): if required not in gateway_text: die(f"Engine node-intelligence gateway contract missing: {required}") if not any( version in gateway_text for version in ( "const ENGINE_AGENT_MCP_VERSION = '0.3.0'", "const ENGINE_AGENT_MCP_VERSION = '0.4.0'", "const ENGINE_AGENT_MCP_VERSION = '0.5.0'", "const ENGINE_AGENT_MCP_VERSION = '0.6.0'", ) ): die("Engine node-intelligence gateway MCP version is not registered") validate_engine_node_intelligence_image_archive( payload_dir / ENGINE_NODE_INTELLIGENCE_IMAGE_ARCHIVE_REL, descriptor, ) else: expected_service_files = {"README.md", "activation.json"} actual_service_files = { child.relative_to(service_root).as_posix() for child in service_root.rglob("*") if child.is_file() } if actual_service_files != expected_service_files: die("Engine node-intelligence rollback payload file set mismatch") if sha256_file(gateway) != ENGINE_NODE_INTELLIGENCE_PREDECESSOR_GATEWAY_SHA256: die("Engine node-intelligence rollback gateway baseline mismatch") return descriptor def is_engine_mcp_control_plane_slice(component, entries): return ( component == "engine" and entries is not None and tuple(entries) == ENGINE_MCP_CONTROL_PLANE_ARTIFACT_ENTRIES ) def validate_engine_mcp_control_plane_payload(payload_dir, entries): if tuple(entries) != ENGINE_MCP_CONTROL_PLANE_ARTIFACT_ENTRIES: die("Engine MCP control-plane files.txt exact set/order mismatch") for rel, expected_sha256 in ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256.items(): path = payload_dir / rel if path.is_symlink() or not path.is_file() or sha256_file(path) != expected_sha256: die(f"Engine MCP control-plane target sha256 mismatch: {rel}") gateway = (payload_dir / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL).read_text(encoding="utf-8") graph_route = (payload_dir / "nodedc-source/server/routes/ndcAgentMcp.js").read_text( encoding="utf-8" ) installer = ( payload_dir / "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs" ).read_text(encoding="utf-8") package = read_strict_json( payload_dir / "nodedc-source/server/assets/engine-agent-npm/package.json", "Engine MCP Codex installer package", ) if package.get("name") != "@nodedc/engine-codex-agent" or package.get("version") != "0.1.4": die("Engine MCP Codex installer package identity mismatch") if any( marker not in gateway for marker in ( "const ENGINE_AGENT_MCP_VERSION = '0.5.0'", "engine_open_l2_change_session", "engine_get_l2_change_session", "engine_close_l2_change_session", "never_infer_expiry", "three_identical_failures_without_new_evidence", "engine_plan_data_product_read_grant", "engine_apply_data_product_read_grant", "engine_accept_data_product_read_grant", "engine_rollback_data_product_read_grant", ) ): die("Engine MCP bounded-change-session contract is incomplete") if any( marker not in graph_route for marker in ( "update_node_no_effect", "subworkflow_post_write_equality_failed", "verifiedWrite: true", "graphDigest: expectedDigest", ) ): die("Engine MCP graph post-write equality contract is incomplete") if any( marker not in installer for marker in ( "Do not interrupt the user after ordinary recoverable errors", "Never infer that a provider or managed capability token expired", "Three identical failures with no new evidence", "engine_plan_data_product_*_grant", "durable until an explicit rollback or revoke", ) ): die("Engine MCP Codex bounded-autonomy policy is incomplete") combined = "\n".join((gateway, graph_route, installer)).lower() if "gelios" in combined or "robot2b" in combined: die("provider identity is forbidden in Engine MCP control-plane source") archive_path = payload_dir / "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.4.tgz" expected_members = { "package/bin/nodedc-engine-codex-agent.mjs": ( payload_dir / "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs" ).read_bytes(), "package/package.json": ( payload_dir / "nodedc-source/server/assets/engine-agent-npm/package.json" ).read_bytes(), } observed_members = {} try: with tarfile.open(archive_path, "r:gz") as archive: for member in archive: if not (member.isfile() or member.isdir()) or member.name.startswith("/"): die("Engine MCP Codex installer archive member is unsafe") if member.isfile(): source = archive.extractfile(member) if source is None: die("Engine MCP Codex installer archive member is unreadable") observed_members[member.name] = source.read(MAX_FILE_BYTES + 1) except (OSError, tarfile.TarError): die("Engine MCP Codex installer archive is invalid") if observed_members != expected_members: die("Engine MCP Codex installer archive/source equality mismatch") n8n_route = (payload_dir / "nodedc-source/server/routes/n8n.js").read_text( encoding="utf-8" ) if "engineDataProductReadGrantN8nAdapter" not in n8n_route: die("Engine MCP managed reader native adapter is missing") if "ndcDataProductReaderApi" not in n8n_route or "ndc_edprb_" not in n8n_route: die("Engine MCP managed reader credential contract is incomplete") if "read_grant_must_be_durable" not in n8n_route: die("Engine MCP managed reader durable lifecycle is missing") reader_service = ( payload_dir / "nodedc-source/server/dataProductReadGrant/service.js" ).read_text(encoding="utf-8") if ( "readerGrantLifetime: 'explicit-revoke'" not in reader_service or "expiresAt: null" not in reader_service or "ENGINE_READ_GRANT_TTL_DAYS" in reader_service ): die("Engine MCP managed reader explicit-revoke lifecycle is incomplete") publish_service = ( payload_dir / "nodedc-source/server/dataProductPublishGrant/service.js" ).read_text(encoding="utf-8") if ( "writerGrantLifetime: 'explicit-revoke'" not in publish_service or "migrateCurrentGrantToDurable" not in publish_service or "expiresAt: null" not in publish_service or "ENGINE_PUBLISH_GRANT_TTL_DAYS" in publish_service ): die("Engine MCP managed writer explicit-revoke lifecycle is incomplete") reader_override = payload_dir / ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL try: reader_override_text = reader_override.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): die("Engine MCP managed reader runtime override is unreadable") if reader_override_text != expected_engine_data_product_read_grant_override(): die("Engine MCP managed reader runtime override mismatch") agent_store = (payload_dir / "nodedc-source/server/engineAgents/store.js").read_text( encoding="utf-8" ) for scope in ( "'engine:l2:data-product-read-grant:plan'", "'engine:l2:data-product-read-grant:write'", ): if scope not in agent_store: die(f"Engine MCP managed reader scope is missing: {scope}") descriptor = read_engine_node_intelligence_descriptor( payload_dir / ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL, "Engine MCP control-plane node-intelligence descriptor", ) if ( descriptor.get("action") != "activate" or descriptor["source"].get("gatewaySha256") != ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL] or descriptor["source"].get("upstreamProjectionSha256") != ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256[ "nodedc-source/server/nodeIntelligence/upstreamProjection.js" ] ): die("Engine MCP control-plane descriptor target mismatch") return descriptor def preflight_engine_mcp_control_plane_predecessor(payload_dir): candidate = validate_engine_mcp_control_plane_payload( payload_dir, ENGINE_MCP_CONTROL_PLANE_ARTIFACT_ENTRIES, ) installed = current_engine_node_intelligence_descriptor() validate_installed_engine_node_intelligence_source(installed) if installed is None or installed.get("action") != "activate": die("Engine MCP control-plane requires active node intelligence") expected_candidate = json.loads(json.dumps(installed)) expected_candidate["source"]["gatewaySha256"] = ( ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL] ) expected_candidate["source"]["upstreamProjectionSha256"] = ( ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256[ "nodedc-source/server/nodeIntelligence/upstreamProjection.js" ] ) if candidate != expected_candidate: die("Engine MCP control-plane descriptor crosses node-intelligence source boundary") root = component_root("engine") for rel, expected_sha256 in ENGINE_MCP_CONTROL_PLANE_PREDECESSOR_SHA256.items(): path = root / rel if path.is_symlink() or not path.is_file() or sha256_file(path) != expected_sha256: die(f"Engine MCP control-plane predecessor drift detected: {rel}") for rel in ENGINE_MCP_CONTROL_PLANE_NEW_PATHS: path = root / rel if path.exists() or path.is_symlink(): die(f"Engine MCP control-plane new path already exists: {rel}") backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine MCP control-plane requires the active immutable backend runtime") return { "descriptor": candidate, "predecessor_gateway_sha256": installed["source"]["gatewaySha256"], "target_gateway_sha256": candidate["source"]["gatewaySha256"], "backend_mode": backend["mode"], } def accept_engine_mcp_control_plane_runtime(): root = component_root("engine") descriptor = validate_engine_mcp_control_plane_payload( root, ENGINE_MCP_CONTROL_PLANE_ARTIFACT_ENTRIES, ) installed = current_engine_node_intelligence_descriptor() if installed != descriptor: die("Engine MCP control-plane installed descriptor equality failed") validate_installed_engine_node_intelligence_source(installed) backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine MCP control-plane backend runtime acceptance failed") live = run_engine_backend_probe( ( "node", "--input-type=module", "-e", """ const module=await import('file:///app/server/routes/engineAgentGateway.js'); const store=await import('file:///app/server/engineAgents/store.js'); const names=new Set(module.engineAgentTools.map((tool)=>tool.name)); const required=['engine_plan_data_product_read_grant','engine_apply_data_product_read_grant','engine_accept_data_product_read_grant','engine_rollback_data_product_read_grant']; const scopes=['engine:l2:data-product-read-grant:plan','engine:l2:data-product-read-grant:write']; if(!required.every((name)=>names.has(name))||!scopes.every((scope)=>store.ENGINE_AGENT_SCOPES.includes(scope)))process.exit(2); process.stdout.write('engine-mcp-reader-grant:0.5.0:'+required.length+':'+scopes.length); """.strip(), ), "Engine MCP managed reader capability", container_id=engine_backend_container_id(), ) if live != "engine-mcp-reader-grant:0.5.0:4:2": die("Engine MCP managed reader live capability acceptance mismatch") return { "gateway_sha256": descriptor["source"]["gatewaySha256"], "backend_mode": backend["mode"], "reader_grant": live, } def is_engine_mcp_ontology_sdk_slice(component, entries): return ( component == "engine" and entries is not None and tuple(entries) == ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES ) def validate_engine_mcp_ontology_sdk_payload(payload_dir, entries): if tuple(entries) != ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES: die("Engine MCP Ontology/SDK files.txt exact set/order mismatch") for rel, expected_sha256 in ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256.items(): path = payload_dir / rel if path.is_symlink() or not path.is_file() or sha256_file(path) != expected_sha256: die(f"Engine MCP Ontology/SDK target sha256 mismatch: {rel}") gateway = (payload_dir / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL).read_text( encoding="utf-8" ) installer_path = ( payload_dir / "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs" ) installer = installer_path.read_text(encoding="utf-8") package_path = payload_dir / "nodedc-source/server/assets/engine-agent-npm/package.json" package = read_strict_json(package_path, "Engine MCP Codex installer package") if package.get("name") != "@nodedc/engine-codex-agent" or package.get("version") != "0.1.5": die("Engine MCP Ontology/SDK installer package identity mismatch") if any( marker not in gateway for marker in ( "const ENGINE_AGENT_MCP_VERSION = '0.6.0'", "authenticateEngineAgentOntologyToken", "apiRouter.post('/ontology-mcp'", "process.env.NODEDC_INTERNAL_ACCESS_TOKEN", "serverName: 'nodedc-engine-agent'", "serverName: 'nodedc_ontology'", ) ): die("Engine MCP separate Ontology gateway contract is incomplete") if any( marker not in installer for marker in ( "const ENGINE_SERVER_NAME = 'nodedc-engine-agent'", "const ONTOLOGY_SERVER_NAME = 'nodedc_ontology'", "Ontology tools/catalog are never embedded or multiplexed into Engine MCP", "version: '0.1.5'", ) ): die("Engine MCP separate Ontology installer contract is incomplete") archive_path = payload_dir / "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.5.tgz" expected_members = { "package/bin/nodedc-engine-codex-agent.mjs": installer_path.read_bytes(), "package/package.json": package_path.read_bytes(), } observed_members = {} try: with tarfile.open(archive_path, "r:gz") as archive: for member in archive: if not (member.isfile() or member.isdir()) or member.name.startswith("/"): die("Engine MCP Ontology/SDK installer archive member is unsafe") if member.isfile(): source = archive.extractfile(member) if source is None: die("Engine MCP Ontology/SDK installer archive member is unreadable") observed_members[member.name] = source.read(MAX_FILE_BYTES + 1) except (OSError, tarfile.TarError): die("Engine MCP Ontology/SDK installer archive is invalid") if observed_members != expected_members: die("Engine MCP Ontology/SDK installer archive/source equality mismatch") catalog = read_strict_json( payload_dir / "nodedc-source/server/assets/provider-packages/v1/catalog.json", "Engine provider security catalog", ) packages = catalog.get("packages") if isinstance(catalog, dict) else None if not isinstance(packages, list) or len(packages) != 1: die("Engine provider security catalog must contain one active package") provider = packages[0] capabilities = provider.get("capabilities") if isinstance(provider, dict) else None capability = capabilities[0] if isinstance(capabilities, list) and len(capabilities) == 1 else None request = capability.get("request") if isinstance(capability, dict) else None if ( provider.get("id") != "gelios.provider.v2" or provider.get("version") != "2.0.0" or (provider.get("providerCredential") or {}).get("credentialType") != "httpQueryAuth" or not isinstance(request, dict) or request.get("method") != "GET" or request.get("url") != "https://admin.geliospro.com/sdk/?svc=get_units¶ms=%7B%7D" ): die("Engine Gelios SDK/query-auth security projection mismatch") resolver = ( payload_dir / "nodedc-source/server/dataProductPublishGrant/providerCatalog.js" ).read_text(encoding="utf-8") if "new Set(['httpBearerAuth', 'httpQueryAuth'])" not in resolver: die("Engine provider credential type allowlist mismatch") store = (payload_dir / "nodedc-source/server/engineAgents/store.js").read_text( encoding="utf-8" ) if any( marker not in store for marker in ( "const STORE_VERSION = 3", "ontologyTokenHash", "`ndc_eao_${", "authenticateEngineAgentOntologyToken", ) ): die("Engine separate Ontology token store migration is incomplete") descriptor = read_engine_node_intelligence_descriptor( payload_dir / ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL, "Engine MCP Ontology/SDK node-intelligence descriptor", ) if ( descriptor.get("action") != "activate" or descriptor["source"].get("gatewaySha256") != ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL] or descriptor["source"].get("upstreamProjectionSha256") != ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256[ "nodedc-source/server/nodeIntelligence/upstreamProjection.js" ] ): die("Engine MCP Ontology/SDK descriptor target mismatch") return descriptor def preflight_engine_mcp_ontology_sdk_predecessor(payload_dir): candidate = validate_engine_mcp_ontology_sdk_payload( payload_dir, ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES, ) installed = current_engine_node_intelligence_descriptor() validate_installed_engine_node_intelligence_source(installed) if installed is None or installed.get("action") != "activate": die("Engine MCP Ontology/SDK requires active node intelligence") expected_candidate = json.loads(json.dumps(installed)) expected_candidate["source"]["gatewaySha256"] = ( ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL] ) if candidate != expected_candidate: die("Engine MCP Ontology/SDK descriptor crosses node-intelligence source boundary") root = component_root("engine") for rel, expected_sha256 in ENGINE_MCP_ONTOLOGY_SDK_PREDECESSOR_SHA256.items(): path = root / rel if path.is_symlink() or not path.is_file() or sha256_file(path) != expected_sha256: die(f"Engine MCP Ontology/SDK predecessor drift detected: {rel}") for rel in ENGINE_MCP_ONTOLOGY_SDK_NEW_PATHS: path = root / rel if path.exists() or path.is_symlink(): die(f"Engine MCP Ontology/SDK new path already exists: {rel}") backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine MCP Ontology/SDK requires the active immutable backend runtime") return { "descriptor": candidate, "predecessor_gateway_sha256": installed["source"]["gatewaySha256"], "target_gateway_sha256": candidate["source"]["gatewaySha256"], "backend_mode": backend["mode"], } def accept_engine_mcp_ontology_sdk_runtime(): root = component_root("engine") descriptor = validate_engine_mcp_ontology_sdk_payload( root, ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES, ) installed = current_engine_node_intelligence_descriptor() if installed != descriptor: die("Engine MCP Ontology/SDK installed descriptor equality failed") validate_installed_engine_node_intelligence_source(installed) backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine MCP Ontology/SDK backend runtime acceptance failed") live = run_engine_backend_probe( ( "node", "--input-type=module", "-e", """ const gateway=await import('file:///app/server/routes/engineAgentGateway.js'); const store=await import('file:///app/server/engineAgents/store.js'); const catalog=JSON.parse(await (await import('node:fs/promises')).readFile('/app/server/assets/provider-packages/v1/catalog.json','utf8')); const provider=catalog.packages?.[0]; if(gateway.ENGINE_AGENT_MCP_VERSION!=='0.6.0')process.exit(2); if(typeof store.authenticateEngineAgentOntologyToken!=='function')process.exit(3); if(provider?.id!=='gelios.provider.v2'||provider?.providerCredential?.credentialType!=='httpQueryAuth')process.exit(4); process.stdout.write('engine-mcp-ontology-sdk:0.6.0:0.1.5:gelios.provider.v2:httpQueryAuth'); """.strip(), ), "Engine MCP Ontology/SDK capability", container_id=engine_backend_container_id(), ) expected = "engine-mcp-ontology-sdk:0.6.0:0.1.5:gelios.provider.v2:httpQueryAuth" if live != expected: die("Engine MCP Ontology/SDK live capability acceptance mismatch") return { "gateway_sha256": descriptor["source"]["gatewaySha256"], "backend_mode": backend["mode"], "capability": live, } def is_engine_mcp_autonomy_provider_v5_slice(component, entries): return ( component == "engine" and entries is not None and tuple(entries) == ENGINE_MCP_AUTONOMY_PROVIDER_V5_ARTIFACT_ENTRIES ) def validate_engine_mcp_autonomy_provider_v5_payload(payload_dir, entries): if tuple(entries) != ENGINE_MCP_AUTONOMY_PROVIDER_V5_ARTIFACT_ENTRIES: die("Engine MCP autonomy/provider v5 files.txt exact set/order mismatch") for rel, expected_sha256 in ENGINE_MCP_AUTONOMY_PROVIDER_V5_TARGET_SHA256.items(): path = payload_dir / rel if path.is_symlink() or not path.is_file() or sha256_file(path) != expected_sha256: die(f"Engine MCP autonomy/provider v5 target sha256 mismatch: {rel}") gateway = (payload_dir / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL).read_text( encoding="utf-8" ) installer_path = ( payload_dir / "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs" ) installer = installer_path.read_text(encoding="utf-8") package_path = payload_dir / "nodedc-source/server/assets/engine-agent-npm/package.json" package = read_strict_json(package_path, "Engine MCP Codex installer package") if package.get("name") != "@nodedc/engine-codex-agent" or package.get("version") != "0.1.6": die("Engine MCP autonomy/provider v5 installer package identity mismatch") if any( marker not in gateway for marker in ( "const ENGINE_AGENT_MCP_VERSION = '0.6.0'", "authenticateEngineAgentOntologyToken", "apiRouter.post('/ontology-mcp'", "'nodedc-engine-codex-agent-0.1.6.tgz'", ) ): die("Engine MCP autonomy/provider v5 gateway contract is incomplete") if any( marker not in installer for marker in ( "MCP tool availability establishes capability authority", "the user's requested objective establishes intent scope", "machine safety barrier, not a permission ceremony", "Never repeat an identical read, validation, apply or run", "Three identical failures with no new evidence or state change are a critical stop", "const ONTOLOGY_SERVER_NAME = 'nodedc_ontology'", "version: '0.1.6'", ) ) or "only with explicit user confirmation" in installer: die("Engine MCP capability-scoped autonomy policy is incomplete") archive_path = payload_dir / "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.6.tgz" expected_members = { "package/bin/nodedc-engine-codex-agent.mjs": installer_path.read_bytes(), "package/package.json": package_path.read_bytes(), } observed_members = {} try: with tarfile.open(archive_path, "r:gz") as archive: for member in archive: if not (member.isfile() or member.isdir()) or member.name.startswith("/"): die("Engine MCP autonomy/provider v5 installer archive member is unsafe") if member.isfile(): source = archive.extractfile(member) if source is None: die("Engine MCP autonomy/provider v5 installer archive member is unreadable") observed_members[member.name] = source.read(MAX_FILE_BYTES + 1) except (OSError, tarfile.TarError): die("Engine MCP autonomy/provider v5 installer archive is invalid") if observed_members != expected_members: die("Engine MCP autonomy/provider v5 installer archive/source equality mismatch") catalog = read_strict_json( payload_dir / "nodedc-source/server/assets/provider-packages/v1/catalog.json", "Engine provider security catalog", ) packages = catalog.get("packages") if isinstance(catalog, dict) else None if ( catalog.get("schemaVersion") != "nodedc.engine.provider-security-catalog/v1" or not isinstance(packages, list) or [item.get("id") for item in packages if isinstance(item, dict)] != ["gelios.provider.v4", "gelios.provider.v5"] ): die("Engine Gelios provider v4/v5 catalog identity mismatch") expected_products = { "gelios.provider.v4": "fleet.positions.current.v3", "gelios.provider.v5": "fleet.positions.current.v4", } expected_requests = [ ("gelios.monitoring_config.current.read", "https://api.geliospro.com/api/v1/users/me/monitoring-config"), ("gelios.units.current.read", "https://api.geliospro.com/api/v1/units?incltrip=true"), ] for provider in packages: provider_id = provider.get("id") credential = provider.get("providerCredential") or {} capabilities = provider.get("capabilities") observed_requests = [ (item.get("id"), (item.get("request") or {}).get("url")) for item in capabilities ] if isinstance(capabilities, list) else None if ( provider_id not in expected_products or credential.get("credentialType") != "ndcProviderRotatingAccessApi" or credential.get("authModeId") != "gelios.rest-rotating-bearer.v3" or observed_requests != expected_requests or any(item.get("dataProductIds") != [expected_products[provider_id]] for item in capabilities) ): die("Engine Gelios provider v4/v5 security projection mismatch") store = (payload_dir / "nodedc-source/server/engineAgents/store.js").read_text( encoding="utf-8" ) if any( marker not in store for marker in ( "const STORE_VERSION = 3", "ontologyTokenHash", "authenticateEngineAgentOntologyToken", ) ): die("Engine MCP autonomy/provider v5 Ontology token store migration is incomplete") descriptor = read_engine_node_intelligence_descriptor( payload_dir / ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL, "Engine MCP autonomy/provider v5 node-intelligence descriptor", ) if ( descriptor.get("action") != "activate" or descriptor["source"].get("gatewaySha256") != ENGINE_MCP_AUTONOMY_PROVIDER_V5_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL] or descriptor["source"].get("upstreamProjectionSha256") != ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256[ "nodedc-source/server/nodeIntelligence/upstreamProjection.js" ] ): die("Engine MCP autonomy/provider v5 descriptor target mismatch") return descriptor def preflight_engine_mcp_autonomy_provider_v5_predecessor(payload_dir): candidate = validate_engine_mcp_autonomy_provider_v5_payload( payload_dir, ENGINE_MCP_AUTONOMY_PROVIDER_V5_ARTIFACT_ENTRIES, ) installed = current_engine_node_intelligence_descriptor() validate_installed_engine_node_intelligence_source(installed) if installed is None or installed.get("action") != "activate": die("Engine MCP autonomy/provider v5 requires active node intelligence") expected_candidate = json.loads(json.dumps(installed)) expected_candidate["source"]["gatewaySha256"] = ( ENGINE_MCP_AUTONOMY_PROVIDER_V5_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL] ) if candidate != expected_candidate: die("Engine MCP autonomy/provider v5 crosses node-intelligence source boundary") root = component_root("engine") for rel, expected_sha256 in ENGINE_MCP_AUTONOMY_PROVIDER_V5_PREDECESSOR_SHA256.items(): path = root / rel if path.is_symlink() or not path.is_file() or sha256_file(path) != expected_sha256: die(f"Engine MCP autonomy/provider v5 predecessor drift detected: {rel}") for rel in ENGINE_MCP_AUTONOMY_PROVIDER_V5_NEW_PATHS: path = root / rel if path.exists() or path.is_symlink(): die(f"Engine MCP autonomy/provider v5 new path already exists: {rel}") backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine MCP autonomy/provider v5 requires the active immutable backend runtime") return { "descriptor": candidate, "predecessor_gateway_sha256": installed["source"]["gatewaySha256"], "target_gateway_sha256": candidate["source"]["gatewaySha256"], "backend_mode": backend["mode"], } def engine_mcp_autonomy_provider_v5_installed_state(): root = component_root("engine") target = all( (root / rel).is_file() and not (root / rel).is_symlink() and sha256_file(root / rel) == expected for rel, expected in ENGINE_MCP_AUTONOMY_PROVIDER_V5_TARGET_SHA256.items() ) descriptor = current_engine_node_intelligence_descriptor() if descriptor is not None: validate_installed_engine_node_intelligence_source(descriptor) if target and descriptor and descriptor.get("source", {}).get("gatewaySha256") == ( ENGINE_MCP_AUTONOMY_PROVIDER_V5_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL] ): return "target" predecessor = all( (root / rel).is_file() and not (root / rel).is_symlink() and sha256_file(root / rel) == expected for rel, expected in ENGINE_MCP_AUTONOMY_PROVIDER_V5_PREDECESSOR_SHA256.items() ) new_paths_absent = all( not (root / rel).exists() and not (root / rel).is_symlink() for rel in ENGINE_MCP_AUTONOMY_PROVIDER_V5_NEW_PATHS ) if predecessor and new_paths_absent and descriptor and descriptor.get("source", {}).get("gatewaySha256") == ( ENGINE_MCP_AUTONOMY_PROVIDER_V5_PREDECESSOR_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL] ): return "predecessor" die("Engine MCP autonomy/provider v5 installed state is neither target nor rollback predecessor") def accept_engine_mcp_autonomy_provider_v5_runtime(): root = component_root("engine") descriptor = validate_engine_mcp_autonomy_provider_v5_payload( root, ENGINE_MCP_AUTONOMY_PROVIDER_V5_ARTIFACT_ENTRIES, ) installed = current_engine_node_intelligence_descriptor() if installed != descriptor: die("Engine MCP autonomy/provider v5 installed descriptor equality failed") validate_installed_engine_node_intelligence_source(installed) backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine MCP autonomy/provider v5 backend runtime acceptance failed") live = run_engine_backend_probe( ( "node", "--input-type=module", "-e", """ const gateway=await import('file:///app/server/routes/engineAgentGateway.js'); const store=await import('file:///app/server/engineAgents/store.js'); const fs=await import('node:fs/promises'); const catalog=JSON.parse(await fs.readFile('/app/server/assets/provider-packages/v1/catalog.json','utf8')); const pkg=JSON.parse(await fs.readFile('/app/server/assets/engine-agent-npm/package.json','utf8')); const ids=catalog.packages?.map((item)=>item.id).join(','); const target=catalog.packages?.find((item)=>item.id==='gelios.provider.v5'); if(gateway.ENGINE_AGENT_MCP_VERSION!=='0.6.0')process.exit(2); if(typeof store.authenticateEngineAgentOntologyToken!=='function')process.exit(3); if(pkg.version!=='0.1.6'||ids!=='gelios.provider.v4,gelios.provider.v5')process.exit(4); if(target?.capabilities?.some((item)=>item.dataProductIds?.[0]!=='fleet.positions.current.v4'))process.exit(5); process.stdout.write('engine-mcp-autonomy-provider-v5:0.6.0:0.1.6:'+ids); """.strip(), ), "Engine MCP autonomy/provider v5 capability", container_id=engine_backend_container_id(), ) expected = "engine-mcp-autonomy-provider-v5:0.6.0:0.1.6:gelios.provider.v4,gelios.provider.v5" if live != expected: die("Engine MCP autonomy/provider v5 live capability acceptance mismatch") return { "gateway_sha256": descriptor["source"]["gatewaySha256"], "backend_mode": backend["mode"], "capability": live, } def is_engine_provider_security_catalog_slice(component, entries): return ( component == "engine" and entries is not None and tuple(entries) == ENGINE_PROVIDER_SECURITY_CATALOG_ARTIFACT_ENTRIES ) def validate_engine_provider_security_catalog_payload(payload_dir, entries): if tuple(entries) != ENGINE_PROVIDER_SECURITY_CATALOG_ARTIFACT_ENTRIES: die("Engine provider security catalog files.txt exact set/order mismatch") catalog_path = payload_dir / ENGINE_PROVIDER_SECURITY_CATALOG_REL if ( catalog_path.is_symlink() or not catalog_path.is_file() or sha256_file(catalog_path) != ENGINE_PROVIDER_SECURITY_CATALOG_TARGET_SHA256 ): die("Engine provider security catalog target sha256 mismatch") catalog = read_strict_json(catalog_path, "Engine provider security catalog") packages = catalog.get("packages") if isinstance(catalog, dict) else None package_ids = [item.get("id") for item in packages if isinstance(item, dict)] \ if isinstance(packages, list) else None provider = next( ( item for item in packages if isinstance(item, dict) and item.get("id") == "gelios.provider.v8" ), None, ) if isinstance(packages, list) else None previous_provider = next( ( item for item in packages if isinstance(item, dict) and item.get("id") == "gelios.provider.v7" ), None, ) if isinstance(packages, list) else None capabilities = provider.get("capabilities") if isinstance(provider, dict) else None capability = capabilities[0] if isinstance(capabilities, list) and len(capabilities) == 1 else None request = capability.get("request") if isinstance(capability, dict) else None credential = provider.get("providerCredential") if isinstance(provider, dict) else None publisher = provider.get("publisher") if isinstance(provider, dict) else None if ( catalog.get("schemaVersion") != "nodedc.engine.provider-security-catalog/v1" or package_ids != [ "gelios.provider.v1", "gelios.provider.v4", "gelios.provider.v5", "moscow-department-of-transport.pmd-slow-zones.v1", "gelios.provider.v7", "gelios.provider.v8", ] or not isinstance(provider, dict) or provider.get("version") != "8.0.0" or provider.get("providerId") != "gelios" or credential != { "authModeId": "gelios.rest-rotating-bearer.v3", "credentialType": "ndcProviderRotatingAccessApi", } or not isinstance(capability, dict) or capability.get("id") != "gelios.units.current.read" or capability.get("classification") != "read" or capability.get("status") != "implemented" or request != { "method": "GET", "url": ( "https://api.geliospro.com/api/v1/units?incltrip=true&inclcntrs=true" "&inclsnsrs=true&incllsv=true" ), } or capability.get("dataProductIds") != ["fleet.units.profile.current.v1"] or publisher != { "nodeType": "n8n-nodes-ndc.ndcDataProductPublish", "credentialType": "ndcDataProductWriterApi", } or previous_provider != { "id": "gelios.provider.v7", "version": "7.0.0", "providerId": "gelios", "providerCredential": { "authModeId": "gelios.rest-rotating-bearer.v3", "credentialType": "ndcProviderRotatingAccessApi", }, "capabilities": [ { "id": "gelios.units.current.read", "classification": "read", "status": "implemented", "request": { "method": "GET", "url": ( "https://api.geliospro.com/api/v1/units?incltrip=true" "&inclcntrs=true&inclsnsrs=true&incllsv=true" ), }, "dataProductIds": ["fleet.positions.current.v5"], }, ], "publisher": { "nodeType": "n8n-nodes-ndc.ndcDataProductPublish", "credentialType": "ndcDataProductWriterApi", }, } ): die("Engine provider security catalog v7/v8 projection mismatch") return catalog def preflight_engine_provider_security_catalog_predecessor(): catalog_path = component_root("engine") / ENGINE_PROVIDER_SECURITY_CATALOG_REL try: path_stat = catalog_path.lstat() except FileNotFoundError: die("Engine provider security catalog predecessor is missing") if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode): die("Engine provider security catalog predecessor is unsafe") actual_sha256 = sha256_file(catalog_path) if actual_sha256 != ENGINE_PROVIDER_SECURITY_CATALOG_PREDECESSOR_SHA256: die( "Engine provider security catalog predecessor drift detected: " f"expected={ENGINE_PROVIDER_SECURITY_CATALOG_PREDECESSOR_SHA256} " f"actual={actual_sha256}" ) backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine provider security catalog requires the active immutable backend") return {"catalog_sha256": actual_sha256, "backend_mode": backend["mode"]} def accept_engine_provider_security_catalog_runtime(): root = component_root("engine") validate_engine_provider_security_catalog_payload( root, ENGINE_PROVIDER_SECURITY_CATALOG_ARTIFACT_ENTRIES, ) live = run_engine_backend_probe( ( "node", "--input-type=module", "-e", """ const module=await import('file:///app/server/dataProductPublishGrant/providerCatalog.js'); const catalog=await module.loadProviderSecurityCatalog(); const validation=module.validateProviderSecurityCatalog(catalog); const provider=catalog.packages?.find((item)=>item.id==='gelios.provider.v8'); const capability=provider?.capabilities?.[0]; const slot='ndcProviderRotatingAccessApi'; const credential={id:'native-provider',name:'provider',nodeDcCredentialId:'provider-ref'}; const policy={kind:'httpRequest',allowedHosts:['api.geliospro.com'],requireHttps:true,disableRedirects:true}; const queryParameters={parameters:[{name:'incltrip',value:'true'},{name:'inclcntrs',value:'true'},{name:'inclsnsrs',value:'true'},{name:'incllsv',value:'true'}]}; const source={id:'units',data:{n8n:{id:'units',name:'units',type:'n8n-nodes-base.httpRequest',parameters:{method:'GET',url:'https://api.geliospro.com/api/v1/units',sendQuery:true,specifyQuery:'keypair',queryParameters},credentials:{[slot]:credential}},nodedcAgentCredentialPolicies:{[slot]:policy}}}; const profilePublish={id:'profile-publish',data:{n8n:{id:'profile-publish',name:'profile-publish',type:'n8n-nodes-ndc.ndcDataProductPublish',parameters:{dataProductId:'fleet.units.profile.current.v1'},credentials:{}}}}; const positionsPublish={id:'positions-publish',data:{n8n:{id:'positions-publish',name:'positions-publish',type:'n8n-nodes-ndc.ndcDataProductPublish',parameters:{dataProductId:'fleet.positions.current.v5'},credentials:{}}}}; const profileGraph={nodes:[source,profilePublish],edges:[{source:'units',target:'profile-publish'}]}; const positionsGraph={nodes:[source,positionsPublish],edges:[{source:'units',target:'positions-publish'}]}; const profileResolved=module.resolveProviderConnectionFromGraph({graph:profileGraph,targetNodeId:'profile-publish',catalog}); const positionsResolved=module.resolveProviderConnectionFromGraph({graph:positionsGraph,targetNodeId:'positions-publish',catalog}); const drift=structuredClone(profileGraph);drift.nodes[0].data.n8n.parameters.queryParameters.parameters.pop(); const rejected=module.resolveProviderConnectionFromGraph({graph:drift,targetNodeId:'profile-publish',catalog}); if(!validation.ok||provider?.providerCredential?.credentialType!==slot||capability?.request?.url!=='https://api.geliospro.com/api/v1/units?incltrip=true&inclcntrs=true&inclsnsrs=true&incllsv=true'||capability?.dataProductIds?.[0]!=='fleet.units.profile.current.v1'||!profileResolved.ok||profileResolved.descriptor?.packageId!=='gelios.provider.v8'||profileResolved.descriptor?.providerCredentialRef!=='provider-ref'||!positionsResolved.ok||positionsResolved.descriptor?.packageId!=='gelios.provider.v7'||positionsResolved.descriptor?.dataProductId!=='fleet.positions.current.v5'||rejected.blockers?.join(',')!=='publish_grant_provider_request_not_exact')process.exit(2); process.stdout.write('engine-provider-catalog:gelios.provider.v7:fleet.positions.current.v5|gelios.provider.v8:fleet.units.profile.current.v1'); """.strip(), ), "Engine provider security catalog v8 unit profile", container_id=engine_backend_container_id(), ) expected = ( "engine-provider-catalog:gelios.provider.v7:fleet.positions.current.v5|" "gelios.provider.v8:fleet.units.profile.current.v1" ) if live != expected: die("Engine provider security catalog live acceptance mismatch") return {"catalog_sha256": ENGINE_PROVIDER_SECURITY_CATALOG_TARGET_SHA256, "live": live} def accept_engine_composite_provider_runtime(): catalog_path = component_root("engine") / ENGINE_PROVIDER_SECURITY_CATALOG_REL if ( catalog_path.is_symlink() or not catalog_path.is_file() or sha256_file(catalog_path) != ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_TARGET_SHA256 ): return None live = run_engine_backend_probe( ( "node", "--input-type=module", "-e", """ const module=await import('file:///app/server/dataProductPublishGrant/providerCatalog.js'); const catalog=await module.loadProviderSecurityCatalog(); const validation=module.validateProviderSecurityCatalog(catalog); const credential={id:'native-provider',name:'provider',nodeDcCredentialId:'provider-ref'}; const policy={kind:'httpRequest',allowedHosts:['api.geliospro.com'],requireHttps:true,disableRedirects:true}; const slot='ndcProviderRotatingAccessApi'; const read=(id,url,parameters={})=>({id,data:{n8n:{id,name:id,type:'n8n-nodes-base.httpRequest',parameters:{method:'GET',url,...parameters},credentials:{[slot]:credential}},nodedcAgentCredentialPolicies:{[slot]:policy}}}); const query={sendQuery:true,specifyQuery:'keypair',queryParameters:{parameters:[{name:'incltrip',value:'true'}]}}; const graph={nodes:[read('monitoring','https://api.geliospro.com/api/v1/users/me/monitoring-config'),read('units','https://api.geliospro.com/api/v1/units',query),{id:'publish',data:{n8n:{id:'publish',name:'publish',type:'n8n-nodes-ndc.ndcDataProductPublish',parameters:{dataProductId:'fleet.positions.current.v3'},credentials:{}}}}],edges:[{source:'monitoring',target:'publish'},{source:'units',target:'publish'}]}; const resolved=module.resolveProviderConnectionFromGraph({graph,targetNodeId:'publish',catalog}); if(!validation.ok||catalog.packages?.[0]?.id!=='gelios.provider.v4'||catalog.packages?.[0]?.providerCredential?.credentialType!==slot||!resolved.ok||resolved.descriptor?.providerCredentialRef!=='provider-ref'||resolved.descriptor?.capabilityIds?.join(',')!=='gelios.monitoring_config.current.read,gelios.units.current.read'||resolved.descriptor?.providerRequestNodeIds?.join(',')!=='monitoring,units')process.exit(2); process.stdout.write('engine-composite-provider:gelios.provider.v4:ndcProviderRotatingAccessApi:fleet.positions.current.v3:monitoring,units'); """.strip(), ), "Engine composite provider authority", container_id=engine_backend_container_id(), ) expected = ( "engine-composite-provider:gelios.provider.v4:" "ndcProviderRotatingAccessApi:fleet.positions.current.v3:monitoring,units" ) if live != expected: die("Engine composite provider live acceptance mismatch") return { "catalog_sha256": ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_TARGET_SHA256, "live": live, } def accept_engine_provider_rotating_slot_runtime(): root = component_root("engine") validate_engine_provider_rotating_slot_slice( root, ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES, ) live = run_engine_backend_probe( ( "node", "--input-type=module", "-e", """ const module=await import('file:///app/server/dataProductPublishGrant/providerCatalog.js'); const catalog=await module.loadProviderSecurityCatalog(); const validation=module.validateProviderSecurityCatalog(catalog); const credential={id:'native-provider',name:'provider',nodeDcCredentialId:'provider-ref'}; const policy={kind:'httpRequest',allowedHosts:['api.geliospro.com'],requireHttps:true,disableRedirects:true}; const slot='ndcProviderRotatingAccessApi'; const read=(id,url,parameters={})=>({id,data:{n8n:{id,name:id,type:'n8n-nodes-base.httpRequest',parameters:{method:'GET',url,...parameters},credentials:{[slot]:credential}},nodedcAgentCredentialPolicies:{[slot]:policy}}}); const query={sendQuery:true,specifyQuery:'keypair',queryParameters:{parameters:[{name:'incltrip',value:'true'}]}}; const graph={nodes:[read('monitoring','https://api.geliospro.com/api/v1/users/me/monitoring-config'),read('units','https://api.geliospro.com/api/v1/units',query),{id:'publish',data:{n8n:{id:'publish',name:'publish',type:'n8n-nodes-ndc.ndcDataProductPublish',parameters:{dataProductId:'fleet.positions.current.v3'},credentials:{}}}}],edges:[{source:'monitoring',target:'publish'},{source:'units',target:'publish'}]}; const resolved=module.resolveProviderConnectionFromGraph({graph,targetNodeId:'publish',catalog}); if(!validation.ok||catalog.packages?.[0]?.id!=='gelios.provider.v4'||catalog.packages?.[0]?.providerCredential?.credentialType!==slot||!resolved.ok||resolved.descriptor?.providerCredentialRef!=='provider-ref'||resolved.descriptor?.capabilityIds?.join(',')!=='gelios.monitoring_config.current.read,gelios.units.current.read'||resolved.descriptor?.providerRequestNodeIds?.join(',')!=='monitoring,units')process.exit(2); process.stdout.write('engine-provider-rotating-slot:gelios.provider.v4:ndcProviderRotatingAccessApi:fleet.positions.current.v3:monitoring,units'); """.strip(), ), "Engine provider rotating slot authority", container_id=engine_backend_container_id(), ) expected = ( "engine-provider-rotating-slot:gelios.provider.v4:" "ndcProviderRotatingAccessApi:fleet.positions.current.v3:monitoring,units" ) if live != expected: die("Engine provider rotating slot live acceptance mismatch") return { "target_sha256": dict(ENGINE_PROVIDER_ROTATING_SLOT_TARGET_SHA256), "live": live, } def accept_engine_provider_authority_diagnostics_runtime(): root = component_root("engine") validate_engine_provider_authority_diagnostics_slice( root, ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES, ) live = run_engine_backend_probe( ( "node", "--input-type=module", "-e", """ const module=await import('file:///app/server/nodeIntelligence/upstreamProjection.js'); const privateValidation={nodeId:'private-1',nodeName:'NDC Data Product Publish',valid:true,errors:[],warnings:[]}; const privateOnly=module.reconcilePrivateNodeValidation({valid:false,errorCount:1,warningCount:0,summary:{errorCount:1,warningCount:0},errors:[{node:'NDC Data Product Publish',message:'Unknown node type: \"ndcDataProductPublish\".'}],warnings:[]},['private-1'],[privateValidation]); const mixed=module.reconcilePrivateNodeValidation({valid:false,errorCount:2,warningCount:0,summary:{errorCount:2,warningCount:0},errors:[{node:'NDC Data Product Publish',message:'Unknown node type: \"ndcDataProductPublish\".'},{nodeId:'built-in-1',nodeName:'HTTP',message:'URL is required'}],warnings:[]},['private-1'],[privateValidation]); if(!privateOnly.valid||privateOnly.errors?.length!==0||privateOnly.summary?.errorCount!==0||privateOnly.summary?.warningCount!==1||mixed.valid||mixed.errors?.length!==1||mixed.errors?.[0]?.nodeId!=='built-in-1')process.exit(2); process.stdout.write('engine-private-node-reconciliation:exact-id-or-name:v1'); """.strip(), ), "Engine private node validation reconciliation", container_id=engine_backend_container_id(), ) expected = "engine-private-node-reconciliation:exact-id-or-name:v1" if live != expected: die("Engine provider authority diagnostics live acceptance mismatch") return { "target_sha256": dict(ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256), "live": live, } def accept_engine_depttrans_zone_authority_v1_runtime(): root = component_root("engine") validate_engine_depttrans_zone_authority_v1_slice( root, ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES, ) live = run_engine_backend_probe( ( "node", "--input-type=module", "-e", """ const module=await import('file:///app/server/dataProductPublishGrant/providerCatalog.js'); const catalog=await module.loadProviderSecurityCatalog(); const parameters={method:'GET',url:'http://map-gateway:18103/internal/zone-sources/v1/profiles/moscow-pmd-slow-zones/current',authentication:'none',sendQuery:false,sendHeaders:true,specifyHeaders:'keypair',headerParameters:{parameters:[{name:'Accept',value:'application/json'}]},sendBody:false,options:{response:{response:{fullResponse:false,responseFormat:'json'}},allowUnauthorizedCerts:false,redirect:{redirect:{followRedirects:false}},timeout:15000}}; const graph={nodes:[{id:'source',data:{n8n:{id:'source',name:'source',type:'n8n-nodes-base.httpRequest',parameters}}},{id:'publish',data:{n8n:{id:'publish',name:'publish',type:'n8n-nodes-ndc.ndcDataProductPublish',parameters:{dataProductId:'map.zones.current.v2',publishMode:'replace'},credentials:{}}}}],edges:[{source:'source',target:'publish'}]}; const resolved=module.resolveProviderConnectionFromGraph({graph,targetNodeId:'publish',catalog}); const drift=structuredClone(graph);drift.nodes[0].data.n8n.parameters.options.redirect.redirect.followRedirects=true; const rejected=module.resolveProviderConnectionFromGraph({graph:drift,targetNodeId:'publish',catalog}); if(!module.validateProviderSecurityCatalog(catalog).ok||!resolved.ok||resolved.descriptor?.providerId!=='moscow-department-of-transport'||resolved.descriptor?.authorityBoundary!=='platform-service'||resolved.descriptor?.providerCredentialRef!==null||resolved.descriptor?.platformServiceIds?.join(',')!=='nodedc-map-gateway'||rejected.blockers?.join(',')!=='publish_grant_provider_request_not_exact')process.exit(2); process.stdout.write('engine-depttrans-zone-authority:platform-service:map.zones.current.v2:nodedc-map-gateway'); """.strip(), ), "Engine Depttrans zone authority v1", container_id=engine_backend_container_id(), ) expected = ( "engine-depttrans-zone-authority:platform-service:" "map.zones.current.v2:nodedc-map-gateway" ) if live != expected: die("Engine Depttrans zone authority v1 live acceptance mismatch") return { "target_sha256": dict(ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_TARGET_SHA256), "live": live, } def accept_engine_provider_target_host_policy_runtime(): root = component_root("engine") validate_engine_provider_target_host_policy_slice( root, ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES, ) live = run_engine_backend_probe( ( "node", "--input-type=module", "-e", """ const module=await import('file:///app/server/routes/n8n.js'); const slot='ndcProviderRotatingAccessApi'; const entry={id:'provider',name:'provider',type:slot,nodeDcCredentialId:'provider-ref',data:{engineAgentAllowedHttpHosts:['*.geliospro.com','telemetry.example']}}; const target={type:'n8n-nodes-base.httpRequest',parameters:{url:'https://api.geliospro.com/api/v1/units'}}; const deniedTarget={type:'n8n-nodes-base.httpRequest',parameters:{url:'https://untrusted.example/collect'}}; const allowed=module.buildEngineAgentCredentialTransportPolicy(entry,{nodes:[]},slot,target); const denied=module.buildEngineAgentCredentialTransportPolicy(entry,{nodes:[]},slot,deniedTarget); if(!allowed.bindable||allowed.policy?.allowedHosts?.join(',')!=='api.geliospro.com'||allowed.policy?.requireHttps!==true||allowed.policy?.disableRedirects!==true||denied.bindable||denied.reason!=='credential_host_not_allowed')process.exit(2); process.stdout.write('engine-provider-target-host-policy:exact-literal-host:v1'); """.strip(), ), "Engine provider target host policy", container_id=engine_backend_container_id(), ) expected = "engine-provider-target-host-policy:exact-literal-host:v1" if live != expected: die("Engine provider target host policy live acceptance mismatch") return { "target_sha256": dict(ENGINE_PROVIDER_TARGET_HOST_POLICY_TARGET_SHA256), "live": live, } def validate_no_lifecycle_scripts(payload_dir, label): forbidden = ("preinstall", "install", "postinstall", "prepare", "prepack", "postpack") for package_path in payload_dir.rglob("package.json"): package = read_strict_json(package_path, f"{label} package.json", max_bytes=1024 * 1024) scripts = package.get("scripts") or {} if not isinstance(scripts, dict): die(f"{label} package scripts must be an object") for name in forbidden: if name in scripts: die(f"{label} lifecycle script is forbidden: {package_path.relative_to(payload_dir)}:{name}") def expected_engine_credential_backend_override(): return "\n".join(( "services:", " nodedc-backend:", f" image: {ENGINE_CREDENTIAL_BACKEND_IMAGE}", " pull_policy: never", " environment:", " HOME: /tmp", " command:", " - /bin/sh", " - -lc", " - |-|".replace("|-|", "|-"), " set -eu", " test \"$$(command -v node)\" = /usr/local/bin/node", " command -v sqlite3 >/dev/null", " command -v docker >/dev/null", " docker compose version >/dev/null", f" test \"$$(sha256sum /app/package-lock.json | cut -d ' ' -f 1)\" = {ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256}", " test -d /app/node_modules", " mkdir -p /app/server/data/api", " if [ -d /seed-api ]; then", " for d in cesium gelios overpass wind yandex; do", " src=/seed-api/$$d", " [ -d \"$$src\" ] || continue", " dst=/app/server/data/api/$$d", " mkdir -p \"$$dst\"", " if [ \"$$NODEDC_API_SEED_MODE\" = force ]; then", " cp -R \"$$src/.\" \"$$dst/\"", " elif [ -z \"$$(ls -A \"$$dst\" 2>/dev/null)\" ]; then", " cp -R \"$$src/.\" \"$$dst/\"", " fi", " done", " fi", " if [ \"$$NODEDC_DATA_SEED_MODE\" != off ] && [ \"$$NODEDC_DATA_SEED_MODE\" != none ]; then", " for d in workflows n8n; do", " src=/seed-data/$$d", " dst=/app/server/data/$$d", " [ -d \"$$src\" ] || continue", " mkdir -p \"$$dst\"", " if [ \"$$NODEDC_DATA_SEED_MODE\" = merge ] || [ \"$$NODEDC_DATA_SEED_MODE\" = force ]; then", " cp -R \"$$src/.\" \"$$dst/\"", " elif [ -z \"$$(ls -A \"$$dst\" 2>/dev/null)\" ]; then", " cp -R \"$$src/.\" \"$$dst/\"", " fi", " done", " fi", " exec node server/index.js", " volumes:", " - ./nodedc-backend-node_modules:/app/node_modules:ro", " tmpfs:", " - /tmp:mode=1777", " - /run:mode=0755", " - /var/cache:mode=0755", " - /var/log:mode=0755", "", )) def expected_engine_data_product_publish_grant_override(): return "\n".join(( "services:", " nodedc-backend:", ' user: "0:0"', " environment:", f" ENGINE_DATA_PLANE_BASE_URL: {EXTERNAL_DATA_PLANE_INTERNAL_URL}", f" ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE: {ENGINE_EDP_PRIVATE_KEY_CONTAINER_PATH}", f" ENGINE_CONTROL_PLANE_PUBLISH_GRANT_ROOT: {ENGINE_PUBLISH_GRANT_CONTAINER_PATH}", " volumes:", " - type: bind", f" source: {ENGINE_PUBLISH_GRANT_STATE_PATH}", f" target: {ENGINE_PUBLISH_GRANT_CONTAINER_PATH}", " bind:", " create_host_path: false", " - type: bind", f" source: {ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE}", f" target: {ENGINE_EDP_PRIVATE_KEY_CONTAINER_PATH}", " read_only: true", " bind:", " create_host_path: false", "", )) def expected_engine_data_product_read_grant_override(): return "\n".join(( "services:", " nodedc-backend:", " environment:", f" ENGINE_CONTROL_PLANE_READ_GRANT_ROOT: {ENGINE_READ_GRANT_CONTAINER_PATH}", " volumes:", " - type: bind", f" source: {ENGINE_READ_GRANT_STATE_PATH}", f" target: {ENGINE_READ_GRANT_CONTAINER_PATH}", " bind:", " create_host_path: false", "", )) def validate_engine_credential_sink_slice(payload_dir, entries): if tuple(entries) != ENGINE_CREDENTIAL_SINK_ARTIFACT_ENTRIES: die("Engine credential sink files.txt exact set/order mismatch") index_text = (payload_dir / "nodedc-source/server/index.js").read_text(encoding="utf-8") if "engineCredentialSink" not in index_text: die("Engine credential sink router mount is missing") vendor_contract = payload_dir / "nodedc-source/server/credentialSink/vendor/engine-credential-sink.mjs" if sha256_file(vendor_contract) != ENGINE_CREDENTIAL_SINK_CONTRACT_SHA256: die("Engine credential sink vendored contract sha256 mismatch") override = payload_dir / ENGINE_CREDENTIAL_BACKEND_OVERRIDE_TEMPLATE_REL try: override_text = override.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): die("Engine credential immutable runtime override is unreadable") if override_text != expected_engine_credential_backend_override(): die("Engine credential immutable runtime override mismatch") if re.search(r"\b(?:apk|npm|yarn|pnpm)\b", override_text): die("Engine credential mutable runtime setup is forbidden") generic_roots = ( payload_dir / "nodedc-source/server/credentialSink", payload_dir / "nodedc-source/server/credentialPolicies/ndcPrivateNode.js", payload_dir / "nodedc-source/server/routes/engineCredentialSink.js", ) for root in generic_roots: candidates = [root] if root.is_file() else list(root.rglob("*")) for path in candidates: if not path.is_file() or path.suffix.lower() not in (".js", ".mjs", ".json"): continue try: text_value = path.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): die(f"Engine credential sink source is unreadable: {path.relative_to(payload_dir)}") if re.search(r"gelios|robot2b", text_value, re.IGNORECASE): die(f"Engine credential sink provider logic is forbidden: {path.relative_to(payload_dir)}") validate_no_lifecycle_scripts(payload_dir, "Engine credential sink") def validate_engine_data_product_publish_grant_slice(payload_dir, entries): if tuple(entries) != ENGINE_DATA_PRODUCT_PUBLISH_GRANT_ARTIFACT_ENTRIES: die("Engine data product publish grant files.txt exact set/order mismatch") override = payload_dir / ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL try: override_text = override.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): die("Engine data product publish grant runtime override is unreadable") if override_text != expected_engine_data_product_publish_grant_override(): die("Engine data product publish grant runtime override mismatch") n8n_route = (payload_dir / "nodedc-source/server/routes/n8n.js").read_text(encoding="utf-8") if "from '../credentialSink/" in n8n_route: die("Engine data product publish grant must not depend on the legacy sink") for adapter in ( "engineCredentialSinkN8nAdapter", "engineDataProductPublishGrantN8nAdapter", ): if adapter not in n8n_route: die(f"Engine data product publish grant adapter is missing: {adapter}") gateway = (payload_dir / "nodedc-source/server/routes/engineAgentGateway.js").read_text( encoding="utf-8" ) for tool in ( "engine_plan_data_product_publish_grant", "engine_apply_data_product_publish_grant", "engine_accept_data_product_publish_grant", "engine_rollback_data_product_publish_grant", ): if tool not in gateway: die(f"Engine data product publish grant tool is missing: {tool}") catalog_path = payload_dir / ENGINE_PROVIDER_SECURITY_CATALOG_REL if sha256_file(catalog_path) == ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_TARGET_SHA256: catalog = read_strict_json(catalog_path, "Engine composite provider security catalog") packages = catalog.get("packages") if isinstance(catalog, dict) else None provider = packages[0] if isinstance(packages, list) and len(packages) == 1 else None capabilities = provider.get("capabilities") if isinstance(provider, dict) else None if ( catalog.get("schemaVersion") != "nodedc.engine.provider-security-catalog/v1" or not isinstance(provider, dict) or provider.get("id") != "gelios.provider.v4" or provider.get("version") != "4.0.0" or provider.get("providerId") != "gelios" or provider.get("providerCredential") != { "authModeId": "gelios.rest-rotating-bearer.v3", "credentialType": "httpBearerAuth", } or capabilities != [{ "id": "gelios.monitoring_config.current.read", "classification": "read", "status": "implemented", "request": { "method": "GET", "url": "https://api.geliospro.com/api/v1/users/me/monitoring-config", }, "dataProductIds": ["fleet.positions.current.v3"], }, { "id": "gelios.units.current.read", "classification": "read", "status": "implemented", "request": { "method": "GET", "url": "https://api.geliospro.com/api/v1/units?incltrip=true", }, "dataProductIds": ["fleet.positions.current.v3"], }] or provider.get("publisher") != { "nodeType": "n8n-nodes-ndc.ndcDataProductPublish", "credentialType": "ndcDataProductWriterApi", } ): die("Engine composite provider security catalog projection mismatch") provider_catalog = ( payload_dir / "nodedc-source/server/dataProductPublishGrant/providerCatalog.js" ).read_text(encoding="utf-8") publish_service = ( payload_dir / "nodedc-source/server/dataProductPublishGrant/service.js" ).read_text(encoding="utf-8") publish_store = ( payload_dir / "nodedc-source/server/dataProductPublishGrant/store.js" ).read_text(encoding="utf-8") if any( marker not in provider_catalog for marker in ( "function capabilityRequests(capability)", "function exactHttpRequestUrl(n8n)", "capabilityIds", "providerRequestNodeIds", "providerCredentialRefs.size !== 1", ) ): die("Engine composite provider resolver contract mismatch") if any( marker not in publish_service for marker in ( "capabilities: descriptor.capabilityIds", "providerRequestNodeIds: descriptor.providerRequestNodeIds", ) ): die("Engine composite provider plan projection mismatch") if any( marker not in publish_store for marker in ( "Array.isArray(raw.capabilityIds)", "Array.isArray(raw.providerRequestNodeIds)", "new Set(descriptor.providerRequestNodeIds).size", ) ): die("Engine composite provider durable descriptor mismatch") validate_no_lifecycle_scripts(payload_dir, "Engine data product publish grant") def validate_engine_composite_provider_v4_slice(payload_dir, entries): if tuple(entries) != ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES: die("Engine composite provider v4 files.txt exact set/order mismatch") for rel, expected_sha256 in ENGINE_COMPOSITE_PROVIDER_V4_TARGET_SHA256.items(): path = payload_dir / rel try: path_stat = path.lstat() except FileNotFoundError: die(f"Engine composite provider v4 target is missing: {rel}") if ( stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode) or sha256_file(path) != expected_sha256 ): die(f"Engine composite provider v4 target sha256 mismatch: {rel}") catalog = read_strict_json( payload_dir / ENGINE_PROVIDER_SECURITY_CATALOG_REL, "Engine composite provider v4 catalog", ) packages = catalog.get("packages") if isinstance(catalog, dict) else None provider = packages[0] if isinstance(packages, list) and len(packages) == 1 else None capabilities = provider.get("capabilities") if isinstance(provider, dict) else None if ( catalog.get("schemaVersion") != "nodedc.engine.provider-security-catalog/v1" or not isinstance(provider, dict) or provider.get("id") != "gelios.provider.v4" or provider.get("version") != "4.0.0" or provider.get("providerCredential") != { "authModeId": "gelios.rest-rotating-bearer.v3", "credentialType": "httpBearerAuth", } or not isinstance(capabilities, list) or [item.get("id") for item in capabilities if isinstance(item, dict)] != [ "gelios.monitoring_config.current.read", "gelios.units.current.read", ] or [item.get("dataProductIds") for item in capabilities if isinstance(item, dict)] != [ ["fleet.positions.current.v3"], ["fleet.positions.current.v3"], ] or [ (item.get("request") or {}).get("url") for item in capabilities if isinstance(item, dict) ] != [ "https://api.geliospro.com/api/v1/users/me/monitoring-config", "https://api.geliospro.com/api/v1/units?incltrip=true", ] ): die("Engine composite provider v4 projection mismatch") source_markers = { "nodedc-source/server/dataProductPublishGrant/providerCatalog.js": ( "function exactHttpRequestUrl(n8n)", "const capabilityIds = productCapabilities.map", "providerCredentialRefs.size !== 1", ), "nodedc-source/server/dataProductPublishGrant/service.js": ( "capabilities: descriptor.capabilityIds", "providerRequestNodeIds: descriptor.providerRequestNodeIds", ), "nodedc-source/server/dataProductPublishGrant/store.js": ( "Array.isArray(raw.capabilityIds)", "Array.isArray(raw.providerRequestNodeIds)", ), } for rel, markers in source_markers.items(): source = (payload_dir / rel).read_text(encoding="utf-8") if any(marker not in source for marker in markers): die(f"Engine composite provider v4 source contract mismatch: {rel}") def validate_engine_provider_rotating_slot_slice(payload_dir, entries): if tuple(entries) != ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES: die("Engine provider rotating slot files.txt exact set/order mismatch") for rel, expected_sha256 in ENGINE_PROVIDER_ROTATING_SLOT_TARGET_SHA256.items(): path = payload_dir / rel try: path_stat = path.lstat() except FileNotFoundError: die(f"Engine provider rotating slot target is missing: {rel}") if ( stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode) or sha256_file(path) != expected_sha256 ): die(f"Engine provider rotating slot target sha256 mismatch: {rel}") catalog = read_strict_json( payload_dir / ENGINE_PROVIDER_SECURITY_CATALOG_REL, "Engine provider rotating slot catalog", ) packages = catalog.get("packages") if isinstance(catalog, dict) else None provider = packages[0] if isinstance(packages, list) and len(packages) == 1 else None if ( not isinstance(provider, dict) or provider.get("id") != "gelios.provider.v4" or provider.get("providerCredential") != { "authModeId": "gelios.rest-rotating-bearer.v3", "credentialType": "ndcProviderRotatingAccessApi", } ): die("Engine provider rotating slot catalog projection mismatch") resolver = ( payload_dir / "nodedc-source/server/dataProductPublishGrant/providerCatalog.js" ).read_text(encoding="utf-8") if ( "'ndcProviderRotatingAccessApi'" not in resolver or "n8n.credentials?.[providerPackage.providerCredential.credentialType]" not in resolver or "nodedcAgentCredentialPolicies?.[providerPackage.providerCredential.credentialType]" not in resolver ): die("Engine provider rotating slot resolver contract mismatch") def validate_engine_provider_authority_diagnostics_slice(payload_dir, entries): if tuple(entries) != ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES: die("Engine provider authority diagnostics files.txt exact set/order mismatch") for rel, expected_sha256 in ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256.items(): path = payload_dir / rel try: path_stat = path.lstat() except FileNotFoundError: die(f"Engine provider authority diagnostics target is missing: {rel}") if ( stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode) or sha256_file(path) != expected_sha256 ): die(f"Engine provider authority diagnostics target sha256 mismatch: {rel}") source = ( payload_dir / "nodedc-source/server/nodeIntelligence/upstreamProjection.js" ).read_text(encoding="utf-8") required_markers = ( "isUnknownPrivateNodeIssue", "issue?.nodeName || issue?.node", "privateResults", "NDC_PRIVATE_NODE_VALIDATED_BY_ENGINE_CATALOG", ) if any(marker not in source for marker in required_markers): die("Engine private node validation reconciliation contract mismatch") def validate_engine_depttrans_zone_authority_v1_slice(payload_dir, entries): if tuple(entries) != ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES: die("Engine Depttrans zone authority v1 files.txt exact set/order mismatch") for rel, expected_sha256 in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_TARGET_SHA256.items(): path = payload_dir / rel try: path_stat = path.lstat() except FileNotFoundError: die(f"Engine Depttrans zone authority v1 target is missing: {rel}") if ( stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode) or sha256_file(path) != expected_sha256 ): die(f"Engine Depttrans zone authority v1 target sha256 mismatch: {rel}") catalog = read_strict_json( payload_dir / ENGINE_PROVIDER_SECURITY_CATALOG_REL, "Engine Depttrans zone authority v1 catalog", ) packages = catalog.get("packages") if isinstance(catalog, dict) else None package_ids = [item.get("id") for item in packages if isinstance(item, dict)] \ if isinstance(packages, list) else None provider = next( ( item for item in packages if isinstance(item, dict) and item.get("id") == "moscow-department-of-transport.pmd-slow-zones.v1" ), None, ) if isinstance(packages, list) else None capability = provider.get("capabilities", [None])[0] if isinstance(provider, dict) else None request = capability.get("request") if isinstance(capability, dict) else None marker = read_strict_json( payload_dir / "nodedc-source/server/assets/provider-packages/v1/depttrans-zone-authority-v1.json", "Engine Depttrans zone authority v1 marker", ) if ( catalog.get("schemaVersion") != "nodedc.engine.provider-security-catalog/v1" or package_ids != [ "gelios.provider.v1", "gelios.provider.v4", "gelios.provider.v5", "moscow-department-of-transport.pmd-slow-zones.v1", ] or provider.get("providerId") != "moscow-department-of-transport" or provider.get("version") != "1.0.0" or "providerCredential" in provider or capability.get("id") != "depttrans.pmd-slow-zones.current.read" or capability.get("dataProductIds") != ["map.zones.current.v2"] or request != { "method": "GET", "url": "http://map-gateway:18103/internal/zone-sources/v1/profiles/moscow-pmd-slow-zones/current", "authorityBoundary": "platform-service", "serviceId": "nodedc-map-gateway", "network": "engine", } or marker != { "schemaVersion": "nodedc.engine.platform-service-authority/v1", "id": "moscow-department-of-transport.pmd-slow-zones.v1", "providerId": "moscow-department-of-transport", "authorityBoundary": "platform-service", "serviceId": "nodedc-map-gateway", "network": "engine", "dataProductId": "map.zones.current.v2", } ): die("Engine Depttrans zone authority v1 catalog projection mismatch") required_markers = { "nodedc-source/server/dataProductPublishGrant/providerCatalog.js": ( "function platformServiceRequestIsExact", "authorityBoundary === 'platform-service'", "platformServiceIds", ), "nodedc-source/server/dataProductPublishGrant/service.js": ( "pinned_platform_service_no_graph_credential", "platformServiceIds: descriptor.platformServiceIds", ), "nodedc-source/server/dataProductPublishGrant/store.js": ( "new Set(['provider-credential', 'platform-service'])", "descriptor.authorityBoundary === 'platform-service'", ), } for rel, markers in required_markers.items(): source = (payload_dir / rel).read_text(encoding="utf-8") if any(marker not in source for marker in markers): die(f"Engine Depttrans zone authority v1 source contract mismatch: {rel}") def validate_engine_provider_target_host_policy_slice(payload_dir, entries): if tuple(entries) != ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES: die("Engine provider target host policy files.txt exact set/order mismatch") for rel, expected_sha256 in ENGINE_PROVIDER_TARGET_HOST_POLICY_TARGET_SHA256.items(): path = payload_dir / rel try: path_stat = path.lstat() except FileNotFoundError: die(f"Engine provider target host policy target is missing: {rel}") if ( stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode) or sha256_file(path) != expected_sha256 ): die(f"Engine provider target host policy target sha256 mismatch: {rel}") source = (payload_dir / "nodedc-source/server/routes/n8n.js").read_text(encoding="utf-8") required_markers = ( "const eligibleHosts = explicitHosts.length", "allowedHosts: [targetHost]", "credential_host_not_allowed", ) if any(marker not in source for marker in required_markers): die("Engine provider target host policy contract mismatch") def validate_engine_agent_full_grant_migration_slice(payload_dir, entries): if tuple(entries) != ENGINE_AGENT_FULL_GRANT_MIGRATION_ARTIFACT_ENTRIES: die("Engine agent full grant migration files.txt exact set/order mismatch") store_path = payload_dir / ENGINE_AGENT_FULL_GRANT_MIGRATION_STORE_REL try: store_source = store_path.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): die("Engine agent full grant migration store is unreadable") if sha256_file(store_path) != ENGINE_AGENT_FULL_GRANT_MIGRATION_TARGET_SHA256: die("Engine agent full grant migration target sha256 mismatch") required = ( "const STORE_VERSION = 2", "export const ENGINE_AGENT_FULL_DEVELOPER_PROFILE = 'full-developer'", "export const ENGINE_AGENT_CUSTOM_PROFILE = 'custom'", "const LEGACY_FULL_DEVELOPER_SCOPES = Object.freeze([", "'engine:l2:data-product-publish-grant:plan'", "'engine:l2:data-product-publish-grant:write'", "const migrateLegacyFullDeveloper = sourceVersion === 1", "LEGACY_FULL_DEVELOPER_SCOPES.every((scope) => scopes.includes(scope))", "profile === ENGINE_AGENT_FULL_DEVELOPER_PROFILE ? [...ENGINE_AGENT_SCOPES] : scopes", "throw new Error('engine_agent_store_version_unsupported')", ) if any(value not in store_source for value in required): die("Engine agent full grant migration contract mismatch") if re.search(r"gelios|robot2b", store_source, re.IGNORECASE): die("Engine agent full grant migration provider logic is forbidden") def current_engine_n8n_transition_descriptor(): path = component_root("engine") / ENGINE_N8N_TRANSITION_DESCRIPTOR_REL if not path.exists(): return None if path.is_symlink() or not path.is_file(): die("installed Engine n8n transition descriptor is unsafe") return read_engine_n8n_transition_descriptor(path, "installed Engine n8n transition descriptor") def current_engine_node_intelligence_descriptor(): path = component_root("engine") / ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL if not path.exists(): return None if path.is_symlink() or not path.is_file(): die("installed Engine node-intelligence descriptor is unsafe") return read_engine_node_intelligence_descriptor( path, "installed Engine node-intelligence descriptor", ) def validate_installed_engine_node_intelligence_source(descriptor): if descriptor is None or descriptor.get("action") != "activate": die("active Engine node-intelligence descriptor is required") root = component_root("engine") source_root = root / ENGINE_NODE_INTELLIGENCE_SOURCE_REL expected_source_files = { "catalog.js": descriptor["source"]["catalogSha256"], "upstreamMcpClient.js": descriptor["source"]["upstreamClientSha256"], "upstreamProjection.js": descriptor["source"]["upstreamProjectionSha256"], } try: source_stat = source_root.lstat() except FileNotFoundError: die("installed Engine node-intelligence source is missing") if stat.S_ISLNK(source_stat.st_mode) or not stat.S_ISDIR(source_stat.st_mode): die("installed Engine node-intelligence source is unsafe") actual_files = { child.relative_to(source_root).as_posix() for child in source_root.rglob("*") if child.is_file() } if actual_files != set(expected_source_files): die("installed Engine node-intelligence source file set mismatch") for rel, expected_sha256 in expected_source_files.items(): path = source_root / rel if path.is_symlink() or sha256_file(path) != expected_sha256: die(f"installed Engine node-intelligence source drift detected: {rel}") gateway = root / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL if gateway.is_symlink() or sha256_file(gateway) != descriptor["source"]["gatewaySha256"]: die("installed Engine node-intelligence gateway drift detected") override = root / ENGINE_NODE_INTELLIGENCE_OVERRIDE_REL try: override_stat = override.lstat() override_text = override.read_text(encoding="utf-8") except (FileNotFoundError, OSError, UnicodeDecodeError): die("installed Engine node-intelligence Compose override is missing") if stat.S_ISLNK(override_stat.st_mode) or not stat.S_ISREG(override_stat.st_mode): die("installed Engine node-intelligence Compose override is unsafe") if ( override_text != expected_engine_node_intelligence_compose_override() or sha256_file(override) != descriptor["source"]["composeOverrideSha256"] ): die("installed Engine node-intelligence Compose override drift detected") readme = root / ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL / "README.md" if readme.is_symlink() or sha256_file(readme) != descriptor["source"]["readmeSha256"]: die("installed Engine node-intelligence README drift detected") return descriptor["source"]["gatewaySha256"] def preflight_engine_node_intelligence_predecessor(payload_dir): candidate = read_engine_node_intelligence_descriptor( payload_dir / ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL, "candidate Engine node-intelligence descriptor", ) root = component_root("engine") compose_path = root / "docker-compose.yml" gateway_path = root / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL for path, label in ( (compose_path, "Compose"), (gateway_path, "gateway"), ): try: path_stat = path.lstat() except FileNotFoundError: die(f"Engine node-intelligence predecessor {label} is missing") if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode): die(f"Engine node-intelligence predecessor {label} is unsafe") if sha256_file(compose_path) != candidate["predecessor"]["composeSha256"]: die("Engine node-intelligence predecessor Compose drift detected") current = current_engine_node_intelligence_descriptor() current_state = ( current["releaseId"] if current is not None and current["action"] == "activate" else "inactive" ) if current_state != candidate["expectedCurrent"]: die( "Engine node-intelligence predecessor state mismatch: " f"expected={candidate['expectedCurrent']} actual={current_state}" ) actual_gateway_sha256 = sha256_file(gateway_path) if actual_gateway_sha256 != candidate["predecessor"]["gatewaySha256"]: die( "Engine node-intelligence predecessor gateway drift detected: " f"expected={candidate['predecessor']['gatewaySha256']} " f"actual={actual_gateway_sha256}" ) if candidate["action"] == "activate": if current is not None and current["action"] == "activate": die("Engine node-intelligence activation requires an inactive predecessor") override = root / ENGINE_NODE_INTELLIGENCE_OVERRIDE_REL if override.exists() or override.is_symlink(): die("inactive Engine node-intelligence predecessor still has a Compose override") else: validate_installed_engine_node_intelligence_source(current) backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine node-intelligence requires the active immutable backend") return { "action": candidate["action"], "current_state": current_state, "gateway_sha256": actual_gateway_sha256, "backend_mode": backend["mode"], "descriptor": candidate, } def validate_engine_n8n_staged_release(descriptor): release_rel = f"releases/n8n-nodes-ndc/{descriptor['releaseId']}" release_dir = N8N_PRIVATE_EXTENSION_RELEASES_ROOT / release_rel current = N8N_PRIVATE_EXTENSION_RELEASES_ROOT for part in PurePosixPath(release_rel).parts: current = current / part try: current_stat = current.lstat() except FileNotFoundError: die(f"staged private extension release is missing: {release_rel}") if stat.S_ISLNK(current_stat.st_mode): die(f"staged private extension release symlink rejected: {current}") validate_n8n_private_extension_release(N8N_PRIVATE_EXTENSION_RELEASES_ROOT, [release_rel]) release = read_strict_json(release_dir / "release.json", "staged private extension release manifest") if release.get("releaseId") != descriptor["releaseId"]: die("staged private extension release id mismatch") if release.get("package", {}).get("version") != descriptor["packageVersion"]: die("staged private extension package version mismatch") if release.get("package", {}).get("sha256") != descriptor["packageSha256"]: die("staged private extension package sha256 mismatch") for path in [release_dir, *release_dir.rglob("*")]: path_stat = path.lstat() if path_stat.st_uid != 0 or path_stat.st_gid != 0: die(f"staged private extension release ownership mismatch: {path}") expected_mode = 0o555 if stat.S_ISDIR(path_stat.st_mode) else 0o444 if stat.S_IMODE(path_stat.st_mode) != expected_mode: die(f"staged private extension release mode mismatch: {path}") return release_dir def load_artifact(artifact, work_dir): safe_extract(artifact, work_dir) manifest_path = work_dir / "manifest.env" files_path = work_dir / "files.txt" payload_dir = work_dir / "payload" if not manifest_path.is_file(): die("manifest.env missing after extract") if not files_path.is_file(): die("files.txt missing after extract") if not payload_dir.is_dir(): die("payload directory missing after extract") manifest = parse_manifest(manifest_path) entries = parse_files_list(files_path) for rel in entries: allowed_payload_path(manifest["component"], rel) if not (payload_dir / rel).exists(): die(f"files.txt entry missing in payload: {rel}") validate_payload_tree(manifest["component"], payload_dir, entries) if manifest["component"] == "n8n-private-extension": validate_n8n_private_extension_release(payload_dir, entries) if manifest["component"] == "engine": touches_private_extension = any( rel == "nodedc-source/services/n8n/private-extensions" or rel.startswith("nodedc-source/services/n8n/private-extensions/") for rel in entries ) if touches_private_extension and not is_engine_n8n_transition(manifest["component"], entries): die("Engine private-extension payload requires the canonical transition descriptor") if is_engine_n8n_transition(manifest["component"], entries): validate_engine_n8n_transition(payload_dir, entries) touches_node_intelligence = any( rel == ENGINE_NODE_INTELLIGENCE_SOURCE_REL or rel.startswith(ENGINE_NODE_INTELLIGENCE_SOURCE_REL + "/") or rel == ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL or rel.startswith(ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL + "/") for rel in entries ) if ( touches_node_intelligence and not is_engine_node_intelligence_transition(manifest["component"], entries) and not is_engine_mcp_control_plane_slice(manifest["component"], entries) and not is_engine_mcp_ontology_sdk_slice(manifest["component"], entries) and not is_engine_mcp_autonomy_provider_v5_slice(manifest["component"], entries) and not is_engine_provider_authority_diagnostics_slice( manifest["component"], entries, ) ): die("Engine node-intelligence payload requires the canonical transition") if is_engine_node_intelligence_transition(manifest["component"], entries): validate_engine_node_intelligence_transition(payload_dir, entries) if is_engine_mcp_control_plane_slice(manifest["component"], entries): validate_engine_mcp_control_plane_payload(payload_dir, entries) if is_engine_mcp_ontology_sdk_slice(manifest["component"], entries): validate_engine_mcp_ontology_sdk_payload(payload_dir, entries) if is_engine_mcp_autonomy_provider_v5_slice(manifest["component"], entries): validate_engine_mcp_autonomy_provider_v5_payload(payload_dir, entries) if is_engine_composite_provider_v4_slice(manifest["component"], entries): validate_engine_composite_provider_v4_slice(payload_dir, entries) if is_engine_provider_rotating_slot_slice(manifest["component"], entries): validate_engine_provider_rotating_slot_slice(payload_dir, entries) if is_engine_provider_authority_diagnostics_slice(manifest["component"], entries): validate_engine_provider_authority_diagnostics_slice(payload_dir, entries) if is_engine_depttrans_zone_authority_v1_slice(manifest["component"], entries): validate_engine_depttrans_zone_authority_v1_slice(payload_dir, entries) if is_engine_provider_target_host_policy_slice(manifest["component"], entries): validate_engine_provider_target_host_policy_slice(payload_dir, entries) if is_engine_provider_security_catalog_slice(manifest["component"], entries): validate_engine_provider_security_catalog_payload(payload_dir, entries) if touches_engine_credential_sink(manifest["component"], entries): validate_engine_credential_sink_slice(payload_dir, entries) if ( not is_engine_mcp_control_plane_slice(manifest["component"], entries) and not is_engine_mcp_ontology_sdk_slice(manifest["component"], entries) and not is_engine_mcp_autonomy_provider_v5_slice(manifest["component"], entries) and not is_engine_composite_provider_v4_slice(manifest["component"], entries) and not is_engine_provider_rotating_slot_slice(manifest["component"], entries) and not is_engine_provider_authority_diagnostics_slice(manifest["component"], entries) and not is_engine_depttrans_zone_authority_v1_slice(manifest["component"], entries) and not is_engine_provider_target_host_policy_slice(manifest["component"], entries) and ( touches_engine_data_product_publish_grant(entries) or ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL in entries ) ): validate_engine_data_product_publish_grant_slice(payload_dir, entries) elif ( not is_engine_mcp_control_plane_slice(manifest["component"], entries) and not is_engine_mcp_ontology_sdk_slice(manifest["component"], entries) and not is_engine_mcp_autonomy_provider_v5_slice(manifest["component"], entries) and ENGINE_AGENT_FULL_GRANT_MIGRATION_STORE_REL in entries ): validate_engine_agent_full_grant_migration_slice(payload_dir, entries) return manifest, entries, payload_dir def load_state(path): if not path.exists(): return [] rows = [] with path.open("r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue rows.append(json.loads(line)) return rows def append_jsonl(path, row): path.parent.mkdir(parents=True, exist_ok=True) with path.open("a", encoding="utf-8") as f: f.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n") def state_has_sha(sha): return any(row.get("sha256") == sha for row in load_state(STATE_FILE)) def state_has_patch_id(patch_id): return any(row.get("id") == patch_id for row in load_state(STATE_FILE)) class DeployLock: def __enter__(self): try: LOCK_DIR.mkdir() except FileExistsError: die(f"deploy lock is busy: {LOCK_DIR}") (LOCK_DIR / "pid").write_text(str(os.getpid()), encoding="utf-8") return self def __exit__(self, exc_type, exc, tb): shutil.rmtree(LOCK_DIR, ignore_errors=True) def component_root(component): return COMPONENTS[component]["payload_root"] def component_compose_root(component): return COMPONENTS[component].get("compose_root", component_root(component)) def component_compose_project(component): return COMPONENTS[component].get("compose_project") def component_artifact_only(component): return bool(COMPONENTS[component].get("artifact_only")) def touches_engine_credential_sink(component, entries): if component != "engine" or entries is None: return False return any( rel == "nodedc-source/server/credentialSink" or rel.startswith("nodedc-source/server/credentialSink/") or rel == "nodedc-source/server/routes/engineCredentialSink.js" for rel in entries ) # Frozen compatibility predicate retained because the last successful Engine # credential runtime canon includes it in run_compose(). The corresponding # Platform service is not an allowed artifact path and has no deploy branch. def touches_engine_credential_provisioner(component, entries): return False def touches_external_data_plane_files(entries): runtime_contract_files = { "platform/packages/external-provider-contract/package.json", "platform/packages/external-provider-contract/src/contract-version.mjs", "platform/packages/external-provider-contract/src/data-plane.mjs", "platform/packages/external-provider-contract/src/data-product.mjs", "platform/packages/external-provider-contract/src/intake-batch.mjs", "platform/packages/external-provider-contract/src/index.mjs", "platform/packages/external-provider-contract/src/sensitive-field-policy.mjs", } return any( rel == "platform/services/external-data-plane" or rel.startswith("platform/services/external-data-plane/") or rel in runtime_contract_files or rel == "platform/docker-compose.external-data-plane.yml" for rel in entries ) def touches_engine_data_product_publish_grant(entries): if entries is None: return False return any( rel == "nodedc-source/server/dataProductPublishGrant" or rel.startswith("nodedc-source/server/dataProductPublishGrant/") for rel in entries ) def is_engine_data_product_publish_grant_slice(component, entries): return ( component == "engine" and entries is not None and tuple(entries) == ENGINE_DATA_PRODUCT_PUBLISH_GRANT_ARTIFACT_ENTRIES ) def is_engine_composite_provider_v4_slice(component, entries): return ( component == "engine" and entries is not None and tuple(entries) == ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES ) def is_engine_provider_rotating_slot_slice(component, entries): return ( component == "engine" and entries is not None and tuple(entries) == ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES ) def is_engine_provider_authority_diagnostics_slice(component, entries): return ( component == "engine" and entries is not None and tuple(entries) == ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES ) def is_engine_depttrans_zone_authority_v1_slice(component, entries): return ( component == "engine" and entries is not None and tuple(entries) == ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES ) def is_engine_provider_target_host_policy_slice(component, entries): return ( component == "engine" and entries is not None and tuple(entries) == ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES ) def is_engine_agent_full_grant_migration_slice(component, entries): return ( component == "engine" and entries is not None and tuple(entries) == ENGINE_AGENT_FULL_GRANT_MIGRATION_ARTIFACT_ENTRIES ) def collect_engine_data_product_publish_grant_files(root, entries, label): files = {} for rel in entries: path = root / rel try: path_stat = path.lstat() except FileNotFoundError: die(f"{label} file is missing: {rel}") if stat.S_ISLNK(path_stat.st_mode): die(f"{label} path is unsafe: {rel}") if stat.S_ISREG(path_stat.st_mode): files[rel] = sha256_file(path) continue if not stat.S_ISDIR(path_stat.st_mode): die(f"{label} path is unsafe: {rel}") for child in sorted(path.rglob("*")): child_rel = child.relative_to(root).as_posix() child_stat = child.lstat() if stat.S_ISLNK(child_stat.st_mode): die(f"{label} path is unsafe: {child_rel}") if stat.S_ISDIR(child_stat.st_mode): continue if not stat.S_ISREG(child_stat.st_mode): die(f"{label} path is unsafe: {child_rel}") files[child_rel] = sha256_file(child) return files def validate_installed_engine_data_product_publish_grant_foundation(root): # The original credential-sink baseline remains immutable. The two route # files below became Publish-grant-owned when the initial transition was # applied, so their current contract is validated semantically instead of # comparing them to their pre-transition bytes. publish_owned_predecessors = { "nodedc-source/server/routes/engineAgentGateway.js", "nodedc-source/server/routes/n8n.js", } for rel, expected_sha256 in ENGINE_DATA_PRODUCT_PUBLISH_GRANT_INSTALLED_FOUNDATION_SHA256.items(): if rel in publish_owned_predecessors: continue path = root / rel try: path_stat = path.lstat() except FileNotFoundError: die(f"installed Engine data product publish grant foundation is missing: {rel}") if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode): die(f"installed Engine data product publish grant foundation is unsafe: {rel}") actual_sha256 = sha256_file(path) if actual_sha256 != expected_sha256: die( "installed Engine data product publish grant foundation drift detected: " f"path={rel} expected={expected_sha256} actual={actual_sha256}" ) grant_dir = root / "nodedc-source/server/dataProductPublishGrant" try: grant_stat = grant_dir.lstat() except FileNotFoundError: die("installed Engine data product publish grant source is missing") if stat.S_ISLNK(grant_stat.st_mode) or not stat.S_ISDIR(grant_stat.st_mode): die("installed Engine data product publish grant source is unsafe") installed_names = [] for child in grant_dir.iterdir(): child_stat = child.lstat() if stat.S_ISLNK(child_stat.st_mode) or not stat.S_ISREG(child_stat.st_mode): die(f"installed Engine data product publish grant source is unsafe: {child.name}") installed_names.append(child.name) if tuple(sorted(installed_names)) != ENGINE_DATA_PRODUCT_PUBLISH_GRANT_SOURCE_FILES: die("installed Engine data product publish grant source set mismatch") required_files = ( "nodedc-source/server/assets/provider-packages/v1/catalog.json", "nodedc-source/server/engineAgents/store.js", "nodedc-source/server/routes/engineAgentGateway.js", "nodedc-source/server/routes/n8n.js", ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL, ) for rel in required_files: path = root / rel try: path_stat = path.lstat() except FileNotFoundError: die(f"installed Engine data product publish grant file is missing: {rel}") if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode): die(f"installed Engine data product publish grant file is unsafe: {rel}") try: override_text = (root / ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL).read_text( encoding="utf-8" ) gateway = (root / "nodedc-source/server/routes/engineAgentGateway.js").read_text( encoding="utf-8" ) n8n_route = (root / "nodedc-source/server/routes/n8n.js").read_text( encoding="utf-8" ) except (OSError, UnicodeDecodeError): die("installed Engine data product publish grant source is unreadable") if override_text != expected_engine_data_product_publish_grant_override(): die("installed Engine data product publish grant override drift detected") if "from '../credentialSink/" in n8n_route: die("installed Engine data product publish grant depends on the legacy sink") for adapter in ( "engineCredentialSinkN8nAdapter", "engineDataProductPublishGrantN8nAdapter", ): if adapter not in n8n_route: die(f"installed Engine data product publish grant adapter is missing: {adapter}") for tool in ( "engine_plan_data_product_publish_grant", "engine_apply_data_product_publish_grant", "engine_accept_data_product_publish_grant", "engine_rollback_data_product_publish_grant", ): if tool not in gateway: die(f"installed Engine data product publish grant tool is missing: {tool}") return sha256_file(root / "docker-compose.yml") def preflight_engine_data_product_publish_grant_predecessor(payload_dir=None): root = component_root("engine") installed_override = root / ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL if installed_override.exists() or installed_override.is_symlink(): compose_sha256 = validate_installed_engine_data_product_publish_grant_foundation(root) if payload_dir is None: die("Engine data product publish grant update candidate is required") installed_files = collect_engine_data_product_publish_grant_files( root, ENGINE_DATA_PRODUCT_PUBLISH_GRANT_ARTIFACT_ENTRIES, "installed Engine data product publish grant", ) candidate_files = collect_engine_data_product_publish_grant_files( payload_dir, ENGINE_DATA_PRODUCT_PUBLISH_GRANT_ARTIFACT_ENTRIES, "candidate Engine data product publish grant", ) if set(installed_files) != set(candidate_files): die("Engine data product publish grant update file set mismatch") changed_paths = tuple(sorted( rel for rel, candidate_sha256 in candidate_files.items() if installed_files[rel] != candidate_sha256 )) if not changed_paths: die("Engine data product publish grant update contains no source change") forbidden_changes = tuple( rel for rel in changed_paths if ( not rel.startswith(ENGINE_DATA_PRODUCT_PUBLISH_GRANT_SOURCE_PREFIX) and rel != ENGINE_PROVIDER_SECURITY_CATALOG_REL ) ) if forbidden_changes: die( "Engine data product publish grant update crosses its source boundary: " f"path={forbidden_changes[0]}" ) if ENGINE_PROVIDER_SECURITY_CATALOG_REL in changed_paths: installed_catalog_sha256 = installed_files[ENGINE_PROVIDER_SECURITY_CATALOG_REL] candidate_catalog_sha256 = candidate_files[ENGINE_PROVIDER_SECURITY_CATALOG_REL] if ( installed_catalog_sha256 != ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_PREDECESSOR_SHA256 or candidate_catalog_sha256 != ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_TARGET_SHA256 ): die("Engine composite provider catalog transition mismatch") if changed_paths != ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CHANGED_PATHS: die("Engine composite provider update exact changed path set mismatch") mode = "installed-composite-provider-update" else: mode = "installed-source-update" return { "mode": mode, "compose_sha256": compose_sha256, "changed_paths": changed_paths, } actual = {} for rel, expected_sha256 in ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_SHA256.items(): path = root / rel try: path_stat = path.lstat() except FileNotFoundError: die(f"Engine data product publish grant predecessor file is missing: {rel}") if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode): die(f"Engine data product publish grant predecessor file is unsafe: {rel}") actual_sha256 = sha256_file(path) if actual_sha256 != expected_sha256: die( "Engine data product publish grant predecessor drift detected: " f"path={rel} expected={expected_sha256} actual={actual_sha256}" ) actual[rel] = actual_sha256 return { "mode": "credential-sink-initial-transition", "compose_sha256": actual["docker-compose.yml"], "changed_paths": (), } def preflight_engine_composite_provider_v4_predecessor(): root = component_root("engine") actual = {} for rel, expected_sha256 in ENGINE_COMPOSITE_PROVIDER_V4_PREDECESSOR_SHA256.items(): path = root / rel try: path_stat = path.lstat() except FileNotFoundError: die(f"Engine composite provider v4 predecessor is missing: {rel}") if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode): die(f"Engine composite provider v4 predecessor is unsafe: {rel}") actual_sha256 = sha256_file(path) if actual_sha256 != expected_sha256: die( "Engine composite provider v4 predecessor drift detected: " f"path={rel} expected={expected_sha256} actual={actual_sha256}" ) actual[rel] = actual_sha256 backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine composite provider v4 requires the active immutable backend") return { "mode": "exact-composite-provider-v3-to-v4", "predecessor_sha256": actual, "target_sha256": dict(ENGINE_COMPOSITE_PROVIDER_V4_TARGET_SHA256), "backend_mode": backend["mode"], } def preflight_engine_provider_rotating_slot_predecessor(): root = component_root("engine") actual = {} for rel, expected_sha256 in ENGINE_PROVIDER_ROTATING_SLOT_PREDECESSOR_SHA256.items(): path = root / rel try: path_stat = path.lstat() except FileNotFoundError: die(f"Engine provider rotating slot predecessor is missing: {rel}") if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode): die(f"Engine provider rotating slot predecessor is unsafe: {rel}") actual_sha256 = sha256_file(path) if actual_sha256 != expected_sha256: die( "Engine provider rotating slot predecessor drift detected: " f"path={rel} expected={expected_sha256} actual={actual_sha256}" ) actual[rel] = actual_sha256 backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine provider rotating slot requires the active immutable backend") return { "mode": "exact-gelios-v4-credential-slot-alignment", "predecessor_sha256": actual, "target_sha256": dict(ENGINE_PROVIDER_ROTATING_SLOT_TARGET_SHA256), "backend_mode": backend["mode"], } def preflight_engine_provider_authority_diagnostics_predecessor(): root = component_root("engine") actual = {} for rel, expected_sha256 in ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_PREDECESSOR_SHA256.items(): path = root / rel try: path_stat = path.lstat() except FileNotFoundError: die(f"Engine provider authority diagnostics predecessor is missing: {rel}") if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode): die(f"Engine provider authority diagnostics predecessor is unsafe: {rel}") actual_sha256 = sha256_file(path) if actual_sha256 != expected_sha256: die( "Engine provider authority diagnostics predecessor drift detected: " f"path={rel} expected={expected_sha256} actual={actual_sha256}" ) actual[rel] = actual_sha256 backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine provider authority diagnostics requires the active immutable backend") return { "mode": "private-node-name-reconciliation", "predecessor_sha256": actual, "target_sha256": dict(ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256), "backend_mode": backend["mode"], } def preflight_engine_depttrans_zone_authority_v1_predecessor(): root = component_root("engine") actual = {} for rel, expected_sha256 in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_PREDECESSOR_SHA256.items(): path = root / rel try: path_stat = path.lstat() except FileNotFoundError: die(f"Engine Depttrans zone authority v1 predecessor is missing: {rel}") if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode): die(f"Engine Depttrans zone authority v1 predecessor is unsafe: {rel}") actual_sha256 = sha256_file(path) if actual_sha256 != expected_sha256: die( "Engine Depttrans zone authority v1 predecessor drift detected: " f"path={rel} expected={expected_sha256} actual={actual_sha256}" ) actual[rel] = actual_sha256 for rel in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_NEW_PATHS: path = root / rel if path.exists() or path.is_symlink(): die(f"Engine Depttrans zone authority v1 new path already exists: {rel}") backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine Depttrans zone authority v1 requires the active immutable backend") return { "mode": "exact-platform-service-authority-v1", "predecessor_sha256": actual, "target_sha256": dict(ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_TARGET_SHA256), "backend_mode": backend["mode"], } def print_engine_depttrans_zone_authority_v1_plan(preflight): print("engine_provider_authority_transition=platform-service-v1") print("engine_provider_package=moscow-department-of-transport.pmd-slow-zones.v1") print("engine_provider_id=moscow-department-of-transport") print("engine_provider_authority_boundary=platform-service") print("engine_provider_platform_service=nodedc-map-gateway") print("engine_provider_platform_network=engine") print("engine_data_product=map.zones.current.v2") print("provider_credential_values=preserved") predecessor_sha256 = preflight["predecessor_sha256"] target_sha256 = preflight["target_sha256"] for changed_path in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES: print(f"engine_depttrans_zone_authority_changed_path={changed_path}") if changed_path in predecessor_sha256: print( f"engine_depttrans_zone_authority_predecessor_sha256[{changed_path}]=" f"{predecessor_sha256[changed_path]}" ) elif changed_path in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_NEW_PATHS: print( f"engine_depttrans_zone_authority_predecessor_state[{changed_path}]=" "absent" ) else: die( "Engine Depttrans zone authority v1 plan has no predecessor state: " f"{changed_path}" ) if changed_path not in target_sha256: die( "Engine Depttrans zone authority v1 plan has no target sha256: " f"{changed_path}" ) print( f"engine_depttrans_zone_authority_target_sha256[{changed_path}]=" f"{target_sha256[changed_path]}" ) print(f"backend_current_barrier={preflight['backend_mode']}") print("backend_force_recreate=yes") print("backend_pull=never") print("l2_graph=untouched") print("n8n_l1=untouched") print("engine_ui=untouched") print("engine_databases=untouched") print("mcp_nginx=untouched") def preflight_engine_provider_target_host_policy_predecessor(): root = component_root("engine") actual = {} for rel, expected_sha256 in ENGINE_PROVIDER_TARGET_HOST_POLICY_PREDECESSOR_SHA256.items(): path = root / rel try: path_stat = path.lstat() except FileNotFoundError: die(f"Engine provider target host policy predecessor is missing: {rel}") if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode): die(f"Engine provider target host policy predecessor is unsafe: {rel}") actual_sha256 = sha256_file(path) if actual_sha256 != expected_sha256: die( "Engine provider target host policy predecessor drift detected: " f"path={rel} expected={expected_sha256} actual={actual_sha256}" ) actual[rel] = actual_sha256 backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine provider target host policy requires the active immutable backend") return { "mode": "exact-provider-literal-target-host", "predecessor_sha256": actual, "target_sha256": dict(ENGINE_PROVIDER_TARGET_HOST_POLICY_TARGET_SHA256), "backend_mode": backend["mode"], } def preflight_engine_agent_full_grant_migration_predecessor(): root = component_root("engine") store_path = root / ENGINE_AGENT_FULL_GRANT_MIGRATION_STORE_REL try: store_stat = store_path.lstat() except FileNotFoundError: die("Engine agent full grant migration predecessor is missing") if stat.S_ISLNK(store_stat.st_mode) or not stat.S_ISREG(store_stat.st_mode): die("Engine agent full grant migration predecessor is unsafe") actual_sha256 = sha256_file(store_path) if actual_sha256 != ENGINE_AGENT_FULL_GRANT_MIGRATION_PREDECESSOR_SHA256: die( "Engine agent full grant migration predecessor drift detected: " f"expected={ENGINE_AGENT_FULL_GRANT_MIGRATION_PREDECESSOR_SHA256} " f"actual={actual_sha256}" ) publish_override = root / ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL try: publish_stat = publish_override.lstat() publish_text = publish_override.read_text(encoding="utf-8") except (FileNotFoundError, OSError, UnicodeDecodeError): die("Engine agent full grant migration requires the installed Publish overlay") if ( stat.S_ISLNK(publish_stat.st_mode) or not stat.S_ISREG(publish_stat.st_mode) or publish_text != expected_engine_data_product_publish_grant_override() ): die("Engine agent full grant migration Publish overlay mismatch") return actual_sha256 def is_platform_provider_catalog_only(entries): prefix = "platform/packages/external-provider-contract/providers/" return bool(entries) and all(rel.startswith(prefix) for rel in entries) def component_services(component, entries=None): if ( is_engine_mcp_control_plane_slice(component, entries) or is_engine_mcp_ontology_sdk_slice(component, entries) or is_engine_mcp_autonomy_provider_v5_slice(component, entries) or is_engine_composite_provider_v4_slice(component, entries) or is_engine_provider_rotating_slot_slice(component, entries) or is_engine_provider_authority_diagnostics_slice(component, entries) or is_engine_depttrans_zone_authority_v1_slice(component, entries) or is_engine_provider_target_host_policy_slice(component, entries) or is_engine_provider_security_catalog_slice(component, entries) ): # This slice updates only the existing Engine backend control plane. # Node intelligence keeps the same immutable sidecar image and n8n/L1 # retain their current generations. return ("nodedc-backend",) if is_engine_node_intelligence_transition(component, entries): if tuple(entries) == ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES: return (ENGINE_NODE_INTELLIGENCE_SERVICE, "nodedc-backend") return ("nodedc-backend",) if is_engine_n8n_transition(component, entries): # The actual Engine topology has one n8n process and no separately # deployed worker/webhook services. The release barrier therefore # force-recreates exactly this service and never touches its database. return ("n8n",) if touches_engine_credential_sink(component, entries): # The sink is served by the existing Engine backend. It has no frontend # or n8n process code, so preserve those generations during this slice. return ("nodedc-backend",) if is_engine_data_product_publish_grant_slice(component, entries): # Publish-grant source and its fixed runtime overlay are backend-owned. # n8n, the UI and every database retain their existing generations. return ("nodedc-backend",) if is_engine_agent_full_grant_migration_slice(component, entries): # This one-time compatibility migration changes only Engine Agent # authorization normalization inside the already-active backend. return ("nodedc-backend",) if component == "dc-cms" and entries is not None: selected = [] def add(*services): for service in services: if service not in selected: selected.append(service) touches_compose = any(rel == "infra/docker-compose.yml" for rel in entries) touches_proxy = any(rel == "infra/reverse-proxy" or rel.startswith("infra/reverse-proxy/") for rel in entries) touches_authentik = any(rel == "infra/authentik" or rel.startswith("infra/authentik/") for rel in entries) touches_app = any( rel in ("Dockerfile", "package.json", "package-lock.json", "README.md") or rel.startswith(("admin/", "projects/", "server/")) for rel in entries ) if touches_compose: return COMPONENTS[component]["services"] if touches_authentik: add("postgresql-authentik", "authentik-server", "authentik-worker", "authentik-bootstrap", "cms-app", "reverse-proxy") if touches_app: add("cms-app") if touches_proxy: add("reverse-proxy") if selected: return tuple(selected) if component == "tasker" and entries is not None: selected = ["api", "worker", "beat-worker", "web"] touches_compose = any( rel in ("plane-app/docker-compose.yaml", "plane-app/docker-compose.synology.override.yml") for rel in entries ) touches_proxy = any(rel == "plane-src/apps/proxy/Caddyfile.ce" for rel in entries) if touches_compose or touches_proxy: selected.append("proxy") return tuple(selected) if component == "platform" and entries is not None: if is_platform_provider_catalog_only(entries): return () selected = [] def add(*services): for service in services: if service not in selected: selected.append(service) touches_compose = any(rel == "platform/docker-compose.platform-http.yml" for rel in entries) touches_caddy = any(rel == "platform/Caddyfile.http" for rel in entries) touches_notification = any(rel == "platform/notification-core" or rel.startswith("platform/notification-core/") for rel in entries) touches_ai_workspace = any(rel == "platform/ai-workspace-hub" or rel.startswith("platform/ai-workspace-hub/") for rel in entries) touches_ai_workspace_assistant = any(rel == "platform/ai-workspace-assistant" or rel.startswith("platform/ai-workspace-assistant/") for rel in entries) touches_ontology = any(rel == "platform/ontology-core" or rel.startswith("platform/ontology-core/") for rel in entries) touches_gelios = any(rel == "platform/gelios-gateway" or rel.startswith("platform/gelios-gateway/") for rel in entries) touches_map_gateway = any(rel == "platform/services/map-gateway" or rel.startswith("platform/services/map-gateway/") for rel in entries) touches_external_data_plane = touches_external_data_plane_files(entries) touches_authentik = any(rel == "authentik/custom-templates" or rel.startswith("authentik/custom-templates/") for rel in entries) map_gateway_only_compose = touches_compose and touches_map_gateway and not any((touches_notification, touches_ai_workspace, touches_ai_workspace_assistant, touches_ontology, touches_gelios, touches_external_data_plane, touches_authentik)) restart_all_for_compose = touches_compose and not map_gateway_only_compose if touches_notification or restart_all_for_compose: add("notification-postgres", "notification-core", "launcher", "reverse-proxy") if touches_ai_workspace or restart_all_for_compose: add("ai-workspace-hub", "reverse-proxy") if touches_ai_workspace_assistant or restart_all_for_compose: add("ai-workspace-postgres", "ai-workspace-assistant") if touches_ontology or restart_all_for_compose: add("ontology-core", "ai-workspace-hub") if touches_gelios or restart_all_for_compose: add("gelios-postgres", "gelios-gateway") if touches_map_gateway: add("map-gateway") if touches_external_data_plane: # The Timescale/PostgreSQL service is durable infrastructure. Both # contract and managed EDP changes recreate only the EDP # application process. add("external-data-plane") if touches_authentik: add("authentik-server", "authentik-worker", "reverse-proxy") if touches_caddy: add("reverse-proxy") if selected: return tuple(selected) return COMPONENTS[component]["services"] def component_compose_no_deps(component, entries=None): if component == "dc-cms" and entries is not None: touches_compose = any(rel == "infra/docker-compose.yml" for rel in entries) touches_proxy = any(rel == "infra/reverse-proxy" or rel.startswith("infra/reverse-proxy/") for rel in entries) touches_authentik = any(rel == "infra/authentik" or rel.startswith("infra/authentik/") for rel in entries) return not (touches_compose or touches_proxy or touches_authentik) return bool(COMPONENTS[component].get("compose_no_deps")) def engine_backend_immutable_runtime_is_current(): metadata = validate_engine_backend_activation_marker() compose_project = component_compose_root("engine").name result = subprocess.run( [ str(DOCKER), "container", "ls", "-a", "--filter", f"label=com.docker.compose.project={compose_project}", "--filter", "label=com.docker.compose.service=nodedc-backend", "--format", "{{.ID}}", ], check=False, capture_output=True, text=True, ) container_ids = [line.strip() for line in result.stdout.splitlines() if line.strip()] if (result.returncode != 0 or len(container_ids) != 1 or not re.fullmatch(r"[a-f0-9]{12,64}", container_ids[0])): die("Engine backend immutable runtime topology is unproven") containers = docker_json( ["container", "inspect", container_ids[0]], "Engine backend immutable runtime container inspect", ) container = containers[0] if isinstance(containers, list) and len(containers) == 1 else None image = inspect_engine_backend_derived_image(metadata) return ( isinstance(container, dict) and container.get("Image") == image.get("Id") and (container.get("Config") or {}).get("Image") == ENGINE_CREDENTIAL_BACKEND_IMAGE ) def component_compose_files(component, allow_prepared_engine_backend=False): if component == "engine": root = component_root(component) files = [root / "docker-compose.yml"] descriptor = current_engine_n8n_transition_descriptor() if descriptor and descriptor["action"] == "activate": override = root / ENGINE_N8N_COMPOSE_OVERRIDE_REL if override.is_symlink() or not override.is_file(): die("active Engine n8n Compose override is missing or unsafe") try: override_text = override.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): die("active Engine n8n Compose override cannot be read") if override_text != expected_engine_n8n_compose_override(descriptor): die("installed Engine n8n Compose override drift detected") files.append(override) if (ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE.exists() or ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE.is_symlink()): validate_engine_backend_activation_marker() if (allow_prepared_engine_backend or engine_backend_immutable_runtime_is_current()): files.append(ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE) publish_grant_override = root / ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL if publish_grant_override.exists() or publish_grant_override.is_symlink(): if publish_grant_override.is_symlink() or not publish_grant_override.is_file(): die("installed Engine data product publish grant override is unsafe") try: publish_grant_override_text = publish_grant_override.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): die("installed Engine data product publish grant override cannot be read") if publish_grant_override_text != expected_engine_data_product_publish_grant_override(): die("installed Engine data product publish grant override drift detected") # Canonical order is immutable: base, active L2 extension, credential # sink, then the additive Publish grant overlay. files.append(publish_grant_override) read_grant_override = root / ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL if read_grant_override.exists() or read_grant_override.is_symlink(): if read_grant_override.is_symlink() or not read_grant_override.is_file(): die("installed Engine data product read grant override is unsafe") try: read_grant_override_text = read_grant_override.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): die("installed Engine data product read grant override cannot be read") if read_grant_override_text != expected_engine_data_product_read_grant_override(): die("installed Engine data product read grant override drift detected") # Read authority is additive and ordered after the writer/private-key overlay. files.append(read_grant_override) node_intelligence_descriptor = current_engine_node_intelligence_descriptor() if ( node_intelligence_descriptor is not None and node_intelligence_descriptor["action"] == "activate" ): validate_installed_engine_node_intelligence_source( node_intelligence_descriptor ) files.append(root / ENGINE_NODE_INTELLIGENCE_OVERRIDE_REL) return tuple(files) files = COMPONENTS[component].get("compose_files", ()) if component == "platform": # The EDP overlay is optional until its first dedicated artifact lands. # Once installed (or after a candidate has been copied), it participates # in every authoritative Compose operation. files = tuple( path for path in files if path.name != Path(PLATFORM_EXTERNAL_DATA_PLANE_COMPOSE_REL).name or path.is_file() ) return files def component_compose_env_file(component): return COMPONENTS[component].get("compose_env_file") def component_build_root(component): return COMPONENTS[component].get("build_root") def component_build_args(component, entries=None): if component == "platform" and entries is not None: touches_notification = any(rel == "platform/notification-core" or rel.startswith("platform/notification-core/") for rel in entries) return COMPONENTS[component].get("build") if touches_notification else None if component == "bim-viewer" and entries is not None: touches_converter = any(rel == "converter" or rel.startswith("converter/") for rel in entries) return COMPONENTS[component].get("build") if touches_converter else None return COMPONENTS[component].get("build") def component_builds(component, entries=None): if component == "platform" and entries is not None: touches_compose = any(rel == "platform/docker-compose.platform-http.yml" for rel in entries) touches_notification = any(rel == "platform/notification-core" or rel.startswith("platform/notification-core/") for rel in entries) touches_ai_workspace = any(rel == "platform/ai-workspace-hub" or rel.startswith("platform/ai-workspace-hub/") for rel in entries) touches_ai_workspace_assistant = any(rel == "platform/ai-workspace-assistant" or rel.startswith("platform/ai-workspace-assistant/") for rel in entries) touches_ontology = any(rel == "platform/ontology-core" or rel.startswith("platform/ontology-core/") for rel in entries) touches_gelios = any(rel == "platform/gelios-gateway" or rel.startswith("platform/gelios-gateway/") for rel in entries) touches_map_gateway = any(rel == "platform/services/map-gateway" or rel.startswith("platform/services/map-gateway/") for rel in entries) touches_external_data_plane = touches_external_data_plane_files(entries) map_gateway_only_compose = touches_compose and touches_map_gateway and not any((touches_notification, touches_ai_workspace, touches_ai_workspace_assistant, touches_ontology, touches_gelios, touches_external_data_plane)) rebuild_all_for_compose = touches_compose and not map_gateway_only_compose builds = [] if touches_notification or rebuild_all_for_compose: builds.append(( Path("/volume1/docker/nodedc-platform/platform/notification-core"), ("build", "--no-cache", "-t", "nodedc/notification-core:local", "."), )) if touches_ai_workspace or rebuild_all_for_compose: builds.append(( Path("/volume1/docker/nodedc-platform/platform/ai-workspace-hub"), ("build", "--no-cache", "-t", "nodedc/ai-workspace-hub:local", "."), )) if touches_ai_workspace_assistant or rebuild_all_for_compose: builds.append(( Path("/volume1/docker/nodedc-platform/platform/ai-workspace-assistant"), ("build", "--no-cache", "-t", "nodedc/ai-workspace-assistant:local", "."), )) if touches_ontology or rebuild_all_for_compose: builds.append(( Path("/volume1/docker/nodedc-platform/platform/ontology-core"), ("build", "--no-cache", "-t", "nodedc/ontology-core:local", "."), )) if touches_gelios or rebuild_all_for_compose: builds.append(( Path("/volume1/docker/nodedc-platform/platform/gelios-gateway"), ("build", "--no-cache", "-t", "nodedc/gelios-gateway:local", "."), )) if touches_map_gateway: builds.append(( Path("/volume1/docker/nodedc-platform/platform/services/map-gateway"), ("build", "--no-cache", "-t", "nodedc/map-gateway:local", "."), )) if touches_external_data_plane: builds.append(( Path("/volume1/docker/nodedc-platform/platform"), ("build", "--no-cache", "-f", "services/external-data-plane/Dockerfile", "-t", "nodedc/external-data-plane:local", "."), )) return tuple(builds) if component == "tasker": plane_src = Path("/volume1/docker/nodedc-platform/tasker/plane-src") return ( ( plane_src / "apps/api", ( "build", "--network=host", "-t", "nodedc/plane-backend:local", "-f", "Dockerfile.api", ".", ), ), ( plane_src, ( "build", "--network=host", "--build-arg", "VITE_NODEDC_LAUNCHER_URL=https://hub.nodedc.ru", "--build-arg", "VITE_NODEDC_OIDC_LOGIN_ENABLED=1", "-t", "nodedc/plane-frontend:ru", "-f", "apps/web/Dockerfile.web.nas-legacy", ".", ), ), ) build_root = component_build_root(component) build_args = component_build_args(component, entries) if build_root and build_args: return ((build_root, build_args),) return () def docker_json(args, label): result = subprocess.run( [str(DOCKER), *args], check=False, capture_output=True, text=True, ) if result.returncode != 0: die(f"{label} failed: exit={result.returncode}") try: return json.loads(result.stdout) except json.JSONDecodeError: die(f"{label} returned invalid JSON") def inspect_local_image(image_ref, label): images = docker_json(["image", "inspect", image_ref], f"{label} inspect") if not isinstance(images, list) or len(images) != 1 or not isinstance(images[0], dict): die(f"{label} inspect shape mismatch") image = images[0] image_id = image.get("Id") if (not isinstance(image_id, str) or not re.fullmatch(r"sha256:[a-f0-9]{64}", image_id) or image.get("Architecture") != "amd64" or image.get("Os") != "linux"): die(f"{label} identity/platform mismatch") return image_id def inspect_optional_local_image(image_ref, label): result = subprocess.run( [str(DOCKER), "image", "inspect", image_ref], check=False, capture_output=True, text=True, ) if result.returncode != 0: return None try: images = json.loads(result.stdout) except json.JSONDecodeError: die(f"{label} returned invalid JSON") if not isinstance(images, list) or len(images) != 1 or not isinstance(images[0], dict): die(f"{label} inspect shape mismatch") image = images[0] image_id = image.get("Id") if (not isinstance(image_id, str) or not re.fullmatch(r"sha256:[a-f0-9]{64}", image_id) or image.get("Architecture") != "amd64" or image.get("Os") != "linux"): die(f"{label} identity/platform mismatch") return image_id def ensure_engine_node_intelligence_secret(): try: runtime_stat = ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR.lstat() except FileNotFoundError: ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR.mkdir(parents=False, exist_ok=False) runtime_stat = ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR.lstat() if stat.S_ISLNK(runtime_stat.st_mode) or not stat.S_ISDIR(runtime_stat.st_mode): die("Engine node-intelligence runtime directory is unsafe") os.chown( ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR, 0, ENGINE_NODE_INTELLIGENCE_RUNTIME_GID, ) ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR.chmod(0o710) try: secret_stat = ENGINE_NODE_INTELLIGENCE_SECRET_FILE.lstat() except FileNotFoundError: value = secrets.token_urlsafe(48) temporary = ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR / ( f".{ENGINE_NODE_INTELLIGENCE_SECRET_FILE.name}." f"{os.getpid()}.{time.time_ns()}.tmp" ) descriptor = None try: descriptor = os.open( str(temporary), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o440, ) os.write(descriptor, f"{value}\n".encode("ascii")) os.fsync(descriptor) os.fchown(descriptor, 0, ENGINE_NODE_INTELLIGENCE_RUNTIME_GID) os.fchmod(descriptor, 0o440) os.close(descriptor) descriptor = None os.replace(temporary, ENGINE_NODE_INTELLIGENCE_SECRET_FILE) fsync_directory(ENGINE_NODE_INTELLIGENCE_RUNTIME_DIR) finally: if descriptor is not None: os.close(descriptor) if temporary.exists(): temporary.unlink() return "created" if ( stat.S_ISLNK(secret_stat.st_mode) or not stat.S_ISREG(secret_stat.st_mode) or secret_stat.st_uid != 0 or secret_stat.st_gid != ENGINE_NODE_INTELLIGENCE_RUNTIME_GID or stat.S_IMODE(secret_stat.st_mode) != 0o440 or secret_stat.st_size < 49 or secret_stat.st_size > 512 ): die("Engine node-intelligence secret file is unsafe") try: value = ENGINE_NODE_INTELLIGENCE_SECRET_FILE.read_text( encoding="ascii" ).strip() except (OSError, UnicodeDecodeError): die("Engine node-intelligence secret file is unreadable") if not ENGINE_NODE_INTELLIGENCE_SECRET_RE.fullmatch(value): die("Engine node-intelligence secret file has invalid format") return "reused" def inspect_engine_node_intelligence_image(descriptor, required=True): result = subprocess.run( [str(DOCKER), "image", "inspect", ENGINE_NODE_INTELLIGENCE_IMAGE], check=False, capture_output=True, text=True, ) if result.returncode != 0: if not required: return None die("Engine node-intelligence image is missing") try: images = json.loads(result.stdout) except json.JSONDecodeError: die("Engine node-intelligence image inspect returned invalid JSON") if not isinstance(images, list) or len(images) != 1 or not isinstance(images[0], dict): die("Engine node-intelligence image inspect shape mismatch") image = images[0] image_id = image.get("Id") config = image.get("Config") or {} labels = config.get("Labels") or {} repo_tags = image.get("RepoTags") or [] if ( not isinstance(image_id, str) or not re.fullmatch(r"sha256:[a-f0-9]{64}", image_id) or image.get("Architecture") != descriptor["image"]["architecture"] or image.get("Os") != descriptor["image"]["os"] or ENGINE_NODE_INTELLIGENCE_IMAGE not in repo_tags or not isinstance(labels, dict) or labels.get("org.opencontainers.image.revision") != ENGINE_NODE_INTELLIGENCE_UPSTREAM_COMMIT or config.get("Entrypoint") != ["/usr/local/bin/docker-entrypoint.sh"] or config.get("Cmd") != ["node", "dist/mcp/index.js"] ): die("Engine node-intelligence image identity mismatch") env = config.get("Env") or [] if any( str(value).startswith(("AUTH_TOKEN=", "N8N_API_URL=", "N8N_API_KEY=")) for value in env ): die("Engine node-intelligence image contains runtime authority") return image def install_engine_node_intelligence_image(descriptor): if descriptor.get("action") != "activate": die("Engine node-intelligence image install requires activation") source_archive = ( component_root("engine") / ENGINE_NODE_INTELLIGENCE_IMAGE_ARCHIVE_REL ) validate_engine_node_intelligence_image_archive(source_archive, descriptor) try: releases_stat = ENGINE_NODE_INTELLIGENCE_RELEASES_DIR.lstat() except FileNotFoundError: ENGINE_NODE_INTELLIGENCE_RELEASES_DIR.mkdir(parents=False, exist_ok=False) releases_stat = ENGINE_NODE_INTELLIGENCE_RELEASES_DIR.lstat() if stat.S_ISLNK(releases_stat.st_mode) or not stat.S_ISDIR(releases_stat.st_mode): die("Engine node-intelligence releases directory is unsafe") os.chown(ENGINE_NODE_INTELLIGENCE_RELEASES_DIR, 0, 0) ENGINE_NODE_INTELLIGENCE_RELEASES_DIR.chmod(0o500) release_dir = ( ENGINE_NODE_INTELLIGENCE_RELEASES_DIR / descriptor["releaseId"] ) try: release_stat = release_dir.lstat() except FileNotFoundError: ENGINE_NODE_INTELLIGENCE_RELEASES_DIR.chmod(0o700) try: release_dir.mkdir(mode=0o500, exist_ok=False) finally: ENGINE_NODE_INTELLIGENCE_RELEASES_DIR.chmod(0o500) release_stat = release_dir.lstat() if ( stat.S_ISLNK(release_stat.st_mode) or not stat.S_ISDIR(release_stat.st_mode) or release_stat.st_uid != 0 ): die("Engine node-intelligence release directory is unsafe") os.chown(release_dir, 0, 0) release_dir.chmod(0o500) release_archive = release_dir / "engine-node-intelligence.tar" if release_archive.exists() or release_archive.is_symlink(): release_archive_stat = release_archive.lstat() if ( stat.S_ISLNK(release_archive_stat.st_mode) or not stat.S_ISREG(release_archive_stat.st_mode) or release_archive_stat.st_uid != 0 or release_archive_stat.st_gid != 0 or stat.S_IMODE(release_archive_stat.st_mode) != 0o400 or sha256_file(release_archive) != descriptor["image"]["archiveSha256"] ): die("Engine node-intelligence sealed release collision") else: release_dir.chmod(0o700) temporary = release_dir / ( f".engine-node-intelligence.tar.{os.getpid()}.{time.time_ns()}.tmp" ) try: with source_archive.open("rb") as source, temporary.open("xb") as output: shutil.copyfileobj(source, output, 1024 * 1024) output.flush() os.fsync(output.fileno()) os.chown(temporary, 0, 0) temporary.chmod(0o400) if sha256_file(temporary) != descriptor["image"]["archiveSha256"]: die("Engine node-intelligence sealed release copy mismatch") os.replace(temporary, release_archive) fsync_directory(release_dir) finally: if temporary.exists(): temporary.unlink() release_dir.chmod(0o500) load = subprocess.run( [str(DOCKER), "image", "load", "--input", str(release_archive)], check=False, capture_output=True, text=True, timeout=600, ) if load.returncode != 0: die("Engine node-intelligence offline image load failed") image = inspect_engine_node_intelligence_image(descriptor) return image["Id"] def engine_backend_container_id(): result = subprocess.run( [*compose_base_cmd("engine"), "ps", "-q", "nodedc-backend"], cwd=str(component_compose_root("engine")), check=False, capture_output=True, text=True, ) container_ids = [line.strip() for line in result.stdout.splitlines() if line.strip()] if (result.returncode != 0 or len(container_ids) != 1 or not re.fullmatch(r"[a-f0-9]{12,64}", container_ids[0])): die("Engine backend topology mismatch") return container_ids[0] def run_engine_backend_probe(arguments, label, container_id=None, image_ref=None): if (container_id is None) == (image_ref is None): die(f"{label} probe target mismatch") if container_id is not None: cmd = [str(DOCKER), "exec", container_id, *arguments] else: cmd = [ str(DOCKER), "run", "--rm", "--network", "none", "--tmpfs", "/tmp:mode=1777", "--env", "HOME=/tmp", image_ref, *arguments, ] result = subprocess.run( cmd, check=False, capture_output=True, text=True, timeout=90, ) value = result.stdout.strip() if (result.returncode != 0 or result.stderr.strip() or not value or len(value) > 512 or any(ord(character) < 0x20 and character not in "\t" for character in value)): die(f"Engine backend {label} probe failed") return value def engine_backend_tool_versions(container_id=None, image_ref=None): versions = { "node": run_engine_backend_probe(("node", "--version"), "node", container_id, image_ref), "sqlite": run_engine_backend_probe(("sqlite3", "--version"), "sqlite", container_id, image_ref), "docker": run_engine_backend_probe(("docker", "--version"), "docker", container_id, image_ref), "compose": run_engine_backend_probe( ("docker", "compose", "version"), "Docker Compose", container_id, image_ref ), } if (not re.fullmatch(r"v20\.[0-9]+\.[0-9]+", versions["node"]) or not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:\s+.*)?", versions["sqlite"]) or not re.fullmatch(r"Docker version [0-9]+\.[0-9]+\.[0-9]+, build [A-Za-z0-9._-]+", versions["docker"]) or not re.fullmatch(r"Docker Compose version v?[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9._-]+)?", versions["compose"])): die("Engine backend runtime tool version format mismatch") return versions def validate_engine_backend_mounts(container, node_modules_read_only): expected = { "/app": ("bind", True), "/app/deploy/docker-compose.yml": ("bind", False), "/seed-api": ("bind", False), "/seed-data": ("bind", False), "/app/node_modules": ("bind", not node_modules_read_only), "/app/server/data": ("bind", True), "/app/server/storage": ("bind", True), "/app/server/logs": ("bind", True), "/var/run/docker.sock": ("bind", True), } mounts = container.get("Mounts") if not isinstance(mounts, list): die("Engine backend mount inventory is missing") actual = {} node_modules_source = None for mount in mounts: if not isinstance(mount, dict) or not isinstance(mount.get("Destination"), str): die("Engine backend mount inventory is invalid") destination = mount["Destination"] if destination in actual: die(f"Engine backend duplicate mount destination: {destination}") actual[destination] = (mount.get("Type"), bool(mount.get("RW"))) if destination == "/app/node_modules": node_modules_source = mount.get("Source") if actual != expected: die("Engine backend mount barrier mismatch") if (node_modules_source != str(ENGINE_CREDENTIAL_BACKEND_NODE_MODULES_DIR) or not ENGINE_CREDENTIAL_BACKEND_NODE_MODULES_DIR.is_absolute()): die("Engine backend node_modules mount source mismatch") def validate_engine_backend_mounts_for_installed_runtime( container, node_modules_read_only, ): publish_override = ( component_root("engine") / ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL ) publish_installed = publish_override.exists() or publish_override.is_symlink() if not publish_installed: # The legacy credential runtime remains byte-for-byte authoritative # until the exact Publish overlay has been installed. return validate_engine_backend_mounts(container, node_modules_read_only) if publish_override.is_symlink() or not publish_override.is_file(): die("installed Engine data product publish grant override is unsafe") try: publish_override_text = publish_override.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): die("installed Engine data product publish grant override cannot be read") if publish_override_text != expected_engine_data_product_publish_grant_override(): die("installed Engine data product publish grant override drift detected") expected_publish_mounts = { ENGINE_PUBLISH_GRANT_CONTAINER_PATH: ( "bind", True, str(ENGINE_PUBLISH_GRANT_STATE_PATH), ), ENGINE_EDP_PRIVATE_KEY_CONTAINER_PATH: ( "bind", False, str(ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE), ), } read_override = component_root("engine") / ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL read_installed = read_override.exists() or read_override.is_symlink() if read_installed: if read_override.is_symlink() or not read_override.is_file(): die("installed Engine data product read grant override is unsafe") try: read_override_text = read_override.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): die("installed Engine data product read grant override cannot be read") if read_override_text != expected_engine_data_product_read_grant_override(): die("installed Engine data product read grant override drift detected") expected_publish_mounts[ENGINE_READ_GRANT_CONTAINER_PATH] = ( "bind", True, str(ENGINE_READ_GRANT_STATE_PATH), ) mounts = container.get("Mounts") if not isinstance(mounts, list): die("Engine backend mount inventory is missing") baseline_mounts = [] observed_publish_mounts = set() for mount in mounts: if not isinstance(mount, dict) or not isinstance(mount.get("Destination"), str): die("Engine backend mount inventory is invalid") destination = mount["Destination"] expected = expected_publish_mounts.get(destination) if expected is None: baseline_mounts.append(mount) continue if destination in observed_publish_mounts: die(f"Engine backend duplicate mount destination: {destination}") actual = ( mount.get("Type"), bool(mount.get("RW")), mount.get("Source"), ) if actual != expected: die(f"Engine backend Publish mount barrier mismatch: {destination}") observed_publish_mounts.add(destination) if observed_publish_mounts != set(expected_publish_mounts): die("Engine backend Publish mount inventory mismatch") projected = dict(container) projected["Mounts"] = baseline_mounts return validate_engine_backend_mounts(projected, node_modules_read_only) def validate_engine_backend_node_intelligence_mounts_for_installed_runtime( container, node_modules_read_only, ): descriptor = current_engine_node_intelligence_descriptor() active = descriptor is not None and descriptor["action"] == "activate" if not active: # The pre-existing runtime guard remains authoritative when this # independent overlay is inactive. Any stray secret mount therefore # still fails its exact baseline inventory. return validate_engine_backend_mounts_for_installed_runtime( container, node_modules_read_only, ) validate_installed_engine_node_intelligence_source(descriptor) mounts = container.get("Mounts") if not isinstance(mounts, list): die("Engine backend mount inventory is missing") baseline_mounts = [] observed = 0 for mount in mounts: if not isinstance(mount, dict) or not isinstance(mount.get("Destination"), str): die("Engine backend mount inventory is invalid") if mount["Destination"] != ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH: baseline_mounts.append(mount) continue observed += 1 actual = ( mount.get("Type"), bool(mount.get("RW")), mount.get("Source"), ) expected = ( "bind", False, str(ENGINE_NODE_INTELLIGENCE_SECRET_FILE), ) if observed != 1 or actual != expected: die("Engine backend node-intelligence mount barrier mismatch") if observed != 1: die("Engine backend node-intelligence mount inventory mismatch") # Remove only the independently proven additive mount, then delegate the # remaining inventory to the existing Publish-aware guard and ultimately # the byte-protected legacy baseline validator. projected = dict(container) projected["Mounts"] = baseline_mounts return validate_engine_backend_mounts_for_installed_runtime( projected, node_modules_read_only, ) def validate_engine_backend_dependencies(container_id): package_path = component_root("engine") / "nodedc-source/package.json" try: package_stat = package_path.lstat() except FileNotFoundError: die("Engine backend package.json is missing") if stat.S_ISLNK(package_stat.st_mode) or not stat.S_ISREG(package_stat.st_mode): die("Engine backend package.json is unsafe") package = read_strict_json(package_path, "Engine backend package.json", max_bytes=2 * 1024 * 1024) dependencies = package.get("dependencies") if not isinstance(dependencies, dict) or not dependencies: die("Engine backend dependency manifest is invalid") names = sorted(dependencies) if any(not re.fullmatch(r"(?:@[a-z0-9._-]+/)?[a-z0-9._-]+", name) for name in names): die("Engine backend dependency name is unsafe") script = ( "const names=" + json.dumps(names, separators=(",", ":")) + ";for(const name of names)require.resolve(name,{paths:['/app']});" + "process.stdout.write('dependencies-ok')" ) result = subprocess.run( [str(DOCKER), "exec", "--workdir", "/app", container_id, "node", "-e", script], check=False, capture_output=True, text=True, timeout=120, ) if result.returncode != 0 or result.stderr.strip() or result.stdout.strip() != "dependencies-ok": die("Engine backend mounted dependency barrier mismatch") def engine_backend_node_modules_tree_sha256(container_id): script = """ const crypto=require('crypto'); const fs=require('fs'); const path=require('path'); const root='/app/node_modules'; const hash=crypto.createHash('sha256'); const lock=JSON.parse(fs.readFileSync('/app/package-lock.json','utf8')); if(lock.lockfileVersion!==3||!lock.packages||typeof lock.packages!=='object')throw new Error('lock-invalid'); const expected=new Map(Object.entries(lock.packages).filter(([key,value])=>key.startsWith('node_modules/')&&value&&typeof value==='object')); function secure(stat,label,{symlink=false}={}){ if(stat.uid!==0||stat.gid!==0)throw new Error('owner:'+label); if(!symlink&&(stat.mode&0o022)!==0)throw new Error('writable:'+label); } secure(fs.lstatSync(root),'root'); function packageVersion(packageRoot,key){ const manifest=JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'),'utf8')); const wanted=expected.get(key); if(!wanted||String(manifest.version||'')!==String(wanted.version||''))throw new Error('package-version:'+key); } function inspectPackageDirectory(nodeModulesRoot,relativePrefix){ const names=fs.readdirSync(nodeModulesRoot).sort((a,b)=>Buffer.from(a).compare(Buffer.from(b))); const roots=[]; for(const name of names){ if(name==='.bin'||name==='.package-lock.json')continue; const candidate=path.join(nodeModulesRoot,name); const stat=fs.lstatSync(candidate); if(name.startsWith('@')){ if(!stat.isDirectory())throw new Error('scope-not-directory'); for(const child of fs.readdirSync(candidate).sort((a,b)=>Buffer.from(a).compare(Buffer.from(b)))){ roots.push([path.join(candidate,child),relativePrefix+name+'/'+child]); } }else{ roots.push([candidate,relativePrefix+name]); } } for(const [packageRoot,key] of roots){ const stat=fs.lstatSync(packageRoot); if(!stat.isDirectory()||stat.isSymbolicLink())throw new Error('package-boundary:'+key); packageVersion(packageRoot,key); const nested=path.join(packageRoot,'node_modules'); if(fs.existsSync(nested))inspectPackageDirectory(nested,key+'/node_modules/'); } } inspectPackageDirectory(root,'node_modules/'); for(const [key,value] of expected){ const target=path.join('/app',key); if(!fs.existsSync(target)&&value.optional!==true)throw new Error('package-missing:'+key); if(fs.existsSync(target))packageVersion(target,key); } function visit(relative){ const absolute=path.join(root,relative); const names=fs.readdirSync(absolute).sort((a,b)=>Buffer.from(a).compare(Buffer.from(b))); for(const name of names){ if(name.includes('\\0')||name==='.'||name==='..')throw new Error('unsafe-name'); const child=relative?relative+'/'+name:name; const target=path.join(root,child); const stat=fs.lstatSync(target); const mode=(stat.mode&0o7777).toString(8); if(stat.isDirectory()){ secure(stat,child); hash.update('d\\0'+child+'\\0'+mode+'\\0'); visit(child); }else if(stat.isFile()){ secure(stat,child); hash.update('f\\0'+child+'\\0'+mode+'\\0'+stat.size+'\\0'); hash.update(fs.readFileSync(target)); }else if(stat.isSymbolicLink()){ secure(stat,child,{symlink:true}); const link=fs.readlinkSync(target); if(path.isAbsolute(link))throw new Error('absolute-link:'+child); const resolved=path.resolve(path.dirname(target),link); if(resolved!==root&&!resolved.startsWith(root+'/'))throw new Error('escaping-link:'+child); const real=fs.realpathSync(target); if(real!==root&&!real.startsWith(root+'/'))throw new Error('escaping-real-link:'+child); hash.update('l\\0'+child+'\\0'+mode+'\\0'+link+'\\0'); }else{ throw new Error('special-file'); } } } visit(''); process.stdout.write('sha256:'+hash.digest('hex')); """.strip() result = subprocess.run( [str(DOCKER), "exec", "--workdir", "/app", container_id, "node", "-e", script], check=False, capture_output=True, text=True, timeout=300, ) value = result.stdout.strip() if (result.returncode != 0 or result.stderr.strip() or not re.fullmatch(r"sha256:[a-f0-9]{64}", value)): die("Engine backend node_modules tree proof failed") return value def validate_runtime_owned_file(path, mode, label, min_bytes=1, max_bytes=1024 * 1024 * 1024): try: path_stat = path.lstat() except FileNotFoundError: die(f"{label} is missing") if (stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode) or path_stat.st_uid != 0 or path_stat.st_gid != 0 or stat.S_IMODE(path_stat.st_mode) != mode or path_stat.st_size < min_bytes or path_stat.st_size > max_bytes): die(f"{label} is unsafe") return path_stat def validate_engine_backend_runtime_override_file(): validate_runtime_owned_file( ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE, 0o600, "Engine backend immutable runtime override", max_bytes=64 * 1024, ) try: value = ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): die("Engine backend immutable runtime override is unreadable") if value != expected_engine_credential_backend_override(): die("Engine backend immutable runtime override drift detected") return sha256_file(ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE) def validate_engine_backend_activation_marker(): validate_runtime_owned_file( ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE, 0o600, "Engine backend immutable runtime activation marker", max_bytes=128, ) validate_engine_backend_runtime_override_file() metadata = read_engine_backend_runtime_metadata() validate_runtime_owned_file( ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE, 0o600, "Engine backend canonical rootfs", max_bytes=MAX_PAYLOAD_BYTES, ) expected = f"sha256:{sha256_file(ENGINE_CREDENTIAL_BACKEND_METADATA_FILE)}\n" try: actual = ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE.read_text(encoding="ascii") except (OSError, UnicodeDecodeError): die("Engine backend immutable runtime activation marker is unreadable") if (actual != expected or sha256_file(ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE) != metadata["rootfsSha256"]): die("Engine backend immutable runtime activation marker mismatch") inspect_engine_backend_derived_image(metadata) return metadata def quarantine_engine_backend_partial_runtime(): runtime_exists = ( ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR.exists() or ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR.is_symlink() ) if not runtime_exists: return "absent" ensure_root_runtime_directory(ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR) present = [ path for path in ( ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE, ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE, ENGINE_CREDENTIAL_BACKEND_METADATA_FILE, ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE, ) if path.exists() or path.is_symlink() ] if not present: return "empty" failed_root = ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR / "failed" ensure_root_runtime_directory(failed_root) quarantine = failed_root / f"{stamp()}-{os.getpid()}-{time.time_ns()}" quarantine.mkdir(mode=0o700) os.chown(quarantine, 0, 0) # Move the activation marker first. Once this succeeds, no subsequent # Engine Compose command can consume the incomplete override. ordered = sorted( present, key=lambda path: path != ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE, ) for path in ordered: os.replace(path, quarantine / path.name) fsync_directory(ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR) fsync_directory(quarantine) return "quarantined" def read_engine_backend_runtime_metadata(): validate_runtime_owned_file( ENGINE_CREDENTIAL_BACKEND_METADATA_FILE, 0o600, "Engine backend immutable runtime metadata", max_bytes=64 * 1024, ) value = read_strict_json( ENGINE_CREDENTIAL_BACKEND_METADATA_FILE, "Engine backend immutable runtime metadata", max_bytes=64 * 1024, ) expected_keys = { "schemaVersion", "release", "sourceContainerId", "sourceImageId", "derivedImageId", "rootfsSha256", "packageLockSha256", "nodeModulesTreeSha256", "overrideSha256", "tools", } if (not isinstance(value, dict) or set(value) != expected_keys or value.get("schemaVersion") != "nodedc.engine-backend-immutable-runtime/v1" or value.get("release") != "credential-sink-20260716-001" or not re.fullmatch(r"[a-f0-9]{12,64}", str(value.get("sourceContainerId"))) or not re.fullmatch(r"sha256:[a-f0-9]{64}", str(value.get("sourceImageId"))) or not re.fullmatch(r"sha256:[a-f0-9]{64}", str(value.get("derivedImageId"))) or not re.fullmatch(r"[a-f0-9]{64}", str(value.get("rootfsSha256"))) or value.get("packageLockSha256") != ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256 or not re.fullmatch(r"sha256:[a-f0-9]{64}", str(value.get("nodeModulesTreeSha256"))) or not re.fullmatch(r"[a-f0-9]{64}", str(value.get("overrideSha256"))) or not isinstance(value.get("tools"), dict) or set(value["tools"]) != {"node", "sqlite", "docker", "compose"}): die("Engine backend immutable runtime metadata mismatch") return value def inspect_engine_backend_derived_image( expected_metadata=None, image_ref=ENGINE_CREDENTIAL_BACKEND_IMAGE, expected_source_image_id=None, expected_rootfs_sha256=None, ): images = docker_json( ["image", "inspect", image_ref], "Engine backend immutable image inspect", ) if not isinstance(images, list) or len(images) != 1 or not isinstance(images[0], dict): die("Engine backend immutable image inspect shape mismatch") image = images[0] image_id = image.get("Id") labels = (image.get("Config") or {}).get("Labels") or {} if (not isinstance(image_id, str) or not re.fullmatch(r"sha256:[a-f0-9]{64}", image_id) or image.get("Architecture") != "amd64" or image.get("Os") != "linux" or not isinstance(labels, dict) or labels.get("org.nodedc.release") != "credential-sink-20260716-001" or labels.get("org.nodedc.package-lock-sha256") != ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256 or not re.fullmatch(r"sha256:[a-f0-9]{64}", str(labels.get("org.nodedc.source-image-id"))) or not re.fullmatch(r"[a-f0-9]{64}", str(labels.get("org.nodedc.rootfs-sha256")))): die("Engine backend immutable image identity mismatch") if (expected_source_image_id is not None and labels["org.nodedc.source-image-id"] != expected_source_image_id): die("Engine backend immutable image source collision") if (expected_rootfs_sha256 is not None and labels["org.nodedc.rootfs-sha256"] != expected_rootfs_sha256): die("Engine backend immutable image rootfs collision") if expected_metadata is not None and ( image_id != expected_metadata["derivedImageId"] or labels["org.nodedc.source-image-id"] != expected_metadata["sourceImageId"] or labels["org.nodedc.rootfs-sha256"] != expected_metadata["rootfsSha256"] ): die("Engine backend immutable image metadata drift detected") return image def ensure_engine_backend_release_image( build_context, source_image_id, rootfs_sha256, expected_tools, ): target_id = inspect_optional_local_image( ENGINE_CREDENTIAL_BACKEND_IMAGE, "Engine backend immutable release image", ) if target_id is not None: image = inspect_engine_backend_derived_image( image_ref=ENGINE_CREDENTIAL_BACKEND_IMAGE, expected_source_image_id=source_image_id, expected_rootfs_sha256=rootfs_sha256, ) if (image.get("Id") != target_id or engine_backend_tool_versions( image_ref=ENGINE_CREDENTIAL_BACKEND_IMAGE, ) != expected_tools): die("Engine backend immutable release image collision") return image build_ref = f"nodedc/deploy-build:engine-backend-{rootfs_sha256[:16]}" build_id = inspect_optional_local_image( build_ref, "Engine backend immutable build image", ) if build_id is None: build = subprocess.run( [ str(DOCKER), "build", "--no-cache", "--network", "none", "--pull=false", "-f", "Dockerfile", "-t", build_ref, ".", ], cwd=str(build_context), check=False, timeout=600, ) if build.returncode != 0: die("Engine backend immutable image build failed") build_id = inspect_optional_local_image( build_ref, "Engine backend immutable built image", ) if build_id is None: die("Engine backend immutable built image is missing") build_image = inspect_engine_backend_derived_image( image_ref=build_ref, expected_source_image_id=source_image_id, expected_rootfs_sha256=rootfs_sha256, ) if (build_image.get("Id") != build_id or engine_backend_tool_versions(image_ref=build_ref) != expected_tools): die("Engine backend immutable build image collision") # The deploy lock serializes canonical applies. Recheck the release tag # immediately before assigning it so an existing versioned identity is # never intentionally moved to a different image. target_id = inspect_optional_local_image( ENGINE_CREDENTIAL_BACKEND_IMAGE, "Engine backend immutable release image recheck", ) if target_id is not None: image = inspect_engine_backend_derived_image( image_ref=ENGINE_CREDENTIAL_BACKEND_IMAGE, expected_source_image_id=source_image_id, expected_rootfs_sha256=rootfs_sha256, ) if image.get("Id") != build_id: die("Engine backend immutable release image collision") return image tag = subprocess.run( [str(DOCKER), "image", "tag", build_id, ENGINE_CREDENTIAL_BACKEND_IMAGE], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) if tag.returncode != 0: die("Engine backend immutable release image tag failed") image = inspect_engine_backend_derived_image( image_ref=ENGINE_CREDENTIAL_BACKEND_IMAGE, expected_source_image_id=source_image_id, expected_rootfs_sha256=rootfs_sha256, ) if (image.get("Id") != build_id or engine_backend_tool_versions( image_ref=ENGINE_CREDENTIAL_BACKEND_IMAGE, ) != expected_tools): die("Engine backend immutable release image verification failed") return image def preflight_engine_credential_backend_runtime(): base_image_id = inspect_local_image( ENGINE_CREDENTIAL_BACKEND_BASE_IMAGE, "Engine backend base image", ) package_lock = component_root("engine") / "nodedc-source/package-lock.json" try: lock_stat = package_lock.lstat() except FileNotFoundError: die("Engine backend package-lock.json is missing") if (stat.S_ISLNK(lock_stat.st_mode) or not stat.S_ISREG(lock_stat.st_mode) or sha256_file(package_lock) != ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256): die("Engine backend package-lock barrier mismatch") try: node_modules_stat = ENGINE_CREDENTIAL_BACKEND_NODE_MODULES_DIR.lstat() except FileNotFoundError: die("Engine backend node_modules host source is missing") if (stat.S_ISLNK(node_modules_stat.st_mode) or not stat.S_ISDIR(node_modules_stat.st_mode) or node_modules_stat.st_uid != 0 or node_modules_stat.st_gid != 0 or node_modules_stat.st_mode & (stat.S_IWGRP | stat.S_IWOTH)): die("Engine backend node_modules host source is unsafe") container_id = engine_backend_container_id() containers = docker_json(["container", "inspect", container_id], "Engine backend container inspect") if not isinstance(containers, list) or len(containers) != 1 or not isinstance(containers[0], dict): die("Engine backend container inspect shape mismatch") container = containers[0] state = container.get("State") or {} started_at = state.get("StartedAt") initial_restart_count = int(container.get("RestartCount") or 0) if (state.get("Status") != "running" or (state.get("Health") or {}).get("Status") != "healthy" or not isinstance(started_at, str) or not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9:.]+Z", started_at)): die("Engine backend source container is not stable and healthy") config_image = (container.get("Config") or {}).get("Image") current_image_id = container.get("Image") validate_engine_backend_node_intelligence_mounts_for_installed_runtime( container, node_modules_read_only=config_image == ENGINE_CREDENTIAL_BACKEND_IMAGE, ) if config_image == ENGINE_CREDENTIAL_BACKEND_BASE_IMAGE and current_image_id == base_image_id: runtime_presence = { "activation": ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE.exists() or ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE.is_symlink(), "override": ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE.exists() or ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE.is_symlink(), "metadata": ENGINE_CREDENTIAL_BACKEND_METADATA_FILE.exists() or ENGINE_CREDENTIAL_BACKEND_METADATA_FILE.is_symlink(), "rootfs": ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE.exists() or ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE.is_symlink(), } if all(runtime_presence.values()): validate_engine_backend_activation_marker() override_sha256 = validate_engine_backend_runtime_override_file() metadata = read_engine_backend_runtime_metadata() rootfs_stat = validate_runtime_owned_file( ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE, 0o600, "Engine backend canonical rootfs", max_bytes=MAX_PAYLOAD_BYTES, ) image = inspect_engine_backend_derived_image(metadata) if (rootfs_stat.st_size <= 0 or sha256_file(ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE) != metadata["rootfsSha256"] or override_sha256 != metadata["overrideSha256"] or metadata["sourceImageId"] != base_image_id or metadata["derivedImageId"] != image.get("Id")): die("Engine backend prepared immutable runtime drift detected") mode = "verified-prepared-not-active" elif any(runtime_presence.values()): for name, path in ( ("activation", ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE), ("override", ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE), ("metadata", ENGINE_CREDENTIAL_BACKEND_METADATA_FILE), ("rootfs", ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE), ): if runtime_presence[name]: validate_runtime_owned_file( path, 0o600, f"Engine backend partial runtime {name}", max_bytes=MAX_PAYLOAD_BYTES, ) mode = "recoverable-prepared-partial" else: mode = "verified-running-base" source_image_id = base_image_id elif config_image == ENGINE_CREDENTIAL_BACKEND_IMAGE: override_sha256 = validate_engine_backend_runtime_override_file() metadata = read_engine_backend_runtime_metadata() image = inspect_engine_backend_derived_image(metadata) if current_image_id != image.get("Id") or override_sha256 != metadata["overrideSha256"]: die("Engine backend immutable runtime activation drift detected") mode = "verified-derived-retry" source_image_id = metadata["sourceImageId"] else: die("Engine backend current image barrier mismatch") tools = engine_backend_tool_versions(container_id=container_id) validate_engine_backend_dependencies(container_id) node_modules_tree_sha256 = engine_backend_node_modules_tree_sha256(container_id) if mode in ("verified-derived-retry", "verified-prepared-not-active"): if (node_modules_tree_sha256 != metadata["nodeModulesTreeSha256"] or tools != metadata["tools"]): die("Engine backend prepared runtime source proof drift detected") stable = docker_json(["container", "inspect", container_id], "Engine backend stability inspect") stable_container = stable[0] if isinstance(stable, list) and len(stable) == 1 else None stable_state = stable_container.get("State") if isinstance(stable_container, dict) else None if (not isinstance(stable_state, dict) or stable_state.get("Status") != "running" or (stable_state.get("Health") or {}).get("Status") != "healthy" or stable_state.get("StartedAt") != started_at or int(stable_container.get("RestartCount") or 0) != initial_restart_count): die("Engine backend restarted during immutable runtime preflight") return { "base_image_id": base_image_id, "container_id": container_id, "current_image_id": current_image_id, "mode": mode, "source_image_id": source_image_id, "node_modules_tree_sha256": node_modules_tree_sha256, "tools": tools, } def normalize_rootfs_member_name(name): if not isinstance(name, str) or not name or name.startswith("/") or "\\" in name: die("Engine backend export member path is unsafe") while name.startswith("./"): name = name[2:] parts = name.split("/") if not name or any(not part or part in (".", "..") for part in parts): die("Engine backend export member path is unsafe") if any(part.startswith("._") or part == "__MACOSX" for part in parts): die("Engine backend export contains AppleDouble metadata") return "/".join(parts) def engine_backend_rootfs_member_allowed(name, is_directory): excluded = ( "etc/hosts", "etc/hostname", "etc/resolv.conf", "etc/mtab", "etc/environment", "etc/profile", "etc/profile.d", "etc/ssl/private", "etc/ssh/ssh_host", "etc/machine-id", "etc/docker", "etc/containerd", "usr/local/etc", "usr/local/lib/node_modules", ) if any(name == value or name.startswith(f"{value}/") for value in excluded): return False basename = name.rsplit("/", 1)[-1].lower() if (basename in (".env", "id_rsa", "id_ed25519") or basename.startswith(".env.") or basename.endswith((".p12", ".pfx"))): die(f"Engine backend export secret-like path rejected: {name}") if any(name == root or name.startswith(f"{root}/") for root in ("bin", "sbin", "lib", "usr", "etc")): return True if name in ("var", "var/lib"): return is_directory return name == "var/lib/apk" or name.startswith("var/lib/apk/") def resolve_rootfs_link(name, linkname): if not isinstance(linkname, str) or not linkname or "\\" in linkname: die(f"Engine backend export link target is unsafe: {name}") parts = [] if linkname.startswith("/") else name.split("/")[:-1] for part in linkname.lstrip("/").split("/"): if part in ("", "."): continue if part == "..": if not parts: die(f"Engine backend export link escapes root: {name}") parts.pop() else: parts.append(part) target = "/".join(parts) if not target or not engine_backend_rootfs_member_allowed(target, False): die(f"Engine backend export link leaves safe roots: {name}") return linkname ENGINE_BACKEND_ROOTFS_OMITTED_TOOLCHAIN_LINKS = { "usr/local/bin/corepack": re.compile(r"^\.\./lib/node_modules/corepack/dist/corepack\.js$"), "usr/local/bin/npm": re.compile(r"^\.\./lib/node_modules/npm/bin/npm-cli\.js$"), "usr/local/bin/npx": re.compile(r"^\.\./lib/node_modules/npm/bin/npx-cli\.js$"), "usr/local/bin/yarn": re.compile(r"^/opt/yarn-v[0-9]+\.[0-9]+\.[0-9]+/bin/yarn$"), "usr/local/bin/yarnpkg": re.compile(r"^/opt/yarn-v[0-9]+\.[0-9]+\.[0-9]+/bin/yarnpkg$"), } def engine_backend_rootfs_toolchain_link_is_omitted(name, linkname): expected = ENGINE_BACKEND_ROOTFS_OMITTED_TOOLCHAIN_LINKS.get(name) if expected is None: return False if not isinstance(linkname, str) or not expected.fullmatch(linkname): die(f"Engine backend export omitted toolchain link target mismatch: {name}") return True ENGINE_BACKEND_ROOTFS_SECRET_PATTERNS = ( re.compile(br"-----BEGIN (?:OPENSSH |RSA |EC )?PRIVATE KEY-----"), re.compile(br"ndc_(?:edpwb|edprb|fndbg|edppr)_[A-Za-z0-9_-]{8,}"), re.compile(br"AKIA[0-9A-Z]{16}"), re.compile(br"gh[pousr]_[A-Za-z0-9]{20,}"), re.compile(br"xox[baprs]-[A-Za-z0-9-]{20,}"), ) def spool_and_scan_rootfs_file(source, name, expected_size): if expected_size < 0 or expected_size > MAX_FILE_BYTES: die(f"Engine backend export member size rejected: {name}") temporary = tempfile.SpooledTemporaryFile(max_size=8 * 1024 * 1024) total = 0 carry = b"" while True: chunk = source.read(1024 * 1024) if not chunk: break total += len(chunk) if total > MAX_FILE_BYTES: temporary.close() die(f"Engine backend export member too large: {name}") sample = carry + chunk if any(pattern.search(sample) for pattern in ENGINE_BACKEND_ROOTFS_SECRET_PATTERNS): temporary.close() die(f"Engine backend export secret material rejected: {name}") carry = sample[-256:] temporary.write(chunk) if total != expected_size: temporary.close() die(f"Engine backend export member size mismatch: {name}") temporary.seek(0) return temporary def build_canonical_engine_backend_rootfs(source_tar, destination_tar): total_bytes = 0 with tarfile.open(source_tar, "r:*") as source: members = [] seen = set() source_members = {} for member in source.getmembers(): name = normalize_rootfs_member_name(member.name) if name in seen: die(f"Engine backend export duplicate member: {name}") seen.add(name) source_members[name] = member if not engine_backend_rootfs_member_allowed(name, member.isdir()): continue if not (member.isdir() or member.isfile() or member.issym() or member.islnk()): die(f"Engine backend export special member rejected: {name}") if (member.issym() and engine_backend_rootfs_toolchain_link_is_omitted(name, member.linkname)): continue members.append((name, member)) members.sort(key=lambda item: item[0]) with destination_tar.open("xb") as raw_output: with tarfile.open(fileobj=raw_output, mode="w", format=tarfile.GNU_FORMAT) as output: for name, member in members: info = tarfile.TarInfo(name) info.uid = member.uid info.gid = member.gid info.uname = "" info.gname = "" info.mode = member.mode & 0o7777 info.mtime = 0 if member.isdir(): info.type = tarfile.DIRTYPE info.size = 0 output.addfile(info) continue if member.issym(): info.type = tarfile.SYMTYPE info.linkname = resolve_rootfs_link(name, member.linkname) info.size = 0 output.addfile(info) continue if member.islnk(): target_name = normalize_rootfs_member_name(member.linkname.lstrip("/")) target = source_members.get(target_name) if (target is None or not target.isfile() or not engine_backend_rootfs_member_allowed(target_name, False)): die(f"Engine backend export hardlink target mismatch: {name}") expected_size = target.size source_member = target else: expected_size = member.size source_member = member source_file = source.extractfile(source_member) if source_file is None: die(f"Engine backend export member is unreadable: {name}") with source_file: staged = spool_and_scan_rootfs_file(source_file, name, expected_size) with staged: info.type = tarfile.REGTYPE info.size = expected_size output.addfile(info, staged) total_bytes += expected_size if total_bytes > MAX_PAYLOAD_BYTES: die("Engine backend canonical rootfs exceeds size limit") destination_tar.chmod(0o600) return sha256_file(destination_tar) def atomic_write_root_file(path, data, mode=0o600): path.parent.mkdir(parents=True, exist_ok=True) directory_stat = path.parent.lstat() if (stat.S_ISLNK(directory_stat.st_mode) or not stat.S_ISDIR(directory_stat.st_mode) or directory_stat.st_uid != 0 or directory_stat.st_gid != 0 or stat.S_IMODE(directory_stat.st_mode) != 0o700): die(f"runtime directory is unsafe: {path.parent}") temporary = path.with_name(f".{path.name}.{os.getpid()}.{time.time_ns()}.tmp") descriptor = None try: descriptor = os.open(str(temporary), os.O_WRONLY | os.O_CREAT | os.O_EXCL, mode) os.write(descriptor, data) os.fsync(descriptor) os.fchown(descriptor, 0, 0) os.fchmod(descriptor, mode) os.close(descriptor) descriptor = None os.replace(temporary, path) fsync_directory(path.parent) finally: if descriptor is not None: os.close(descriptor) if temporary.exists(): temporary.unlink() def ensure_root_runtime_directory(path): try: path_stat = path.lstat() except FileNotFoundError: path.mkdir(parents=True, exist_ok=False) path_stat = path.lstat() if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISDIR(path_stat.st_mode): die(f"runtime directory is unsafe: {path}") os.chown(path, 0, 0) path.chmod(0o700) def prepare_engine_credential_backend_runtime(): preflight = preflight_engine_credential_backend_runtime() installed_template = component_root("engine") / ENGINE_CREDENTIAL_BACKEND_OVERRIDE_TEMPLATE_REL try: template_stat = installed_template.lstat() template = installed_template.read_text(encoding="utf-8") except (FileNotFoundError, OSError, UnicodeDecodeError): die("installed Engine backend immutable runtime template is unreadable") if (stat.S_ISLNK(template_stat.st_mode) or not stat.S_ISREG(template_stat.st_mode) or template != expected_engine_credential_backend_override()): die("installed Engine backend immutable runtime template mismatch") ensure_root_runtime_directory(ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR) if preflight["mode"] in ("verified-derived-retry", "verified-prepared-not-active"): validate_engine_backend_activation_marker() validate_engine_backend_runtime_override_file() metadata = read_engine_backend_runtime_metadata() image = inspect_engine_backend_derived_image(metadata) if engine_backend_tool_versions(image_ref=ENGINE_CREDENTIAL_BACKEND_IMAGE) != metadata["tools"]: die("Engine backend immutable runtime tool drift detected") return metadata if preflight["mode"] == "recoverable-prepared-partial": quarantine_engine_backend_partial_runtime() return prepare_engine_credential_backend_runtime() with tempfile.TemporaryDirectory(prefix="engine-backend-runtime-", dir=TMP_DIR) as temporary_name: temporary = Path(temporary_name) raw_export = temporary / "container-export.tar" canonical_rootfs = temporary / "canonical-rootfs.tar" export = subprocess.run( [str(DOCKER), "container", "export", "--output", str(raw_export), preflight["container_id"]], check=False, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=300, ) if export.returncode != 0: die("Engine backend container export failed") rootfs_sha256 = build_canonical_engine_backend_rootfs(raw_export, canonical_rootfs) raw_export.unlink() dockerfile = "\n".join(( "FROM scratch", "ADD canonical-rootfs.tar /", "LABEL org.nodedc.release=\"credential-sink-20260716-001\"", f"LABEL org.nodedc.source-image-id=\"{preflight['source_image_id']}\"", f"LABEL org.nodedc.rootfs-sha256=\"{rootfs_sha256}\"", f"LABEL org.nodedc.package-lock-sha256=\"{ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256}\"", "WORKDIR /app", "CMD [\"/usr/local/bin/node\",\"server/index.js\"]", "", )) (temporary / "Dockerfile").write_text(dockerfile, encoding="utf-8") (temporary / "Dockerfile").chmod(0o600) image = ensure_engine_backend_release_image( temporary, preflight["source_image_id"], rootfs_sha256, preflight["tools"], ) tools = preflight["tools"] os.chown(canonical_rootfs, 0, 0) canonical_rootfs.chmod(0o600) os.replace(canonical_rootfs, ENGINE_CREDENTIAL_BACKEND_ROOTFS_FILE) fsync_directory(ENGINE_CREDENTIAL_BACKEND_RUNTIME_DIR) atomic_write_root_file( ENGINE_CREDENTIAL_BACKEND_OVERRIDE_FILE, template.encode("utf-8"), ) override_sha256 = validate_engine_backend_runtime_override_file() metadata = { "schemaVersion": "nodedc.engine-backend-immutable-runtime/v1", "release": "credential-sink-20260716-001", "sourceContainerId": preflight["container_id"], "sourceImageId": preflight["source_image_id"], "derivedImageId": image["Id"], "rootfsSha256": rootfs_sha256, "packageLockSha256": ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256, "nodeModulesTreeSha256": preflight["node_modules_tree_sha256"], "overrideSha256": override_sha256, "tools": tools, } atomic_write_root_file( ENGINE_CREDENTIAL_BACKEND_METADATA_FILE, (json.dumps(metadata, ensure_ascii=False, indent=2, sort_keys=True) + "\n").encode("utf-8"), ) read_engine_backend_runtime_metadata() atomic_write_root_file( ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE, f"sha256:{sha256_file(ENGINE_CREDENTIAL_BACKEND_METADATA_FILE)}\n".encode("ascii"), ) validate_engine_backend_activation_marker() return metadata def preflight_engine_credential_sink_ready(): url = "http://127.0.0.1:3001/internal/engine-credential-sink/v1/health" try: request = urllib.request.Request(url, headers={"Cache-Control": "no-store"}) with NO_REDIRECT_OPENER.open(request, timeout=10) as response: if response.status != 200: die(f"Engine credential sink preflight failed: HTTP {response.status}") body = response.read(16 * 1024) except urllib.error.HTTPError as exc: die(f"Engine credential sink preflight failed: HTTP {exc.code}") except Exception as exc: die(f"Engine credential sink preflight failed: {type(exc).__name__}") try: value = json.loads(body) except (UnicodeDecodeError, json.JSONDecodeError): die("Engine credential sink preflight returned invalid JSON") if (value.get("ok") is not True or value.get("ready") is not True or value.get("service") != "NDC Engine Credential Sink" or value.get("issuer") != { "serviceId": "platform.engine-credential-provisioner", "keyId": ENGINE_CREDENTIAL_PROVISIONER_KEY_ID, }): die("Engine credential sink preflight identity/readiness mismatch") def engine_n8n_container_ids(): result = subprocess.run( [*compose_base_cmd("engine"), "ps", "-q", "n8n"], cwd=str(component_compose_root("engine")), check=False, capture_output=True, text=True, ) if result.returncode != 0: die("Engine n8n container lookup failed") container_ids = [line.strip() for line in result.stdout.splitlines() if line.strip()] if len(container_ids) != 1: die(f"Engine n8n topology mismatch: expected=1 actual={len(container_ids)}") return container_ids def inspect_engine_n8n_base_image(): images = docker_json(["image", "inspect", ENGINE_N8N_BASE_IMAGE], "Engine n8n base image inspect") if not isinstance(images, list) or len(images) != 1 or not isinstance(images[0], dict): die("Engine n8n base image inspect shape mismatch") image = images[0] image_id = image.get("Id") if not isinstance(image_id, str) or not re.fullmatch(r"sha256:[a-f0-9]{64}", image_id): die("Engine n8n base image id is invalid") if image.get("Architecture") != ENGINE_N8N_BASE_ARCHITECTURE or image.get("Os") != "linux": die("Engine n8n base image platform mismatch") return { "id": image_id, "architecture": image.get("Architecture"), "repo_digests": tuple(image.get("RepoDigests") or ()), } def inspect_container(container_id): containers = docker_json(["inspect", container_id], "Engine n8n container inspect") if not isinstance(containers, list) or len(containers) != 1 or not isinstance(containers[0], dict): die("Engine n8n container inspect shape mismatch") return containers[0] def engine_n8n_version(container_id): result = subprocess.run( [str(DOCKER), "exec", container_id, "n8n", "--version"], check=False, capture_output=True, text=True, timeout=60, ) version = result.stdout.strip() if result.returncode != 0 or version != ENGINE_N8N_VERSION: die("Engine n8n runtime version mismatch") return version def engine_n8n_package_loader_probe_script(): # LoadNodesAndCredentials.init() in n8n 2.3.2 initializes NODE_PATH before # it loads community packages. A standalone `node -e` probe must reproduce # that image-owned module-resolution step or peer imports such as # n8n-workflow fail even though the real n8n loader succeeds. return ( "process.env.NODE_PATH='" + ENGINE_N8N_NODE_MODULES_PATH + "';require('module').Module._initPaths();" "const {PackageDirectoryLoader}=require('" + ENGINE_N8N_NODE_MODULES_PATH + "/n8n-core');" "(async()=>{const loader=new PackageDirectoryLoader('" + ENGINE_N8N_RUNTIME_PACKAGE_PATH + "');await loader.loadAll();process.stdout.write(JSON.stringify({packageName:loader.packageName," "nodes:loader.types.nodes.map(x=>x.name),credentials:loader.types.credentials.map(x=>x.name)}))})()" ".catch(error=>{process.stderr.write(String(error&&error.code||'PROBE_ERROR'));process.exit(1)});" ) def engine_n8n_private_loader_catalog(container_id): container = inspect_container(container_id) package_mounts = [ mount for mount in (container.get("Mounts") or ()) if mount.get("Destination") == ENGINE_N8N_RUNTIME_PACKAGE_PATH ] if not package_mounts: return {"node_types": [], "credential_types": []} if len(package_mounts) != 1 or package_mounts[0].get("RW") is not False: die("Engine n8n package-loader probe found an unsafe package mount") script = engine_n8n_package_loader_probe_script() result = subprocess.run( [str(DOCKER), "exec", container_id, "node", "-e", script], check=False, capture_output=True, text=True, timeout=120, ) if result.returncode != 0: raw_category = (result.stderr or "").strip().upper() category = raw_category if re.fullmatch(r"[A-Z0-9_]{1,64}", raw_category) else "PROBE_ERROR" die(f"Engine n8n package-loader probe failed: category={category}") try: catalog = json.loads(result.stdout) except json.JSONDecodeError: die("Engine n8n package-loader probe returned invalid JSON") require_exact_json_keys(catalog, ("packageName", "nodes", "credentials"), "Engine n8n package-loader probe") if catalog.get("packageName") != "n8n-nodes-ndc": die("Engine n8n package-loader probe package identity mismatch") nodes = catalog.get("nodes") credentials = catalog.get("credentials") if not isinstance(nodes, list) or not all(isinstance(value, str) for value in nodes): die("Engine n8n package-loader probe node set is invalid") if not isinstance(credentials, list) or not all(isinstance(value, str) for value in credentials): die("Engine n8n package-loader probe credential set is invalid") return { "node_types": [f"n8n-nodes-ndc.{value}" for value in nodes], "credential_types": credentials, } def engine_n8n_current_state(): descriptor = current_engine_n8n_transition_descriptor() if descriptor and descriptor["action"] == "activate": return descriptor["releaseId"], descriptor return "verified_inactive", descriptor def validate_engine_n8n_base_compose_source(): compose_path = component_root("engine") / "docker-compose.yml" try: compose = compose_path.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): die("Engine base Compose cannot be read") services = re.findall(r"(?m)^ ([A-Za-z0-9_-]+):\s*$", compose) if tuple(services) != ENGINE_BASE_COMPOSE_SERVICES: die("Engine base Compose exact service topology mismatch") match = re.search(r"(?ms)^ n8n:\s*\n(?P.*?)(?=^ [A-Za-z0-9_-]+:\s*$)", compose) if not match: die("Engine base Compose n8n service cannot be isolated") n8n_service = match.group("body") expected_image = "image: docker.n8n.io/n8nio/n8n:${N8N_IMAGE_TAG:-2.3.2}" if expected_image not in n8n_service: die("Engine base Compose n8n version mismatch") required_fragments = ( "./n8n-data:/home/node/.n8n", "./nodedc-source/services/n8n/runtime-plugin:/opt/nodedc/runtime-plugin:ro", "n8n-postgres:", ) if any(fragment not in n8n_service for fragment in required_fragments): die("Engine base Compose n8n persistence/runtime dependency mismatch") forbidden_fragments = ( "build:", "N8N_CUSTOM_EXTENSIONS", "CUSTOM.", ENGINE_N8N_RUNTIME_PACKAGE_PATH, "N8N_USER_FOLDER", "N8N_COMMUNITY_PACKAGES_ENABLED", "N8N_COMMUNITY_PACKAGES_PREVENT_LOADING", "N8N_REINSTALL_MISSING_PACKAGES", ) if any(fragment in n8n_service for fragment in forbidden_fragments): die("Engine base Compose contains a non-baseline n8n activation setting") def preflight_engine_n8n_transition(descriptor, enforce_expected_current): validate_engine_n8n_base_compose_source() if descriptor["action"] == "activate": validate_engine_n8n_staged_release(descriptor) base_image = inspect_engine_n8n_base_image() container_id = engine_n8n_container_ids()[0] container = inspect_container(container_id) state = container.get("State") or {} if state.get("Status") != "running": die("Engine n8n baseline container is not running") if container.get("Image") != base_image["id"]: die("Engine n8n running image does not match the local 2.3.2 base image") version = engine_n8n_version(container_id) current_state, current_descriptor = engine_n8n_current_state() state_matches = current_state == descriptor["expectedCurrent"] current_catalog = engine_n8n_private_loader_catalog(container_id) current_types = current_catalog["node_types"] current_credentials = current_catalog["credential_types"] expected_current_types = list(ENGINE_N8N_NODE_TYPES) if current_state != "verified_inactive" else [] expected_current_credentials = ( list(engine_n8n_credential_types_for_release(current_state)) if current_state != "verified_inactive" else [] ) if current_types != expected_current_types: die("Engine n8n current live private-node set does not match its transition state") if current_credentials != expected_current_credentials: die("Engine n8n current live private-credential set does not match its transition state") catalog_descriptor = ( current_descriptor if current_state != "verified_inactive" and current_descriptor is not None else inactive_engine_n8n_descriptor(descriptor) ) validate_engine_n8n_catalog_payload(component_root("engine"), catalog_descriptor) if enforce_expected_current and not state_matches: die( "Engine n8n transition stale current state: " f"expected={descriptor['expectedCurrent']} actual={current_state}" ) return { "base_image_id": base_image["id"], "base_image_architecture": base_image["architecture"], "base_image_repo_digests": base_image["repo_digests"], "container_id": container_id, "current_state": current_state, "current_types": current_types, "current_credentials": current_credentials, "n8n_version": version, "state_matches": state_matches, } def add_engine_n8n_sealed_member_paths(expected_paths, member_name): parts = PurePosixPath(member_name).parts if not parts or parts[0] != "package": die("Engine sealed n8n package member root mismatch") for depth in range(1, len(parts) + 1): expected_paths.add(PurePosixPath(*parts[:depth]).as_posix()) def validate_engine_n8n_sealed_release(release_dir, staged_release_dir, descriptor): if release_dir.is_symlink() or not release_dir.is_dir(): die("Engine sealed n8n release is missing or unsafe") for filename in ("package.tgz", "release.json", "rollback.json"): installed = release_dir / filename staged = staged_release_dir / filename if installed.is_symlink() or not installed.is_file(): die(f"Engine sealed n8n release member is unsafe: {filename}") if sha256_file(installed) != sha256_file(staged): die(f"Engine sealed n8n release member mismatch: {filename}") package_root = release_dir / "package" if package_root.is_symlink() or not package_root.is_dir(): die("Engine sealed n8n package tree is missing or unsafe") expected_paths = {"package", "package.tgz", "release.json", "rollback.json"} with tarfile.open(staged_release_dir / "package.tgz", "r:gz") as archive: for member in archive: # npm package tarballs do not have to carry explicit directory # members. Extraction still creates those parent directories, so # they are part of the exact sealed tree rather than unexpected # content. add_engine_n8n_sealed_member_paths(expected_paths, member.name) target = release_dir.joinpath(*PurePosixPath(member.name).parts) target_stat = target.lstat() if stat.S_ISLNK(target_stat.st_mode): die(f"Engine sealed n8n package symlink rejected: {member.name}") if member.isdir(): if not stat.S_ISDIR(target_stat.st_mode): die(f"Engine sealed n8n package directory mismatch: {member.name}") else: if not stat.S_ISREG(target_stat.st_mode) or target_stat.st_size != member.size: die(f"Engine sealed n8n package file mismatch: {member.name}") source = archive.extractfile(member) if source is None: die(f"Engine sealed n8n package file unreadable: {member.name}") import hashlib expected_sha = hashlib.sha256(source.read()).hexdigest() if sha256_file(target) != expected_sha: die(f"Engine sealed n8n package content mismatch: {member.name}") actual_paths = { path.relative_to(release_dir).as_posix() for path in release_dir.rglob("*") } if actual_paths != expected_paths: die("Engine sealed n8n release tree exact member set mismatch") for path in [release_dir, *release_dir.rglob("*")]: path_stat = path.lstat() if path_stat.st_uid != 0 or path_stat.st_gid != 0: die(f"Engine sealed n8n release ownership mismatch: {path}") expected_mode = 0o555 if stat.S_ISDIR(path_stat.st_mode) else 0o444 if stat.S_IMODE(path_stat.st_mode) != expected_mode: die(f"Engine sealed n8n release mode mismatch: {path}") package_json = read_strict_json(package_root / "package.json", "Engine sealed n8n package.json") if package_json.get("name") != "n8n-nodes-ndc" or package_json.get("version") != descriptor["packageVersion"]: die("Engine sealed n8n package identity mismatch") def ensure_engine_n8n_sealed_release(descriptor, current_stamp): staged_release_dir = validate_engine_n8n_staged_release(descriptor) release_parent = ENGINE_N8N_SEALED_RELEASES_ROOT / "releases" / "n8n-nodes-ndc" release_parent.mkdir(parents=True, exist_ok=True) for path in ( ENGINE_N8N_SEALED_RELEASES_ROOT, ENGINE_N8N_SEALED_RELEASES_ROOT / "releases", release_parent, ): path_stat = path.lstat() if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISDIR(path_stat.st_mode): die(f"Engine sealed n8n release parent is unsafe: {path}") os.chown(path, 0, 0) path.chmod(0o755) release_dir = release_parent / descriptor["releaseId"] if release_dir.exists() or release_dir.is_symlink(): validate_engine_n8n_sealed_release(release_dir, staged_release_dir, descriptor) return release_dir next_dir = release_parent / f".{descriptor['releaseId']}.next-{current_stamp}" if next_dir.exists() or next_dir.is_symlink(): die(f"Engine sealed n8n release staging path already exists: {next_dir}") next_dir.mkdir(mode=0o700) for filename in ("package.tgz", "release.json", "rollback.json"): shutil.copy2(staged_release_dir / filename, next_dir / filename) with tarfile.open(staged_release_dir / "package.tgz", "r:gz") as archive: for member in archive: relative = PurePosixPath(member.name) target = next_dir.joinpath(*relative.parts) if member.isdir(): target.mkdir(parents=True, exist_ok=True) continue target.parent.mkdir(parents=True, exist_ok=True) source = archive.extractfile(member) if source is None: die(f"Engine sealed n8n package member unreadable: {member.name}") with target.open("xb") as output: shutil.copyfileobj(source, output) for path in [next_dir, *next_dir.rglob("*")]: path_stat = path.lstat() if stat.S_ISLNK(path_stat.st_mode): die(f"Engine sealed n8n release symlink rejected: {path}") os.chown(path, 0, 0) if stat.S_ISDIR(path_stat.st_mode): path.chmod(0o555) elif stat.S_ISREG(path_stat.st_mode): path.chmod(0o444) else: die(f"Engine sealed n8n release special member rejected: {path}") if release_dir.exists() or release_dir.is_symlink(): die("Engine sealed n8n release appeared concurrently") next_dir.rename(release_dir) validate_engine_n8n_sealed_release(release_dir, staged_release_dir, descriptor) return release_dir def wait_engine_n8n_readiness(container_id): script = ( "const http=require('http');const req=http.get('http://127.0.0.1:5678/healthz/readiness'," "r=>{r.resume();process.exit(r.statusCode===200?0:1)});req.on('error',()=>process.exit(1));" "req.setTimeout(4000,()=>{req.destroy();process.exit(1)});" ) for _attempt in range(1, 61): result = subprocess.run( [str(DOCKER), "exec", container_id, "node", "-e", script], check=False, capture_output=True, text=True, timeout=10, ) if result.returncode == 0: return time.sleep(5) die("Engine n8n readiness check failed") def accept_engine_n8n_runtime(descriptor): base_image = inspect_engine_n8n_base_image() container_id = engine_n8n_container_ids()[0] container = inspect_container(container_id) state = container.get("State") or {} if state.get("Status") != "running" or container.get("Image") != base_image["id"]: die("Engine n8n runtime image/state acceptance failed") wait_engine_n8n_readiness(container_id) engine_n8n_version(container_id) config = container.get("Config") or {} env = set(config.get("Env") or ()) mounts = container.get("Mounts") or () package_mounts = [mount for mount in mounts if mount.get("Destination") == ENGINE_N8N_RUNTIME_PACKAGE_PATH] if descriptor["action"] == "activate": expected_source = f"/volume2/nodedc-demo/{descriptor['sealedReleaseRelativePath']}" if len(package_mounts) != 1: die("Engine n8n package mount count mismatch") package_mount = package_mounts[0] if package_mount.get("Source") != expected_source or package_mount.get("RW") is not False: die("Engine n8n sealed package mount mismatch") required_env = { "N8N_USER_FOLDER=/home/node", "N8N_COMMUNITY_PACKAGES_ENABLED=true", "N8N_COMMUNITY_PACKAGES_PREVENT_LOADING=false", "N8N_REINSTALL_MISSING_PACKAGES=false", } if not required_env.issubset(env): die("Engine n8n package loader environment mismatch") labels = config.get("Labels") or {} if labels.get("nodedc.n8n-private-extension.release") != descriptor["releaseId"]: die("Engine n8n release label mismatch") if labels.get("nodedc.n8n-private-extension.package-sha256") != descriptor["packageSha256"]: die("Engine n8n package label mismatch") else: if package_mounts: die("Engine n8n inactive baseline still has the private package mount") inactive_forbidden = ( "N8N_USER_FOLDER=", "N8N_COMMUNITY_PACKAGES_ENABLED=", "N8N_COMMUNITY_PACKAGES_PREVENT_LOADING=", "N8N_REINSTALL_MISSING_PACKAGES=", ) if any(item.startswith(inactive_forbidden) for item in env): die("Engine n8n inactive baseline still has private package loader flags") if any(item.startswith("N8N_CUSTOM_EXTENSIONS=") or item.startswith("CUSTOM.") for item in env): die("Engine n8n custom extension environment is forbidden") started_at = str(state.get("StartedAt") or "") logs = subprocess.run( [str(DOCKER), "logs", "--since", started_at, container_id], check=False, capture_output=True, text=True, timeout=60, ) if logs.returncode != 0: die("Engine n8n loader log inspection failed") scoped_logs = f"{logs.stdout}\n{logs.stderr}".lower() error_patterns = ( r"n8n-nodes-ndc.{0,400}(?:cannot find|error|failed|duplicate|eacces|syntaxerror)", r"(?:cannot find|error loading|failed to load|duplicate|eacces|syntaxerror).{0,400}n8n-nodes-ndc", ) if any(re.search(pattern, scoped_logs, re.DOTALL) for pattern in error_patterns): die("Engine n8n private extension loader log acceptance failed") expected_types = list(ENGINE_N8N_NODE_TYPES) if descriptor["action"] == "activate" else [] expected_credentials = ( list(engine_n8n_credential_types_for_release(descriptor["releaseId"])) if descriptor["action"] == "activate" else [] ) loader_catalog = engine_n8n_private_loader_catalog(container_id) if loader_catalog["node_types"] != expected_types: die("Engine n8n live package-loader node catalog acceptance failed") if loader_catalog["credential_types"] != expected_credentials: die("Engine n8n live package-loader credential catalog acceptance failed") validate_engine_n8n_catalog_payload(component_root("engine"), descriptor) first_restart_count = int(container.get("RestartCount") or 0) time.sleep(2) stable_container = inspect_container(container_id) if stable_container.get("State", {}).get("Status") != "running": die("Engine n8n container did not remain running") if int(stable_container.get("RestartCount") or 0) != first_restart_count: die("Engine n8n container restarted during acceptance") return { "container_id": container_id, "image_id": base_image["id"], "node_types": expected_types, "credential_types": list(descriptor["expectedCredentialTypes"]), } def plan_artifact(artifact): validate_artifact_location(artifact) ensure_layout() sha = sha256_file(artifact) publish_grant_preflight = None node_intelligence_preflight = None mcp_control_plane_preflight = None mcp_ontology_sdk_preflight = None mcp_autonomy_provider_v5_preflight = None composite_provider_v4_preflight = None provider_rotating_slot_preflight = None provider_authority_diagnostics_preflight = None depttrans_zone_authority_v1_preflight = None provider_target_host_policy_preflight = None provider_catalog_preflight = None with tempfile.TemporaryDirectory(prefix="plan-", dir=TMP_DIR) as tmp: manifest, entries, payload_dir = load_artifact(artifact, Path(tmp)) transition_descriptor = None transition_preflight = None if is_engine_n8n_transition(manifest["component"], entries): transition_descriptor = read_engine_n8n_transition_descriptor( payload_dir / ENGINE_N8N_TRANSITION_DESCRIPTOR_REL ) transition_preflight = preflight_engine_n8n_transition( transition_descriptor, enforce_expected_current=False, ) if is_engine_data_product_publish_grant_slice(manifest["component"], entries): publish_grant_preflight = ( preflight_engine_data_product_publish_grant_predecessor(payload_dir) ) if is_engine_node_intelligence_transition(manifest["component"], entries): node_intelligence_preflight = preflight_engine_node_intelligence_predecessor( payload_dir ) if is_engine_mcp_control_plane_slice(manifest["component"], entries): mcp_control_plane_preflight = preflight_engine_mcp_control_plane_predecessor( payload_dir ) if is_engine_mcp_ontology_sdk_slice(manifest["component"], entries): mcp_ontology_sdk_preflight = preflight_engine_mcp_ontology_sdk_predecessor( payload_dir ) if is_engine_mcp_autonomy_provider_v5_slice(manifest["component"], entries): mcp_autonomy_provider_v5_preflight = ( preflight_engine_mcp_autonomy_provider_v5_predecessor(payload_dir) ) if is_engine_composite_provider_v4_slice(manifest["component"], entries): composite_provider_v4_preflight = preflight_engine_composite_provider_v4_predecessor() if is_engine_provider_rotating_slot_slice(manifest["component"], entries): provider_rotating_slot_preflight = preflight_engine_provider_rotating_slot_predecessor() if is_engine_provider_authority_diagnostics_slice(manifest["component"], entries): provider_authority_diagnostics_preflight = ( preflight_engine_provider_authority_diagnostics_predecessor() ) if is_engine_depttrans_zone_authority_v1_slice(manifest["component"], entries): depttrans_zone_authority_v1_preflight = ( preflight_engine_depttrans_zone_authority_v1_predecessor() ) if is_engine_provider_target_host_policy_slice(manifest["component"], entries): provider_target_host_policy_preflight = ( preflight_engine_provider_target_host_policy_predecessor() ) if is_engine_provider_security_catalog_slice(manifest["component"], entries): provider_catalog_preflight = preflight_engine_provider_security_catalog_predecessor() component = manifest["component"] root = component_root(component) compose_root = component_compose_root(component) services = component_services(component, entries) builds = component_builds(component, entries) touches_publish_grant = is_engine_data_product_publish_grant_slice( component, entries, ) touches_composite_provider_v4 = is_engine_composite_provider_v4_slice( component, entries, ) touches_provider_rotating_slot = is_engine_provider_rotating_slot_slice( component, entries, ) touches_provider_authority_diagnostics = is_engine_provider_authority_diagnostics_slice( component, entries, ) touches_depttrans_zone_authority_v1 = is_engine_depttrans_zone_authority_v1_slice( component, entries, ) touches_provider_target_host_policy = is_engine_provider_target_host_policy_slice( component, entries, ) touches_agent_grant_migration = is_engine_agent_full_grant_migration_slice( component, entries, ) publish_grant_predecessor_sha256 = None agent_grant_migration_predecessor_sha256 = None credential_backend_preflight = None if ( touches_engine_credential_sink(component, entries) or touches_publish_grant or touches_composite_provider_v4 or touches_provider_rotating_slot or touches_provider_authority_diagnostics or touches_depttrans_zone_authority_v1 or touches_provider_target_host_policy or touches_agent_grant_migration ): credential_backend_preflight = preflight_engine_credential_backend_runtime() if touches_publish_grant: if publish_grant_preflight is None: die("Engine data product publish grant preflight is missing") publish_grant_predecessor_sha256 = publish_grant_preflight["compose_sha256"] if credential_backend_preflight["mode"] != "verified-derived-retry": die("Engine data product publish grant requires the active immutable credential backend") if touches_composite_provider_v4: if composite_provider_v4_preflight is None: die("Engine composite provider v4 preflight is missing") if credential_backend_preflight["mode"] != "verified-derived-retry": die("Engine composite provider v4 requires the active immutable credential backend") if touches_provider_rotating_slot: if provider_rotating_slot_preflight is None: die("Engine provider rotating slot preflight is missing") if credential_backend_preflight["mode"] != "verified-derived-retry": die("Engine provider rotating slot requires the active immutable credential backend") if touches_provider_authority_diagnostics: if provider_authority_diagnostics_preflight is None: die("Engine provider authority diagnostics preflight is missing") if credential_backend_preflight["mode"] != "verified-derived-retry": die("Engine provider authority diagnostics requires the active immutable credential backend") if touches_depttrans_zone_authority_v1: if depttrans_zone_authority_v1_preflight is None: die("Engine Depttrans zone authority v1 preflight is missing") if credential_backend_preflight["mode"] != "verified-derived-retry": die("Engine Depttrans zone authority v1 requires the active immutable credential backend") if touches_provider_target_host_policy: if provider_target_host_policy_preflight is None: die("Engine provider target host policy preflight is missing") if credential_backend_preflight["mode"] != "verified-derived-retry": die("Engine provider target host policy requires the active immutable credential backend") if touches_agent_grant_migration: agent_grant_migration_predecessor_sha256 = ( preflight_engine_agent_full_grant_migration_predecessor() ) if credential_backend_preflight["mode"] != "verified-derived-retry": die("Engine agent full grant migration requires the active immutable credential backend") print("== plan ==") print(f"artifact={artifact.name}") print(f"sha256={sha}") print(f"id={manifest['id']}") print(f"component={component}") print(f"type={manifest['type']}") print(f"payload_root={root}") print(f"compose_root={compose_root}") compose_project = component_compose_project(component) if compose_project: print(f"compose_project={compose_project}") for build_root, build_args in builds: print(f"build_root={build_root}") print(f"build={' '.join((str(DOCKER),) + tuple(build_args))}") print(f"services={' '.join(services)}") if node_intelligence_preflight: descriptor = node_intelligence_preflight["descriptor"] print(f"node_intelligence_transition={descriptor['action']}") print(f"node_intelligence_release={descriptor['releaseId']}") print(f"node_intelligence_current_state={node_intelligence_preflight['current_state']}") print(f"node_intelligence_upstream_commit={descriptor['upstream']['commit']}") print(f"node_intelligence_image={descriptor['image']['tag']}") print(f"node_intelligence_image_config_sha256={descriptor['image']['configSha256']}") if descriptor["action"] == "activate": print(f"node_intelligence_image_archive_sha256={descriptor['image']['archiveSha256']}") print("node_intelligence_build=offline-image-load") print("node_intelligence_pull=never") print(f"node_intelligence_secret=runner-managed:{ENGINE_NODE_INTELLIGENCE_SECRET_FILE}") print("node_intelligence_host_ports=none") print(f"predecessor_gateway_sha256={node_intelligence_preflight['gateway_sha256']}") print(f"backend_current_barrier={node_intelligence_preflight['backend_mode']}") if mcp_control_plane_preflight: print("engine_mcp_transition=managed-reader-grant+validator-reconciliation") print("engine_mcp_version=0.5.0") print("engine_mcp_installer=0.1.4") print("engine_mcp_reader_grant=plan+apply+accept+rollback") print( "predecessor_gateway_sha256=" f"{mcp_control_plane_preflight['predecessor_gateway_sha256']}" ) print( "target_gateway_sha256=" f"{mcp_control_plane_preflight['target_gateway_sha256']}" ) print(f"backend_current_barrier={mcp_control_plane_preflight['backend_mode']}") print("node_intelligence_image=preserved") print("n8n_l1=untouched") if mcp_ontology_sdk_preflight: print("engine_mcp_transition=separate-ontology+gelios-sdk-query-auth") print("engine_mcp_version=0.6.0") print("engine_mcp_installer=0.1.5") print("engine_mcp_ontology=separate-read-only-proxy") print("engine_provider_package=gelios.provider.v2") print("engine_provider_credential=httpQueryAuth") print( "predecessor_gateway_sha256=" f"{mcp_ontology_sdk_preflight['predecessor_gateway_sha256']}" ) print( "target_gateway_sha256=" f"{mcp_ontology_sdk_preflight['target_gateway_sha256']}" ) print(f"backend_current_barrier={mcp_ontology_sdk_preflight['backend_mode']}") print("node_intelligence_image=preserved") print("n8n_l1=untouched") if mcp_autonomy_provider_v5_preflight: print("engine_mcp_transition=capability-scoped-autonomy+provider-v5") print("engine_mcp_version=0.6.0") print("engine_mcp_installer=0.1.6") print("engine_mcp_authority=mcp-capability-intersect-user-objective") print("engine_mcp_plan=machine-safety-barrier") print("engine_mcp_retry_stop=three-identical-failures-without-new-evidence") print("engine_provider_packages=gelios.provider.v4,gelios.provider.v5") print("engine_data_product=fleet.positions.current.v4") print( "predecessor_gateway_sha256=" f"{mcp_autonomy_provider_v5_preflight['predecessor_gateway_sha256']}" ) print( "target_gateway_sha256=" f"{mcp_autonomy_provider_v5_preflight['target_gateway_sha256']}" ) print(f"backend_current_barrier={mcp_autonomy_provider_v5_preflight['backend_mode']}") print("node_intelligence_image=preserved") print("n8n_l1=untouched") if provider_catalog_preflight: print("engine_provider_catalog_transition=gelios-unit-profile-authority-v8") print("engine_provider_package=gelios.provider.v8") print("engine_provider_credential=ndcProviderRotatingAccessApi") print( "engine_provider_endpoint=https://api.geliospro.com/api/v1/units?" "incltrip=true&inclcntrs=true&inclsnsrs=true&incllsv=true" ) print("engine_data_product=fleet.units.profile.current.v1") print( "predecessor_catalog_sha256=" f"{provider_catalog_preflight['catalog_sha256']}" ) print(f"target_catalog_sha256={ENGINE_PROVIDER_SECURITY_CATALOG_TARGET_SHA256}") print(f"backend_current_barrier={provider_catalog_preflight['backend_mode']}") print("n8n_l1=untouched") print("engine_ui=untouched") print("engine_databases=untouched") print("mcp_nginx=untouched") if composite_provider_v4_preflight: print("engine_composite_provider_transition=exact-v3-to-v4") print("engine_provider_package=gelios.provider.v4") print( "engine_provider_capabilities=" "gelios.monitoring_config.current.read,gelios.units.current.read" ) print("engine_provider_requests=monitoring-config,units?incltrip=true") print("engine_data_product=fleet.positions.current.v3") for changed_path in ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES: print(f"engine_composite_provider_changed_path={changed_path}") print( f"engine_composite_provider_predecessor_sha256[{changed_path}]=" f"{composite_provider_v4_preflight['predecessor_sha256'][changed_path]}" ) print( f"engine_composite_provider_target_sha256[{changed_path}]=" f"{composite_provider_v4_preflight['target_sha256'][changed_path]}" ) print(f"backend_current_barrier={composite_provider_v4_preflight['backend_mode']}") print("provider_credential_values=preserved") print("backend_force_recreate=yes") print("backend_pull=never") print("n8n_l1=untouched") print("engine_ui=untouched") print("engine_databases=untouched") if provider_rotating_slot_preflight: print("engine_provider_credential_transition=exact-active-slot-alignment") print("engine_provider_package=gelios.provider.v4") print("engine_provider_auth_mode=gelios.rest-rotating-bearer.v3") print("engine_provider_credential_slot=ndcProviderRotatingAccessApi") print("engine_data_product=fleet.positions.current.v3") for changed_path in ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES: print(f"engine_provider_rotating_slot_changed_path={changed_path}") print( f"engine_provider_rotating_slot_predecessor_sha256[{changed_path}]=" f"{provider_rotating_slot_preflight['predecessor_sha256'][changed_path]}" ) print( f"engine_provider_rotating_slot_target_sha256[{changed_path}]=" f"{provider_rotating_slot_preflight['target_sha256'][changed_path]}" ) print(f"backend_current_barrier={provider_rotating_slot_preflight['backend_mode']}") print("provider_credential_values=preserved") print("backend_force_recreate=yes") print("backend_pull=never") print("l2_graph=untouched") print("n8n_l1=untouched") print("engine_ui=untouched") print("engine_databases=untouched") if provider_authority_diagnostics_preflight: print("engine_validation_transition=private-node-name-reconciliation") print("engine_validation_boundary=engine-pinned-private-node-catalog") print("private_node_identity=node-id-or-exact-display-name") print("provider_credential_values=preserved") for changed_path in ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES: print(f"engine_provider_authority_changed_path={changed_path}") print( f"engine_provider_authority_predecessor_sha256[{changed_path}]=" f"{provider_authority_diagnostics_preflight['predecessor_sha256'][changed_path]}" ) print( f"engine_provider_authority_target_sha256[{changed_path}]=" f"{provider_authority_diagnostics_preflight['target_sha256'][changed_path]}" ) print(f"backend_current_barrier={provider_authority_diagnostics_preflight['backend_mode']}") print("backend_force_recreate=yes") print("backend_pull=never") print("l2_graph=untouched") print("n8n_l1=untouched") print("engine_ui=untouched") print("engine_databases=untouched") if depttrans_zone_authority_v1_preflight: print_engine_depttrans_zone_authority_v1_plan( depttrans_zone_authority_v1_preflight ) if provider_target_host_policy_preflight: print("engine_provider_transport_transition=exact-literal-target-host") print("engine_provider_package=gelios.provider.v4") print("engine_data_product=fleet.positions.current.v3") print("provider_credential_values=preserved") for changed_path in ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES: print(f"engine_provider_target_host_policy_changed_path={changed_path}") print( f"engine_provider_target_host_policy_predecessor_sha256[{changed_path}]=" f"{provider_target_host_policy_preflight['predecessor_sha256'][changed_path]}" ) print( f"engine_provider_target_host_policy_target_sha256[{changed_path}]=" f"{provider_target_host_policy_preflight['target_sha256'][changed_path]}" ) print(f"backend_current_barrier={provider_target_host_policy_preflight['backend_mode']}") print("backend_force_recreate=yes") print("backend_pull=never") print("l2_graph=untouched") print("n8n_l1=untouched") print("engine_ui=untouched") print("engine_databases=untouched") if transition_descriptor: print(f"n8n_transition={transition_descriptor['action']}") print(f"n8n_version={transition_descriptor['n8nVersion']}") print(f"n8n_release={transition_descriptor['releaseId']}") print(f"n8n_package_sha256={transition_descriptor['packageSha256']}") print(f"n8n_base_image={transition_descriptor['baseImage']}") print(f"n8n_base_image_id={transition_preflight['base_image_id']}") print(f"n8n_base_image_architecture={transition_preflight['base_image_architecture']}") for repo_digest in transition_preflight["base_image_repo_digests"]: print(f"n8n_base_image_repo_digest={repo_digest}") print(f"n8n_current_state={transition_preflight['current_state']}") print(f"n8n_expected_current={transition_descriptor['expectedCurrent']}") print(f"n8n_state_matches={'yes' if transition_preflight['state_matches'] else 'not-yet'}") print(f"n8n_sealed_package=/volume2/nodedc-demo/{transition_descriptor['sealedReleaseRelativePath']}") print("n8n_build=none") print("n8n_pull=never") print("n8n_force_recreate=yes") print("n8n_persistent_data=preserved:/volume2/nodedc-demo/n8n-data") print("n8n_database=preserved:n8n-postgres") print(f"n8n_expected_node_types={','.join(transition_descriptor['expectedNodeTypes'])}") print(f"n8n_expected_credential_types={','.join(transition_descriptor['expectedCredentialTypes'])}") print(f"n8n_rollback_baseline={transition_descriptor['rollbackBaseline']}") touches_map_gateway = component == "platform" and any(rel == "platform/services/map-gateway" or rel.startswith("platform/services/map-gateway/") for rel in entries) touches_external_data_plane = component == "platform" and touches_external_data_plane_files(entries) if component == "module-foundry" or touches_map_gateway: print(f"runtime_secret=runner-managed:{MAP_GATEWAY_SECRET_FILE}") if component == "module-foundry": print(f"runtime_grants=runner-managed:{EXTERNAL_DATA_PLANE_READER_GRANTS_DIR}") print(f"runtime_grants=runner-managed:{FOUNDRY_BINDING_GRANTS_DIR}") print(f"runtime_private_key=runner-managed:{FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE}") print(f"runtime_public_trust=runner-managed:{FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE}") if touches_external_data_plane: print(f"runtime_secret=runner-managed:{EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}") print(f"runtime_grants=runner-managed:{EXTERNAL_DATA_PLANE_READER_GRANTS_DIR}") print(f"runtime_public_trust=runner-managed:{FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE}") if touches_engine_credential_sink(component, entries): print(f"runtime_identity_key_id={ENGINE_CREDENTIAL_PROVISIONER_KEY_ID}") print(f"runtime_private_key=runner-managed:{ENGINE_CREDENTIAL_PROVISIONER_PRIVATE_KEY_FILE}") print(f"runtime_public_key=runner-managed:{ENGINE_CREDENTIAL_SINK_PUBLIC_KEY_FILE}") if touches_engine_credential_sink(component, entries): print(f"backend_source_container_id={credential_backend_preflight['container_id']}") print(f"backend_source_image_id={credential_backend_preflight['source_image_id']}") print(f"backend_current_image_id={credential_backend_preflight['current_image_id']}") print(f"backend_current_barrier={credential_backend_preflight['mode']}") print(f"backend_derived_image={ENGINE_CREDENTIAL_BACKEND_IMAGE}") print(f"backend_package_lock_sha256={ENGINE_CREDENTIAL_BACKEND_PACKAGE_LOCK_SHA256}") print(f"backend_node_modules_tree_sha256={credential_backend_preflight['node_modules_tree_sha256']}") print("backend_rootfs=safe-roots:/bin,/sbin,/lib,/usr,/etc,/var/lib/apk") print("backend_build_network=none") print("backend_pull=never") print("backend_force_recreate=yes") if touches_publish_grant or touches_external_data_plane: print(f"runtime_private_key=runner-managed:{ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE}") print(f"runtime_public_trust=runner-managed:{ENGINE_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE}") if touches_publish_grant: print(f"publish_grant_transition={publish_grant_preflight['mode']}") for changed_path in publish_grant_preflight["changed_paths"]: print(f"publish_grant_changed_path={changed_path}") print(f"predecessor_compose_sha256={publish_grant_predecessor_sha256}") print(f"backend_current_barrier={credential_backend_preflight['mode']}") print(f"runtime_private_state=runner-managed:{component_root('engine') / ENGINE_PUBLISH_GRANT_STATE_REL}") if publish_grant_preflight["mode"] == "installed-composite-provider-update": print("engine_provider_package=gelios.provider.v4") print("engine_provider_requests=monitoring-config,units?incltrip=true") print("engine_data_product=fleet.positions.current.v3") print( "predecessor_catalog_sha256=" f"{ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_PREDECESSOR_SHA256}" ) print( "target_catalog_sha256=" f"{ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_TARGET_SHA256}" ) print("provider_credential_values=preserved") if touches_agent_grant_migration: print(f"predecessor_store_sha256={agent_grant_migration_predecessor_sha256}") print(f"target_store_sha256={ENGINE_AGENT_FULL_GRANT_MIGRATION_TARGET_SHA256}") print(f"backend_current_barrier={credential_backend_preflight['mode']}") print("agent_grant_migration=v1-full-bundle-to-v2-named-full-developer-profile") print("backend_pull=never") if component in ("proxy-contur", "dc-amd-proxy") or touches_map_gateway: print(f"runtime_secret=runner-synced:{MAP_EGRESS_PROXY_SECRET_FILE}") if component == "dc-amd-proxy": print(f"runtime_state=runner-prepared:{DC_AMD_PROXY_RUNTIME_DIR}") if state_has_sha(sha): print("state=sha-already-applied") elif state_has_patch_id(manifest["id"]): print("state=patch-id-already-applied") else: print("state=new") print("== files ==") for rel in entries: print(f" {rel}") def add_backup_path(tar, root, rel): src = root / rel if src.exists(): tar.add(src, arcname=rel, recursive=True) return True return False def create_backup(root, backup_dir, entries, include_nginx_html): existing = [] missing = [] with tarfile.open(backup_dir / "source-before.tgz", "w:gz") as tar: for rel in entries: if add_backup_path(tar, root, rel): existing.append(rel) else: missing.append(rel) if include_nginx_html and (root / "nginx-html").exists(): tar.add(root / "nginx-html", arcname="nginx-html", recursive=True) existing.append("nginx-html") (backup_dir / "existing-files.txt").write_text("\n".join(existing) + ("\n" if existing else ""), encoding="utf-8") (backup_dir / "missing-files.txt").write_text("\n".join(missing) + ("\n" if missing else ""), encoding="utf-8") def read_backup_path_list(path): if not path.is_file(): die(f"deploy backup path list missing: {path}") return [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()] def validate_backup_partition(entries, existing, missing, label): entry_set = set(entries) existing_set = set(existing) missing_set = set(missing) if len(entry_set) != len(entries): die(f"{label} entries contain duplicates") if len(existing_set) != len(existing) or len(missing_set) != len(missing): die(f"{label} backup path lists contain duplicates") if existing_set & missing_set or existing_set | missing_set != entry_set: die(f"{label} backup entry set mismatch") for rel in entries: if any(rel != other and rel.startswith(other.rstrip("/") + "/") for other in entries): die(f"{label} entries overlap") return existing_set, missing_set def materialize_backup_tree(backup_archive, restore_root, existing): seen_names = set() seen_roots = set() directory_modes = [] member_count = 0 payload_bytes = 0 with tarfile.open(backup_archive, "r:gz") as archive: for member in archive: member_count += 1 if member_count > MAX_MEMBER_COUNT: die("Platform rollback backup has too many members") validate_posix_path(member.name) if member.name in seen_names: die("Platform rollback backup contains duplicate members") seen_names.add(member.name) owners = [ rel for rel in existing if member.name == rel or member.name.startswith(rel.rstrip("/") + "/") ] if len(owners) != 1: die("Platform rollback backup contains an unexpected member") if member.name == owners[0]: seen_roots.add(owners[0]) if not (member.isfile() or member.isdir()): die("Platform rollback backup contains an unsupported member") if member.mode & (stat.S_ISUID | stat.S_ISGID): die("Platform rollback backup contains a privileged member") target = tar_name_to_path(restore_root, member.name) if member.isdir(): target.mkdir(parents=True, exist_ok=True) target.chmod(0o755) directory_modes.append((target, stat.S_IMODE(member.mode) & 0o777)) continue if member.size > MAX_FILE_BYTES: die("Platform rollback backup member is too large") payload_bytes += member.size if payload_bytes > MAX_PAYLOAD_BYTES: die("Platform rollback backup is too large") target.parent.mkdir(parents=True, exist_ok=True) if target.exists(): die("Platform rollback backup member collision") source = archive.extractfile(member) if source is None: die("Platform rollback backup member is unreadable") with source, target.open("xb") as output: shutil.copyfileobj(source, output, 1024 * 1024) target.chmod(stat.S_IMODE(member.mode) & 0o777) if seen_roots != set(existing): die("Platform rollback backup root set mismatch") for target, mode in sorted(directory_modes, key=lambda item: len(item[0].parts), reverse=True): target.chmod(mode) def restore_platform_overlay(root, backup_dir, entries, current_stamp): existing = read_backup_path_list(backup_dir / "existing-files.txt") missing = read_backup_path_list(backup_dir / "missing-files.txt") existing_set, missing_set = validate_backup_partition( entries, existing, missing, "Platform rollback", ) backup_archive = backup_dir / "source-before.tgz" try: backup_stat = backup_archive.lstat() except FileNotFoundError: die("Platform rollback backup archive is missing") if stat.S_ISLNK(backup_stat.st_mode) or not stat.S_ISREG(backup_stat.st_mode): die("Platform rollback backup archive is unsafe") with tempfile.TemporaryDirectory(prefix="platform-rollback-", dir=TMP_DIR) as tmp: restore_root = Path(tmp) materialize_backup_tree(backup_archive, restore_root, existing_set) for rel in entries: destination = root / rel destination_resolved = destination.resolve(strict=False) if not is_relative_to(destination_resolved, root.resolve()): die("Platform rollback target escaped component root") if destination.is_symlink(): die("Platform rollback target is a symlink") if rel in existing_set: staged = restore_root / rel rollback_stamp = f"{current_stamp}-platform-rollback" if staged.is_dir(): replace_directory(staged, destination, rollback_stamp) elif staged.is_file(): replace_file(staged, destination, rollback_stamp) else: die("Platform rollback staged path is missing") continue if rel not in missing_set or not destination.exists(): continue if destination.is_dir(): shutil.rmtree(destination) elif destination.is_file(): destination.unlink() else: die("Platform rollback target has unsupported type") return len(entries) def rollback_platform_apply(root, backup_dir, entries, current_stamp, runtime_started, applied_services): existing = read_backup_path_list(backup_dir / "existing-files.txt") missing = read_backup_path_list(backup_dir / "missing-files.txt") existing_set, missing_set = validate_backup_partition( entries, existing, missing, "Platform runtime rollback", ) edp_was_new = PLATFORM_EXTERNAL_DATA_PLANE_COMPOSE_REL in missing_set candidate_cleanup_failed = False if runtime_started and edp_was_new: # Remove only candidate-only containers while its Compose overlay is # still installed. Deliberately omit --volumes: rollback never deletes # newly written data, even when the first EDP activation is rejected. try: candidate_services = tuple( service for service in applied_services if service == EXTERNAL_DATA_PLANE_SERVICE ) if not candidate_services: die("Platform candidate-only EDP runtime selection is invalid") stop_and_remove_compose_services( "platform", candidate_services, ) except Exception: candidate_cleanup_failed = True restored_count = restore_platform_overlay(root, backup_dir, entries, current_stamp) if candidate_cleanup_failed: die("Platform candidate-only runtime cleanup failed after source restore") if not runtime_started: return f"source-restored-runtime-unchanged:{restored_count}" baseline_entries = [rel for rel in entries if rel in existing_set] if edp_was_new: baseline_entries = [ rel for rel in baseline_entries if not touches_external_data_plane_files((rel,)) ] baseline_services = tuple( service for service in applied_services if not edp_was_new or service not in (EXTERNAL_DATA_PLANE_SERVICE, EXTERNAL_DATA_PLANE_DATABASE_SERVICE) ) if baseline_services: run_component_runtime("platform", baseline_entries, baseline_services) if baseline_services == (EXTERNAL_DATA_PLANE_SERVICE,): # The restored EDP may predate managed provisioning. Prove the # stable baseline contract without requiring a candidate-only field. healthcheck_url(external_data_plane_healthcheck(require_managed=False)) else: run_healthchecks("platform", baseline_entries, baseline_services) return f"source+runtime-restored:{restored_count}" def rollback_engine_apply(root, backup_dir, entries, current_stamp, runtime_started, applied_services): existing = read_backup_path_list(backup_dir / "existing-files.txt") missing = read_backup_path_list(backup_dir / "missing-files.txt") restore_entries = list(entries) if "nginx-html" in existing: restore_entries.append("nginx-html") validate_backup_partition( restore_entries, existing, missing, "Engine runtime rollback", ) restored_count = restore_platform_overlay( root, backup_dir, restore_entries, current_stamp, ) if not runtime_started: return f"source-restored-runtime-unchanged:{restored_count}" run_component_runtime("engine", entries, applied_services) run_healthchecks("engine", entries, applied_services) return f"source+runtime-restored:{restored_count}" def inactive_engine_n8n_descriptor(descriptor): inactive = dict(descriptor) inactive["action"] = "rollback-inactive" inactive["expectedCurrent"] = descriptor["releaseId"] inactive["expectedNodeTypes"] = [] inactive["expectedCredentialTypes"] = [] return inactive def restore_engine_n8n_transition(root, backup_dir, entries, descriptor, current_stamp): existing = set(read_backup_path_list(backup_dir / "existing-files.txt")) missing = set(read_backup_path_list(backup_dir / "missing-files.txt")) if existing | missing != set(entries): die("Engine n8n rollback backup entry set mismatch") required_baseline = { ENGINE_N8N_NODES_CATALOG_REL, ENGINE_N8N_CREDENTIALS_CATALOG_REL, ENGINE_N8N_SCHEMA_META_REL, } if not required_baseline.issubset(existing): die("Engine n8n rollback baseline catalogs were not present before apply") with tempfile.TemporaryDirectory(prefix="engine-n8n-rollback-", dir=TMP_DIR) as tmp: restore_root = Path(tmp) with tarfile.open(backup_dir / "source-before.tgz", "r:gz") as archive: for rel in entries: if rel not in existing: continue try: member = archive.getmember(rel) except KeyError: die(f"Engine n8n rollback backup member missing: {rel}") if not member.isfile(): die(f"Engine n8n rollback backup member is not a file: {rel}") source = archive.extractfile(member) if source is None: die(f"Engine n8n rollback backup member unreadable: {rel}") staged = restore_root / rel staged.parent.mkdir(parents=True, exist_ok=True) with staged.open("xb") as output: shutil.copyfileobj(source, output) replace_file(staged, root / rel, f"{current_stamp}-rollback") if ENGINE_N8N_TRANSITION_DESCRIPTOR_REL not in existing: inactive = inactive_engine_n8n_descriptor(descriptor) descriptor_path = root / ENGINE_N8N_TRANSITION_DESCRIPTOR_REL descriptor_path.parent.mkdir(parents=True, exist_ok=True) next_path = descriptor_path.with_name(f"{descriptor_path.name}.next-{current_stamp}-rollback") if next_path.exists() or next_path.is_symlink(): die(f"Engine n8n rollback descriptor staging path exists: {next_path}") next_path.write_text(json.dumps(inactive, indent=2) + "\n", encoding="utf-8") os.replace(next_path, descriptor_path) restored_descriptor = current_engine_n8n_transition_descriptor() if restored_descriptor is None: die("Engine n8n rollback did not restore an explicit baseline descriptor") run_compose("engine", ("n8n",), entries) accept_engine_n8n_runtime(restored_descriptor) return restored_descriptor["action"] def replace_file(src, dst, current_stamp): dst.parent.mkdir(parents=True, exist_ok=True) tmp = dst.with_name(f"{dst.name}.next-{current_stamp}") if tmp.exists(): die(f"staging path already exists: {tmp}") shutil.copy2(src, tmp) os.replace(tmp, dst) def replace_directory(src, dst, current_stamp): dst.parent.mkdir(parents=True, exist_ok=True) next_path = dst.with_name(f"{dst.name}.next-{current_stamp}") prev_path = dst.with_name(f"{dst.name}.prev-{current_stamp}") if next_path.exists() or prev_path.exists(): die(f"staging path already exists near: {dst}") shutil.copytree(src, next_path) if dst.exists(): dst.rename(prev_path) next_path.rename(dst) def copy_payload_path(payload_dir, root, rel, current_stamp): src = payload_dir / rel dst = root / rel root_real = root.resolve() dst_real = dst.resolve(strict=False) if not is_relative_to(dst_real, root_real): die(f"target escaped component root: {rel}") if src.is_dir(): replace_directory(src, dst, current_stamp) elif src.is_file(): replace_file(src, dst, current_stamp) else: die(f"payload path is neither file nor directory: {rel}") def restore_overlay_source(root, backup_dir, entries, current_stamp): existing = set(read_backup_path_list(backup_dir / "existing-files.txt")) missing = set(read_backup_path_list(backup_dir / "missing-files.txt")) if existing | missing != set(entries) or existing & missing: die("credential bridge rollback backup entry set mismatch") with tempfile.TemporaryDirectory(prefix="credential-bridge-rollback-", dir=TMP_DIR) as tmp: restore_root = Path(tmp) names = set() with tarfile.open(backup_dir / "source-before.tgz", "r:gz") as archive: for member in archive: validate_posix_path(member.name) if member.name in names: die(f"credential bridge rollback duplicate backup member: {member.name}") names.add(member.name) if not path_is_covered_by_files_list(member.name, existing): die(f"credential bridge rollback unexpected backup member: {member.name}") if not (member.isfile() or member.isdir()): die(f"credential bridge rollback special backup member: {member.name}") target = tar_name_to_path(restore_root, member.name) if member.isdir(): target.mkdir(parents=True, exist_ok=True) target.chmod(0o755) continue if member.size > MAX_FILE_BYTES: die(f"credential bridge rollback backup member too large: {member.name}") target.parent.mkdir(parents=True, exist_ok=True) source = archive.extractfile(member) if source is None: die(f"credential bridge rollback unreadable backup member: {member.name}") with source, target.open("xb") as output: shutil.copyfileobj(source, output, 1024 * 1024) target.chmod(0o644) for rel in entries: destination = root / rel if rel in existing: source = restore_root / rel if not source.exists() or source.is_symlink(): die(f"credential bridge rollback source missing or unsafe: {rel}") copy_payload_path(restore_root, root, rel, f"{current_stamp}-rollback") continue if not destination.exists() and not destination.is_symlink(): continue if destination.is_symlink(): die(f"credential bridge rollback installed symlink rejected: {rel}") retained = destination.with_name(f"{destination.name}.failed-{current_stamp}") if retained.exists() or retained.is_symlink(): die(f"credential bridge rollback retained path already exists: {retained}") destination.rename(retained) def rollback_engine_credential_bridge( component, root, backup_dir, entries, current_stamp, engine_backend_recreated=False, engine_backend_initial_mode=None, ): if not touches_engine_credential_sink(component, entries): die("credential bridge rollback called for unrelated artifact") restore_overlay_source(root, backup_dir, entries, current_stamp) if not engine_backend_recreated: if engine_backend_initial_mode == "verified-derived-retry": validate_engine_backend_activation_marker() if not engine_backend_immutable_runtime_is_current(): raise ReconciliationRequired("engine_backend_prior_derived_runtime_unproven") return "engine-sink-source-restored-active-runtime-preserved" if engine_backend_initial_mode == "verified-prepared-not-active": validate_engine_backend_activation_marker() return "engine-sink-source-restored-prepared-runtime-preserved" quarantine = quarantine_engine_backend_partial_runtime() return f"engine-sink-source-restored-before-backend-recreate-{quarantine}" rollback_entries = ("nodedc-source/server/index.js",) # The first Compose call may have failed before replacing the base # container. Original sink entries explicitly authorize the already proven # prepared override for this rollback recreate. run_compose("engine", ("nodedc-backend",), entries) healthcheck_compose_service("engine", "nodedc-backend") run_healthchecks("engine", rollback_entries) runtime = preflight_engine_credential_backend_runtime() if runtime["mode"] != "verified-derived-retry": die("Engine backend immutable rollback runtime acceptance failed") return "engine-sink-source-restored" def rollback_engine_node_intelligence( root, backup_dir, entries, current_stamp, runtime_started=False, node_intelligence_service_stopped=False, ): candidate_descriptor = current_engine_node_intelligence_descriptor() candidate_active = ( candidate_descriptor is not None and candidate_descriptor["action"] == "activate" ) cleanup_failed = False if runtime_started and candidate_active: try: stop_and_remove_compose_services( "engine", (ENGINE_NODE_INTELLIGENCE_SERVICE,), ) except Exception: cleanup_failed = True restore_overlay_source(root, backup_dir, entries, current_stamp) restored = current_engine_node_intelligence_descriptor() restored_action = restored["action"] if restored is not None else "inactive" if cleanup_failed: die("Engine node-intelligence candidate runtime cleanup failed after source restore") reconcile_runtime = runtime_started or node_intelligence_service_stopped if not reconcile_runtime: return f"source-restored-runtime-unchanged:{restored_action}" if restored is not None and restored["action"] == "activate": validate_installed_engine_node_intelligence_source(restored) ensure_engine_node_intelligence_secret() install_engine_node_intelligence_image(restored) run_engine_node_intelligence_compose( (ENGINE_NODE_INTELLIGENCE_SERVICE, "nodedc-backend"), ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES, ) accept_engine_node_intelligence_runtime(restored) return "active" run_engine_node_intelligence_compose( ("nodedc-backend",), ENGINE_NODE_INTELLIGENCE_ROLLBACK_ENTRIES, ) healthcheck_compose_service("engine", "nodedc-backend") healthcheck_url("http://127.0.0.1:3001/health") backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine node-intelligence rollback backend barrier failed") assert_engine_node_intelligence_container_absent() return restored_action def seal_n8n_private_extension_release(root, rel): release_dir = root / rel if not release_dir.is_dir() or release_dir.is_symlink(): die("private extension release was not installed as a directory") for path in [release_dir, *release_dir.rglob("*")]: path_stat = path.lstat() if stat.S_ISLNK(path_stat.st_mode): die(f"private extension installed symlink rejected: {path}") os.chown(path, 0, 0) if stat.S_ISDIR(path_stat.st_mode): path.chmod(0o555) elif stat.S_ISREG(path_stat.st_mode): path.chmod(0o444) else: die(f"private extension installed special file rejected: {path}") def publish_engine_dist(root, current_stamp): dist = root / "nodedc-source" / "dist" if not dist.is_dir(): return dst = root / "nginx-html" next_path = root / f"nginx-html.next-{current_stamp}" prev_path = root / f"nginx-html.prev-{current_stamp}" if next_path.exists() or prev_path.exists(): die("nginx-html staging path already exists") shutil.copytree(dist, next_path) if dst.exists(): dst.rename(prev_path) next_path.rename(dst) def component_publish_dist(component, entries=None): if not COMPONENTS[component].get("publish_dist"): return False if component == "engine": return entries is not None and any( rel == "nodedc-source/dist" or rel.startswith("nodedc-source/dist/") for rel in entries ) return True def run_build(component, entries=None): for build_root, build_args in component_builds(component, entries): dockerfile = "Dockerfile" for idx, arg in enumerate(build_args): if arg == "-f" and idx + 1 < len(build_args): dockerfile = build_args[idx + 1] break if not (build_root / dockerfile).is_file(): die(f"Dockerfile not found in build root: {build_root / dockerfile}") cmd = [str(DOCKER), *build_args] env = os.environ.copy() if component == "tasker": env["DOCKER_BUILDKIT"] = "0" subprocess.run(cmd, cwd=str(build_root), env=env, check=True) def compose_base_cmd(component, allow_prepared_engine_backend=False): cmd = [str(DOCKER), "compose"] compose_project = component_compose_project(component) if compose_project: cmd.extend(["-p", compose_project]) env_file = component_compose_env_file(component) if env_file: cmd.extend(["--env-file", str(env_file)]) for compose_file in component_compose_files( component, allow_prepared_engine_backend=allow_prepared_engine_backend, ): cmd.extend(["-f", str(compose_file)]) return cmd def run_compose(component, services, entries=None): compose_root = component_compose_root(component) allow_prepared_engine_backend = touches_engine_credential_sink(component, entries) cmd = [ *compose_base_cmd( component, allow_prepared_engine_backend=allow_prepared_engine_backend, ), "up", "-d", "--force-recreate", ] if (is_engine_n8n_transition(component, entries) or touches_engine_credential_sink(component, entries) or touches_engine_credential_provisioner(component, entries) or (component == "engine" and ENGINE_CREDENTIAL_BACKEND_ACTIVATION_FILE.exists())): cmd.extend(["--pull", "never"]) if COMPONENTS[component].get("compose_build"): cmd.append("--build") if component_compose_no_deps(component, entries): cmd.append("--no-deps") cmd.extend(services) try: subprocess.run(cmd, cwd=str(compose_root), check=True) except subprocess.CalledProcessError: subprocess.run( [ *compose_base_cmd( component, allow_prepared_engine_backend=allow_prepared_engine_backend, ), "logs", "--no-color", "--tail=180", *services, ], cwd=str(compose_root), check=False, ) raise subprocess.run( [ *compose_base_cmd( component, allow_prepared_engine_backend=allow_prepared_engine_backend, ), "ps", ], cwd=str(compose_root), check=True, ) def run_engine_node_intelligence_compose(services, entries): if not is_engine_node_intelligence_transition("engine", entries): die("Engine node-intelligence Compose called for unrelated artifact") compose_root = component_compose_root("engine") cmd = [ *compose_base_cmd("engine"), "up", "-d", "--force-recreate", "--pull", "never", "--no-deps", *services, ] try: subprocess.run(cmd, cwd=str(compose_root), check=True) except subprocess.CalledProcessError: subprocess.run( [ *compose_base_cmd("engine"), "logs", "--no-color", "--tail=180", *services, ], cwd=str(compose_root), check=False, ) raise subprocess.run( [*compose_base_cmd("engine"), "ps"], cwd=str(compose_root), check=True, ) def stop_and_remove_compose_services(component, services): if not services: return compose_root = component_compose_root(component) subprocess.run( [*compose_base_cmd(component), "rm", "--stop", "--force", *services], cwd=str(compose_root), check=True, ) def prepare_component_runtime(component, entries=None): if is_engine_data_product_publish_grant_slice(component, entries): ensure_engine_edp_managed_provisioner_keypair() ensure_engine_publish_grant_private_state() if is_engine_mcp_control_plane_slice(component, entries): ensure_engine_edp_managed_provisioner_keypair() ensure_engine_data_product_grant_private_state(include_reader=True) if is_engine_n8n_transition(component, entries): descriptor = current_engine_n8n_transition_descriptor() if descriptor is None: die("installed Engine n8n transition descriptor is missing") if descriptor["action"] == "activate": ensure_engine_n8n_sealed_release(descriptor, stamp()) return if is_engine_node_intelligence_transition(component, entries): ensure_engine_node_intelligence_secret() descriptor = current_engine_node_intelligence_descriptor() if descriptor is None: die("installed Engine node-intelligence descriptor is missing") if descriptor["action"] == "activate": install_engine_node_intelligence_image(descriptor) return if touches_engine_credential_sink(component, entries): ensure_engine_credential_issuer_keypair() prepare_engine_credential_backend_runtime() return if component == "module-foundry": ensure_map_gateway_admin_secret() ensure_foundry_edp_managed_provisioner_keypair() ensure_root_owned_grant_directory( EXTERNAL_DATA_PLANE_READER_GRANTS_DIR, "external data plane reader grants", ) ensure_root_owned_grant_directory( FOUNDRY_BINDING_GRANTS_DIR, "foundry binding grants", ) return if component == "proxy-contur": sync_map_egress_proxy_secret() return if component == "dc-amd-proxy": # The connector access value arrives only through the one-time pairing # endpoint. The artifact must never carry it, and the service user is # the only non-root identity that can write this directory. sync_map_egress_proxy_secret() try: runtime_stat = DC_AMD_PROXY_RUNTIME_DIR.lstat() except FileNotFoundError: DC_AMD_PROXY_RUNTIME_DIR.mkdir(parents=True, exist_ok=False) runtime_stat = DC_AMD_PROXY_RUNTIME_DIR.lstat() if stat.S_ISLNK(runtime_stat.st_mode) or not stat.S_ISDIR(runtime_stat.st_mode): die(f"dc-amd-proxy runtime directory is unsafe: {DC_AMD_PROXY_RUNTIME_DIR}") os.chown(DC_AMD_PROXY_RUNTIME_DIR, MAP_GATEWAY_RUNTIME_GID, MAP_GATEWAY_RUNTIME_GID) DC_AMD_PROXY_RUNTIME_DIR.chmod(0o700) connector_state = DC_AMD_PROXY_RUNTIME_DIR / "connector-access" if connector_state.exists() or connector_state.is_symlink(): connector_stat = connector_state.lstat() if ( stat.S_ISLNK(connector_stat.st_mode) or not stat.S_ISREG(connector_stat.st_mode) or connector_stat.st_uid != MAP_GATEWAY_RUNTIME_GID or connector_stat.st_mode & (stat.S_IRWXG | stat.S_IRWXO) or connector_stat.st_size > 512 ): die("dc-amd-proxy connector pair state is unsafe") return if component == "platform" and entries is not None: touches_map_gateway = any(rel == "platform/services/map-gateway" or rel.startswith("platform/services/map-gateway/") for rel in entries) touches_external_data_plane = touches_external_data_plane_files(entries) if touches_map_gateway: # These NAS directories are the only canonical persistent stores # for all Map Page instances. Creation lives in the root-owned # runner, never in a user-supplied artifact or a Compose side effect. cache_root = Path("/volume1/docker/nodedc-platform/map-gateway") for cache_dir in (cache_root / "live-tile-cache", cache_root / "offline-snapshot"): cache_dir.mkdir(parents=True, exist_ok=True) os.chown(cache_dir, 1000, 1000) os.chmod(cache_dir, 0o750) ensure_map_gateway_admin_secret() sync_map_egress_proxy_secret() if touches_external_data_plane: ensure_external_data_plane_provisioner_secret() ensure_engine_edp_managed_provisioner_keypair() ensure_foundry_edp_managed_provisioner_keypair() ensure_root_owned_grant_directory( EXTERNAL_DATA_PLANE_READER_GRANTS_DIR, "external data plane reader grants", ) return if component in ("dc-cms", "dc-cms-site-nodedc"): (Path("/volume1/docker/dc-cms/sites") / "nodedc").mkdir(parents=True, exist_ok=True) return if component != "bim-viewer": return data_root = component_root(component) / "server" / "data" for rel in ("uploads", "projects", "shares", "models", "comments"): (data_root / rel).mkdir(parents=True, exist_ok=True) def run_component_runtime(component, entries, services): if component == "platform" and is_platform_provider_catalog_only(entries): return if component_artifact_only(component): return run_build(component, entries) prepare_component_runtime(component, entries) if is_engine_node_intelligence_transition(component, entries): run_engine_node_intelligence_compose(services, entries) else: run_compose(component, services, entries) def healthcheck_url(check): headers = {} expected_json = {} if isinstance(check, dict): url = check.get("url") headers = check.get("headers") or {} expected_json = check.get("expected_json") or {} else: url = check last_error = None for _attempt in range(1, 61): try: request = urllib.request.Request(url, headers=headers) with NO_REDIRECT_OPENER.open(request, timeout=10) as response: if 200 <= response.status < 400: if expected_json: raw = response.read(64 * 1024 + 1) if len(raw) > 64 * 1024: last_error = "health response too large" continue try: payload = json.loads(raw.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError): last_error = "health response is not json" continue if not isinstance(payload, dict) or any( payload.get(key) != expected for key, expected in expected_json.items() ): last_error = "health response contract mismatch" continue return last_error = f"HTTP {response.status}" except urllib.error.HTTPError as exc: if 200 <= exc.code < 400 and not expected_json: return last_error = f"HTTP {exc.code}" except Exception as exc: last_error = str(exc) time.sleep(5) die(f"healthcheck failed for {url}: {last_error}") def external_data_plane_healthcheck(require_managed=True): expected_json = { "ok": True, "service": "nodedc-external-data-plane", "database": "ready", } if require_managed: expected_json["managedWriterBindingProvisioning"] = "enabled" server_source = ( component_root("platform") / "platform/services/external-data-plane/src/server.mjs" ) try: server_text = server_source.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): server_text = "" if 'managedReaderBindingProvisioning: config.managedProvisionerApiEnabled' in server_text: expected_json["managedReaderBindingProvisioning"] = "enabled" if 'managedWriterBindingLifetime: config.managedProvisionerApiEnabled ? "explicit-revoke" : "disabled"' in server_text: expected_json["managedWriterBindingLifetime"] = "explicit-revoke" if 'managedReaderBindingLifetime: config.managedProvisionerApiEnabled ? "explicit-revoke" : "disabled"' in server_text: expected_json["managedReaderBindingLifetime"] = "explicit-revoke" if 'foundryReaderBindingProvisioning: config.foundryProvisionerApiEnabled ? "digest+server-resolved-source" : "disabled"' in server_text: expected_json["foundryReaderBindingProvisioning"] = "digest+server-resolved-source" expected_json["foundryReaderBindingLifetime"] = "explicit-revoke" return { "url": "http://127.0.0.1:18106/healthz", "expected_json": expected_json, } def module_foundry_healthcheck(): expected_json = { "status": "ok", "service": "nodedc-module-foundry", } server_source = component_root("module-foundry") / "server/catalog-server.mjs" try: server_text = server_source.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): server_text = "" if "dataProductConsumerProvisioner" in server_text: expected_json["dataProductConsumerProvisioner"] = { "configured": True, "auth": "dedicated-ed25519-service-identity", "sourceScope": "external-data-plane-resolved", } return { "url": "http://172.22.0.222:9920/healthz", "expected_json": expected_json, } def component_healthchecks(component, entries=None, services=None): if is_engine_n8n_transition(component, entries): # The transition does not restart the Engine UI/backend generation. # Its own acceptance below verifies n8n readiness, image, mount, logs, # live node export and the pinned Engine MCP catalogs. return () if is_engine_node_intelligence_transition(component, entries): # The exact sidecar/backend topology and live MCP capability calls are # accepted separately below. Keep the generic HTTP barrier scoped to # the backend generation recreated by this transition. return ("http://127.0.0.1:3001/health",) if component == "platform" and entries is not None and is_platform_provider_catalog_only(entries): return () if component == "module-foundry": return (module_foundry_healthcheck(),) if ( ( is_engine_data_product_publish_grant_slice(component, entries) or is_engine_composite_provider_v4_slice(component, entries) or is_engine_provider_rotating_slot_slice(component, entries) or is_engine_provider_authority_diagnostics_slice(component, entries) or is_engine_depttrans_zone_authority_v1_slice(component, entries) or is_engine_provider_target_host_policy_slice(component, entries) or is_engine_agent_full_grant_migration_slice(component, entries) or is_engine_mcp_control_plane_slice(component, entries) or is_engine_mcp_ontology_sdk_slice(component, entries) or is_engine_mcp_autonomy_provider_v5_slice(component, entries) or is_engine_provider_security_catalog_slice(component, entries) ) and services is not None ): selected_services = set(services) checks = [] if "app" in selected_services: checks.append("http://127.0.0.1:8080/") if "nodedc-backend" in selected_services: checks.append("http://127.0.0.1:3001/health") if is_engine_agent_full_grant_migration_slice(component, entries): checks.append("http://127.0.0.1:3001/internal/engine-credential-sink/v1/health") return tuple(checks) if component == "platform" and entries is not None: selected_services = tuple(services) if services is not None else component_services(component, entries) # A narrow EDP application deploy must prove only the service it # recreated. Unrelated Platform routes retain independent lifecycle and # must not turn a valid EDP deploy or rollback into a false failure. if selected_services == (EXTERNAL_DATA_PLANE_SERVICE,): return (external_data_plane_healthcheck(),) checks = list(COMPONENTS[component].get("healthchecks", ())) if touches_engine_credential_sink(component, entries): checks.append("http://127.0.0.1:3001/internal/engine-credential-sink/v1/health") if component == "platform" and entries is not None: touches_compose = any(rel == "platform/docker-compose.platform-http.yml" for rel in entries) touches_ai_workspace_assistant = any(rel == "platform/ai-workspace-assistant" or rel.startswith("platform/ai-workspace-assistant/") for rel in entries) touches_ontology = any(rel == "platform/ontology-core" or rel.startswith("platform/ontology-core/") for rel in entries) touches_gelios = any(rel == "platform/gelios-gateway" or rel.startswith("platform/gelios-gateway/") for rel in entries) touches_map_gateway = any(rel == "platform/services/map-gateway" or rel.startswith("platform/services/map-gateway/") for rel in entries) touches_external_data_plane = touches_external_data_plane_files(entries) map_gateway_only_compose = touches_compose and touches_map_gateway and not any((touches_ai_workspace_assistant, touches_ontology, touches_gelios, touches_external_data_plane)) healthcheck_all_for_compose = touches_compose and not map_gateway_only_compose if healthcheck_all_for_compose or touches_ai_workspace_assistant: checks.append("http://127.0.0.1:18082/healthz") if healthcheck_all_for_compose or touches_ontology: checks.append("http://127.0.0.1:18104/healthz") if healthcheck_all_for_compose or touches_gelios: checks.append("http://127.0.0.1:18105/healthz") if touches_map_gateway: checks.append({"url": "http://127.0.0.1:18103/healthz", "headers": {"x-nodedc-user-id": "deploy-healthcheck"}}) if touches_external_data_plane: checks.append(external_data_plane_healthcheck()) return tuple(checks) def healthcheck_container(container_name): last_status = "unknown" for _attempt in range(1, 61): result = subprocess.run( [str(DOCKER), "inspect", "--format", "{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}", container_name], check=False, capture_output=True, text=True, ) status_text = result.stdout.strip().lower() last_status = status_text or result.stderr.strip() or f"inspect-exit-{result.returncode}" if status_text in ("healthy", "running"): return if status_text in ("unhealthy", "exited", "dead"): break time.sleep(5) die(f"container healthcheck failed for {container_name}: {last_status}") def compose_service_container_id(component, service): result = subprocess.run( [*compose_base_cmd(component), "ps", "-q", service], cwd=str(component_compose_root(component)), check=False, capture_output=True, text=True, ) container_ids = [line.strip() for line in result.stdout.splitlines() if line.strip()] if ( result.returncode != 0 or len(container_ids) != 1 or not re.fullmatch(r"[a-f0-9]{12,64}", container_ids[0]) ): die(f"Compose service container lookup failed: {service}") return container_ids[0] def healthcheck_compose_service(component, service): healthcheck_container(compose_service_container_id(component, service)) def container_environment(container, label): environment = {} for raw in (container.get("Config") or {}).get("Env") or []: if not isinstance(raw, str) or "=" not in raw: die(f"{label} environment inventory is invalid") key, value = raw.split("=", 1) if not key or key in environment: die(f"{label} environment inventory is invalid") environment[key] = value return environment def validate_engine_node_intelligence_secret_mount(container, label): matches = [ mount for mount in (container.get("Mounts") or []) if isinstance(mount, dict) and mount.get("Destination") == ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH ] if len(matches) != 1: die(f"{label} secret mount inventory mismatch") mount = matches[0] if ( mount.get("Type") != "bind" or mount.get("Source") != str(ENGINE_NODE_INTELLIGENCE_SECRET_FILE) or mount.get("RW") is not False ): die(f"{label} secret mount boundary mismatch") def assert_engine_node_intelligence_container_absent(): result = subprocess.run( [ str(DOCKER), "container", "ls", "--all", "--filter", f"label=com.docker.compose.service={ENGINE_NODE_INTELLIGENCE_SERVICE}", "--format", "{{.ID}}", ], check=False, capture_output=True, text=True, ) if result.returncode != 0 or result.stdout.strip(): die("Engine node-intelligence inactive runtime still has a sidecar container") def engine_node_intelligence_live_probe_script(): return """ const {createNodeIntelligenceMcpClient}=await import('file:///app/server/nodeIntelligence/upstreamMcpClient.js'); const client=createNodeIntelligenceMcpClient(); const status=await client.status(); if(!status.ok||status.status!=='ready'||status.authSource!=='file'||status.server?.version!=='2.33.2'||!Object.values(status.capabilities||{}).every(Boolean))throw new Error('status'); const guidance=await client.callTool('get_node',{nodeType:'nodes-base.httpRequest',detail:'minimal'}); if(!guidance||typeof guidance!=='object')throw new Error('guidance'); const nodeValidation=await client.callTool('validate_node',{nodeType:'nodes-base.httpRequest',config:{url:'https://example.com'},mode:'minimal',profile:'ai-friendly'}); if(!nodeValidation||typeof nodeValidation!=='object'||nodeValidation.valid!==true)throw new Error('node-validation'); const workflowValidation=await client.callTool('validate_workflow',{workflow:{name:'NDC acceptance',nodes:[{id:'probe-1',name:'Start',type:'nodes-base.manualTrigger',typeVersion:1,position:[0,0],parameters:{}},{id:'probe-2',name:'Output',type:'nodes-base.set',typeVersion:3.4,position:[240,0],parameters:{}}],connections:{Start:{main:[[{node:'Output',type:'main',index:0}]]}},settings:{}},options:{validateNodes:true,validateConnections:true,validateExpressions:true,profile:'runtime'}}); if(!workflowValidation||typeof workflowValidation!=='object'||workflowValidation.valid!==true)throw new Error('workflow-validation'); process.stdout.write('node-intelligence-live:2.33.2:guidance+node+workflow'); """.strip() def accept_engine_node_intelligence_runtime(descriptor): if descriptor is None or descriptor.get("action") != "activate": die("Engine node-intelligence activation descriptor is required") validate_installed_engine_node_intelligence_source(descriptor) ensure_engine_node_intelligence_secret() image = inspect_engine_node_intelligence_image(descriptor) sidecar_id = compose_service_container_id( "engine", ENGINE_NODE_INTELLIGENCE_SERVICE, ) backend_id = compose_service_container_id("engine", "nodedc-backend") healthcheck_container(sidecar_id) healthcheck_container(backend_id) sidecar = inspect_container(sidecar_id) backend = inspect_container(backend_id) sidecar_config = sidecar.get("Config") or {} sidecar_host = sidecar.get("HostConfig") or {} sidecar_environment = container_environment( sidecar, "Engine node-intelligence sidecar", ) if ( sidecar.get("Image") != image["Id"] or sidecar_config.get("Image") != ENGINE_NODE_INTELLIGENCE_IMAGE or sidecar_config.get("User") != f"{ENGINE_NODE_INTELLIGENCE_RUNTIME_UID}:{ENGINE_NODE_INTELLIGENCE_RUNTIME_GID}" or sidecar_host.get("ReadonlyRootfs") is not True or sidecar_host.get("PortBindings") not in (None, {}) or set(sidecar_host.get("CapDrop") or []) != {"ALL"} or "no-new-privileges:true" not in (sidecar_host.get("SecurityOpt") or []) or sidecar_environment.get("AUTH_TOKEN_FILE") != ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH or sidecar_environment.get("NODE_DB_PATH") != "/app/data/nodes.db" or sidecar_environment.get("REBUILD_ON_START") != "false" or sidecar_environment.get("N8N_MCP_TELEMETRY_DISABLED") != "true" ): die("Engine node-intelligence sidecar runtime contract mismatch") if any( key in sidecar_environment for key in ("AUTH_TOKEN", "N8N_API_URL", "N8N_API_KEY") ): die("Engine node-intelligence sidecar runtime authority leak") validate_engine_node_intelligence_secret_mount( sidecar, "Engine node-intelligence sidecar", ) backend_environment = container_environment(backend, "Engine backend") if ( backend_environment.get("ENGINE_NODE_INTELLIGENCE_MCP_URL") != f"http://{ENGINE_NODE_INTELLIGENCE_SERVICE}:3000/mcp" or backend_environment.get("ENGINE_NODE_INTELLIGENCE_AUTH_TOKEN_FILE") != ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH or backend_environment.get("ENGINE_NODE_INTELLIGENCE_ASSET_ID") != f"nodedc-node-intelligence@{ENGINE_NODE_INTELLIGENCE_RELEASE_ID.replace('-', '+', 1)}" or "ENGINE_NODE_INTELLIGENCE_AUTH_TOKEN" in backend_environment ): die("Engine backend node-intelligence boundary mismatch") validate_engine_node_intelligence_secret_mount( backend, "Engine backend node-intelligence", ) result = run_engine_backend_probe( ( "node", "--input-type=module", "-e", engine_node_intelligence_live_probe_script(), ), "node-intelligence live capability", container_id=backend_id, ) expected = "node-intelligence-live:2.33.2:guidance+node+workflow" if result != expected: die("Engine node-intelligence live capability acceptance mismatch") backend_runtime = preflight_engine_credential_backend_runtime() if backend_runtime["mode"] != "verified-derived-retry": die("Engine node-intelligence backend immutable runtime acceptance failed") return result def accept_engine_agent_full_grant_migration_runtime(): expected_scopes = ( "engine:l2:context:read", "engine:l2:graph:read", "engine:l2:node-schema:read", "engine:l2:graph:plan", "engine:l2:graph:write", "engine:l2:data-product-publish-grant:plan", "engine:l2:data-product-publish-grant:write", "engine:l2:validate", "engine:l2:runtime:read", "engine:l2:deploy-run", "engine:l2:execution:stop", ) script = """ import crypto from 'node:crypto'; import fs from 'node:fs'; const file='/app/server/engineAgents/store.js'; const digest=crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); const module=await import('file:///app/server/engineAgents/store.js'); const expected=JSON.parse(process.argv[1]); if(digest!==process.argv[2]||JSON.stringify(module.ENGINE_AGENT_SCOPES)!==JSON.stringify(expected)||module.ENGINE_AGENT_FULL_DEVELOPER_PROFILE!=='full-developer')process.exit(2); process.stdout.write('store-v2-full-developer:'+digest); """.strip() result = run_engine_backend_probe( ( "node", "--input-type=module", "-e", script, json.dumps(expected_scopes, separators=(",", ":")), ENGINE_AGENT_FULL_GRANT_MIGRATION_TARGET_SHA256, ), "agent full grant migration", container_id=engine_backend_container_id(), ) expected = f"store-v2-full-developer:{ENGINE_AGENT_FULL_GRANT_MIGRATION_TARGET_SHA256}" if result != expected: die("Engine agent full grant migration runtime acceptance mismatch") return result def run_healthchecks(component, entries=None, services=None): if component == "platform" and entries is not None and is_platform_provider_catalog_only(entries): return if is_engine_n8n_transition(component, entries): descriptor = current_engine_n8n_transition_descriptor() if descriptor is None: die("installed Engine n8n transition descriptor is missing during acceptance") accept_engine_n8n_runtime(descriptor) return if is_engine_node_intelligence_transition(component, entries): descriptor = current_engine_node_intelligence_descriptor() if descriptor is None: die( "installed Engine node-intelligence transition descriptor " "is missing during acceptance" ) if descriptor["action"] == "activate": accept_engine_node_intelligence_runtime(descriptor) healthcheck_url("http://127.0.0.1:3001/health") return healthcheck_compose_service("engine", "nodedc-backend") healthcheck_url("http://127.0.0.1:3001/health") backend = preflight_engine_credential_backend_runtime() if backend["mode"] != "verified-derived-retry": die("Engine node-intelligence rollback backend acceptance failed") assert_engine_node_intelligence_container_absent() return touches_publish_grant = is_engine_data_product_publish_grant_slice( component, entries, ) touches_composite_provider_v4 = is_engine_composite_provider_v4_slice( component, entries, ) touches_provider_rotating_slot = is_engine_provider_rotating_slot_slice( component, entries, ) touches_provider_authority_diagnostics = is_engine_provider_authority_diagnostics_slice( component, entries, ) touches_depttrans_zone_authority_v1 = is_engine_depttrans_zone_authority_v1_slice( component, entries, ) touches_provider_target_host_policy = is_engine_provider_target_host_policy_slice( component, entries, ) touches_agent_grant_migration = is_engine_agent_full_grant_migration_slice( component, entries, ) touches_mcp_control_plane = is_engine_mcp_control_plane_slice(component, entries) touches_mcp_ontology_sdk = is_engine_mcp_ontology_sdk_slice(component, entries) touches_mcp_autonomy_provider_v5 = is_engine_mcp_autonomy_provider_v5_slice(component, entries) touches_provider_catalog = is_engine_provider_security_catalog_slice(component, entries) if ( touches_engine_credential_sink(component, entries) or touches_publish_grant or touches_composite_provider_v4 or touches_provider_rotating_slot or touches_provider_authority_diagnostics or touches_depttrans_zone_authority_v1 or touches_provider_target_host_policy or touches_agent_grant_migration or touches_mcp_control_plane or touches_mcp_ontology_sdk or touches_mcp_autonomy_provider_v5 or touches_provider_catalog ): # The HTTP endpoint can become ready before Docker publishes the first # successful health probe. Wait for the Compose health barrier before # the strict immutable-runtime preflight, both on apply and retry. healthcheck_compose_service("engine", "nodedc-backend") for url in component_healthchecks(component, entries, services): healthcheck_url(url) if ( touches_engine_credential_sink(component, entries) or touches_publish_grant or touches_composite_provider_v4 or touches_provider_rotating_slot or touches_provider_authority_diagnostics or touches_depttrans_zone_authority_v1 or touches_provider_target_host_policy or touches_agent_grant_migration or touches_mcp_control_plane or touches_mcp_ontology_sdk or touches_mcp_autonomy_provider_v5 or touches_provider_catalog ): runtime = preflight_engine_credential_backend_runtime() if runtime["mode"] != "verified-derived-retry": die("Engine backend immutable runtime activation acceptance failed") if touches_agent_grant_migration: accept_engine_agent_full_grant_migration_runtime() if touches_mcp_control_plane: accept_engine_mcp_control_plane_runtime() if touches_mcp_ontology_sdk: accept_engine_mcp_ontology_sdk_runtime() if touches_mcp_autonomy_provider_v5: state = engine_mcp_autonomy_provider_v5_installed_state() if state == "target": accept_engine_mcp_autonomy_provider_v5_runtime() if touches_provider_catalog: accept_engine_provider_security_catalog_runtime() if touches_publish_grant: accept_engine_composite_provider_runtime() if touches_composite_provider_v4: root = component_root("engine") installed_sha256 = { rel: sha256_file(root / rel) for rel in ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES } if installed_sha256 == ENGINE_COMPOSITE_PROVIDER_V4_TARGET_SHA256: if accept_engine_composite_provider_runtime() is None: die("Engine composite provider v4 live acceptance was not executed") elif installed_sha256 != ENGINE_COMPOSITE_PROVIDER_V4_PREDECESSOR_SHA256: die("Engine composite provider v4 installed source state is neither target nor rollback predecessor") if touches_provider_rotating_slot: root = component_root("engine") installed_sha256 = { rel: sha256_file(root / rel) for rel in ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES } if installed_sha256 == ENGINE_PROVIDER_ROTATING_SLOT_TARGET_SHA256: accept_engine_provider_rotating_slot_runtime() elif installed_sha256 != ENGINE_PROVIDER_ROTATING_SLOT_PREDECESSOR_SHA256: die("Engine provider rotating slot installed state is neither target nor rollback predecessor") if touches_provider_authority_diagnostics: root = component_root("engine") installed_sha256 = { rel: sha256_file(root / rel) for rel in ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES } if installed_sha256 == ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256: accept_engine_provider_authority_diagnostics_runtime() elif installed_sha256 != ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_PREDECESSOR_SHA256: die("Engine provider authority diagnostics installed state is neither target nor rollback predecessor") if touches_depttrans_zone_authority_v1: root = component_root("engine") target_state = all( (root / rel).is_file() and not (root / rel).is_symlink() and sha256_file(root / rel) == expected for rel, expected in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_TARGET_SHA256.items() ) predecessor_state = all( (root / rel).is_file() and not (root / rel).is_symlink() and sha256_file(root / rel) == expected for rel, expected in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_PREDECESSOR_SHA256.items() ) and all( not (root / rel).exists() and not (root / rel).is_symlink() for rel in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_NEW_PATHS ) if target_state: accept_engine_depttrans_zone_authority_v1_runtime() elif not predecessor_state: die("Engine Depttrans zone authority v1 installed state is neither target nor rollback predecessor") if touches_provider_target_host_policy: root = component_root("engine") installed_sha256 = { rel: sha256_file(root / rel) for rel in ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES } if installed_sha256 == ENGINE_PROVIDER_TARGET_HOST_POLICY_TARGET_SHA256: accept_engine_provider_target_host_policy_runtime() elif installed_sha256 != ENGINE_PROVIDER_TARGET_HOST_POLICY_PREDECESSOR_SHA256: die("Engine provider target host policy installed state is neither target nor rollback predecessor") container_name = COMPONENTS[component].get("health_container") if container_name: healthcheck_container(container_name) def move_artifact(src, target_dir, suffix=""): target_dir.mkdir(parents=True, exist_ok=True) name = src.name + suffix dst = target_dir / name if dst.exists(): dst = target_dir / f"{src.name}.{stamp()}" src.rename(dst) return dst def apply_artifact(artifact): require_root() validate_artifact_location(artifact) ensure_layout() sha = sha256_file(artifact) backup_id = None backup_dir = None manifest = None entries = None component = None root = None services = None transition_descriptor = None node_intelligence_descriptor = None node_intelligence_service_stopped = False apply_started = False engine_backend_recreated = False engine_backend_initial_mode = None runtime_started = False current_stamp = stamp() with DeployLock(): try: with tempfile.TemporaryDirectory(prefix=f"apply-{current_stamp}-", dir=TMP_DIR) as tmp: work = Path(tmp) manifest, entries, payload_dir = load_artifact(artifact, work) patch_id = manifest["id"] component = manifest["component"] root = component_root(component) compose_root = component_compose_root(component) bootstrap_root = bool(COMPONENTS[component].get("bootstrap_root")) services = component_services(component, entries) artifact_only = component_artifact_only(component) if state_has_sha(sha): die(f"artifact sha already applied: {sha}") if state_has_patch_id(patch_id): die(f"patch id already applied: {patch_id}") if not root.is_dir(): if bootstrap_root: root.mkdir(parents=True, exist_ok=True) else: die(f"component payload root not found: {root}") if is_engine_data_product_publish_grant_slice(component, entries): preflight_engine_data_product_publish_grant_predecessor(payload_dir) publish_backend_preflight = preflight_engine_credential_backend_runtime() if publish_backend_preflight["mode"] != "verified-derived-retry": die("Engine data product publish grant requires the active immutable credential backend") if is_engine_composite_provider_v4_slice(component, entries): preflight_engine_composite_provider_v4_predecessor() composite_backend_preflight = preflight_engine_credential_backend_runtime() if composite_backend_preflight["mode"] != "verified-derived-retry": die("Engine composite provider v4 requires the active immutable credential backend") if is_engine_provider_rotating_slot_slice(component, entries): preflight_engine_provider_rotating_slot_predecessor() rotating_slot_backend_preflight = preflight_engine_credential_backend_runtime() if rotating_slot_backend_preflight["mode"] != "verified-derived-retry": die("Engine provider rotating slot requires the active immutable credential backend") if is_engine_provider_authority_diagnostics_slice(component, entries): preflight_engine_provider_authority_diagnostics_predecessor() authority_diagnostics_backend_preflight = preflight_engine_credential_backend_runtime() if authority_diagnostics_backend_preflight["mode"] != "verified-derived-retry": die("Engine provider authority diagnostics requires the active immutable credential backend") if is_engine_depttrans_zone_authority_v1_slice(component, entries): preflight_engine_depttrans_zone_authority_v1_predecessor() depttrans_authority_backend_preflight = preflight_engine_credential_backend_runtime() if depttrans_authority_backend_preflight["mode"] != "verified-derived-retry": die("Engine Depttrans zone authority v1 requires the active immutable credential backend") if is_engine_provider_target_host_policy_slice(component, entries): preflight_engine_provider_target_host_policy_predecessor() target_host_policy_backend_preflight = preflight_engine_credential_backend_runtime() if target_host_policy_backend_preflight["mode"] != "verified-derived-retry": die("Engine provider target host policy requires the active immutable credential backend") if is_engine_agent_full_grant_migration_slice(component, entries): preflight_engine_agent_full_grant_migration_predecessor() migration_backend_preflight = preflight_engine_credential_backend_runtime() if migration_backend_preflight["mode"] != "verified-derived-retry": die("Engine agent full grant migration requires the active immutable credential backend") if is_engine_node_intelligence_transition(component, entries): node_intelligence_preflight = preflight_engine_node_intelligence_predecessor( payload_dir ) node_intelligence_descriptor = node_intelligence_preflight["descriptor"] if is_engine_mcp_control_plane_slice(component, entries): preflight_engine_mcp_control_plane_predecessor(payload_dir) if is_engine_mcp_ontology_sdk_slice(component, entries): preflight_engine_mcp_ontology_sdk_predecessor(payload_dir) if is_engine_mcp_autonomy_provider_v5_slice(component, entries): preflight_engine_mcp_autonomy_provider_v5_predecessor(payload_dir) if is_engine_provider_security_catalog_slice(component, entries): preflight_engine_provider_security_catalog_predecessor() if not artifact_only and not compose_root.is_dir(): if bootstrap_root and is_relative_to(compose_root.resolve(strict=False), root.resolve(strict=False)): compose_root.mkdir(parents=True, exist_ok=True) else: die(f"component compose root not found: {compose_root}") compose_files = component_compose_files(component) if not artifact_only and compose_files: for compose_file in compose_files: if not compose_file.is_file(): compose_entry = None if is_relative_to(compose_file.resolve(strict=False), root.resolve(strict=False)): compose_entry = compose_file.resolve(strict=False).relative_to(root.resolve(strict=False)).as_posix() if compose_entry not in entries: die(f"compose file not found: {compose_file}") elif not artifact_only and not (compose_root / "docker-compose.yml").is_file(): # A bootstrap component begins with no payload root. Its # compose file may therefore be supplied by the same # reviewed overlay that is about to create that root. compose_entry = None if is_relative_to(compose_root.resolve(strict=False), root.resolve(strict=False)): compose_entry = compose_root.resolve(strict=False).relative_to(root.resolve(strict=False)).joinpath("docker-compose.yml").as_posix() if not (bootstrap_root and compose_entry in entries): die(f"docker-compose.yml not found in component compose root: {compose_root}") compose_env_file = component_compose_env_file(component) if not artifact_only and compose_env_file and not compose_env_file.is_file(): die(f"compose env file not found: {compose_env_file}") if not artifact_only and not DOCKER.is_file(): die(f"docker not found: {DOCKER}") if is_engine_n8n_transition(component, entries): transition_descriptor = read_engine_n8n_transition_descriptor( payload_dir / ENGINE_N8N_TRANSITION_DESCRIPTOR_REL ) preflight_engine_n8n_transition( transition_descriptor, enforce_expected_current=True, ) if touches_engine_credential_sink(component, entries): engine_backend_initial_mode = preflight_engine_credential_backend_runtime()["mode"] if COMPONENTS[component].get("immutable_payload"): for rel in entries: destination = root / rel if destination.exists() or destination.is_symlink(): die(f"immutable release already exists: {rel}") backup_id = safe_name(f"{component}-{patch_id}-{current_stamp}") backup_dir = BACKUPS_DIR / backup_id backup_dir.mkdir(parents=True, exist_ok=False) (backup_dir / "manifest.env").write_text((work / "manifest.env").read_text(encoding="utf-8"), encoding="utf-8") (backup_dir / "files.txt").write_text((work / "files.txt").read_text(encoding="utf-8"), encoding="utf-8") include_nginx_html = component == "engine" and component_publish_dist(component, entries) create_backup(root, backup_dir, entries, include_nginx_html) if component == "platform" and touches_external_data_plane_files(entries): validate_backup_partition( entries, read_backup_path_list(backup_dir / "existing-files.txt"), read_backup_path_list(backup_dir / "missing-files.txt"), "Platform pre-apply", ) apply_started = True if ( node_intelligence_descriptor is not None and node_intelligence_descriptor["action"] == "rollback-inactive" ): stop_and_remove_compose_services( "engine", (ENGINE_NODE_INTELLIGENCE_SERVICE,), ) node_intelligence_service_stopped = True for rel in entries: copy_payload_path(payload_dir, root, rel, current_stamp) if component == "n8n-private-extension": for rel in entries: seal_n8n_private_extension_release(root, rel) if component_publish_dist(component, entries): publish_engine_dist(root, current_stamp) if touches_engine_credential_sink(component, entries): run_build(component, entries) prepare_component_runtime(component, entries) # From this point the exact immutable override/image are # verified, so even an ambiguous partial Compose failure # can be safely reconciled by recreating on that runtime. engine_backend_recreated = True run_compose(component, services, entries) else: # Mark the generic runtime before Compose so a partial # candidate start is always eligible for its domain rollback. runtime_started = bool(services) run_component_runtime(component, entries, services) run_healthchecks(component, entries, services) applied_path = move_artifact(artifact, APPLIED_DIR) append_jsonl(STATE_FILE, { "applied_at": utc_now(), "artifact": applied_path.name, "backup_id": backup_id, "component": component, "id": patch_id, "sha256": sha, "status": "ok", }) print(f"deploy-ok patch={patch_id} component={component} backup={backup_id}") except Exception as exc: rollback_status = "not-required" if ( apply_started and backup_dir is not None and root is not None ): if transition_descriptor is not None and is_engine_n8n_transition(component, entries): try: restored_action = restore_engine_n8n_transition( root, backup_dir, entries, transition_descriptor, current_stamp, ) rollback_status = f"ok:{restored_action}" print(f"engine-n8n-automatic-rollback={rollback_status}", file=sys.stderr) except Exception as rollback_exc: rollback_status = f"failed:{type(rollback_exc).__name__}" print("engine-n8n-automatic-rollback=failed", file=sys.stderr) elif ( node_intelligence_descriptor is not None and is_engine_node_intelligence_transition(component, entries) ): try: restored_action = rollback_engine_node_intelligence( root, backup_dir, entries, current_stamp, runtime_started=runtime_started, node_intelligence_service_stopped=node_intelligence_service_stopped, ) rollback_status = f"ok:{restored_action}" print( f"engine-node-intelligence-automatic-rollback={rollback_status}", file=sys.stderr, ) except Exception as rollback_exc: rollback_status = f"failed:{type(rollback_exc).__name__}" print( "engine-node-intelligence-automatic-rollback=failed", file=sys.stderr, ) elif touches_engine_credential_sink(component, entries): try: restored_action = rollback_engine_credential_bridge( component, root, backup_dir, entries, current_stamp, engine_backend_recreated=engine_backend_recreated, engine_backend_initial_mode=engine_backend_initial_mode, ) rollback_status = f"ok:{restored_action}" print(f"engine-credential-automatic-rollback={rollback_status}", file=sys.stderr) except ReconciliationRequired: rollback_status = "deferred:reconciliation-required" print("engine-credential-automatic-rollback=deferred:reconciliation-required", file=sys.stderr) except Exception as rollback_exc: rollback_status = f"failed:{type(rollback_exc).__name__}" print("engine-credential-automatic-rollback=failed", file=sys.stderr) elif ( ( is_engine_data_product_publish_grant_slice(component, entries) or is_engine_composite_provider_v4_slice(component, entries) or is_engine_provider_rotating_slot_slice(component, entries) or is_engine_provider_authority_diagnostics_slice(component, entries) or is_engine_depttrans_zone_authority_v1_slice(component, entries) or is_engine_provider_target_host_policy_slice(component, entries) or is_engine_agent_full_grant_migration_slice(component, entries) or is_engine_mcp_control_plane_slice(component, entries) or is_engine_mcp_ontology_sdk_slice(component, entries) or is_engine_mcp_autonomy_provider_v5_slice(component, entries) or is_engine_provider_security_catalog_slice(component, entries) ) and services is not None ): try: restored_state = rollback_engine_apply( root, backup_dir, entries, current_stamp, runtime_started, services, ) rollback_status = f"ok:engine-overlay:{restored_state}" print(f"engine-automatic-rollback={rollback_status}", file=sys.stderr) except Exception as rollback_exc: rollback_status = f"failed:{type(rollback_exc).__name__}" print("engine-automatic-rollback=failed", file=sys.stderr) elif ( component == "platform" and entries is not None and services is not None and touches_external_data_plane_files(entries) ): try: restored_state = rollback_platform_apply( root, backup_dir, entries, current_stamp, runtime_started, services, ) rollback_status = f"ok:platform-overlay:{restored_state}" print(f"platform-automatic-rollback={rollback_status}", file=sys.stderr) except Exception as rollback_exc: rollback_status = f"failed:{type(rollback_exc).__name__}" print("platform-automatic-rollback=failed", file=sys.stderr) failed_path = None if artifact.exists() and rollback_status != "deferred:reconciliation-required": try: failed_path = move_artifact(artifact, FAILED_DIR, suffix=f".{current_stamp}") except Exception: failed_path = None append_jsonl(FAILED_STATE_FILE, { "artifact": str(failed_path.name if failed_path else artifact.name), "backup_id": backup_id, "component": manifest.get("component") if manifest else None, "failed_at": utc_now(), "id": manifest.get("id") if manifest else None, "message": str(exc), "rollback_status": rollback_status, "sha256": sha, "started_apply": apply_started, "status": ( "reconciliation-required" if rollback_status == "deferred:reconciliation-required" else "failed" ), }) if backup_id: print(f"backup={backup_id}", file=sys.stderr) raise def main(argv): if sys.version_info < (3, 8): die("Python 3.8 or newer is required") parser = argparse.ArgumentParser(prog="nodedc-deploy") sub = parser.add_subparsers(dest="cmd", required=True) plan = sub.add_parser("plan") plan.add_argument("artifact") apply = sub.add_parser("apply") apply.add_argument("artifact") sub.add_parser("verify-install") args = parser.parse_args(argv) if args.cmd == "verify-install": verify_install() return 0 artifact = Path(args.artifact).absolute() if args.cmd == "plan": plan_artifact(artifact) return 0 if args.cmd == "apply": apply_artifact(artifact) return 0 parser.print_help() return 2 if __name__ == "__main__": try: raise SystemExit(main(sys.argv[1:])) except DeployError as exc: print(f"ERROR: {exc}", file=sys.stderr) raise SystemExit(1)