From fdda1535956cabef993ea627a955eeba09dc135a Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 22 Jul 2026 13:06:32 +0300 Subject: [PATCH] feat(deploy): seal Gelios telemetry authority transition --- ...ine-provider-security-catalog-artifact.mjs | 47 ++++-------- infra/deploy-runner/nodedc-deploy | 73 ++++++++++++++----- ...test_engine_depttrans_zone_authority_v1.py | 7 ++ .../test_engine_provider_security_catalog.py | 14 +++- 4 files changed, 85 insertions(+), 56 deletions(-) diff --git a/infra/deploy-runner/build-engine-provider-security-catalog-artifact.mjs b/infra/deploy-runner/build-engine-provider-security-catalog-artifact.mjs index 92505ac..68c6c44 100644 --- a/infra/deploy-runner/build-engine-provider-security-catalog-artifact.mjs +++ b/infra/deploy-runner/build-engine-provider-security-catalog-artifact.mjs @@ -1,17 +1,20 @@ #!/usr/bin/env node import { createHash } from "node:crypto"; import { spawnSync } from "node:child_process"; -import { lstat, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { cp, lstat, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const here = dirname(fileURLToPath(import.meta.url)); const platformRoot = resolve(here, "../.."); +const engineRoot = resolve( + process.env.NODEDC_ENGINE_SOURCE_ROOT || resolve(platformRoot, "../NODEDC_ENGINE_INFRA"), +); const artifactRoot = resolve( process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"), ); -const [transitionId = "20260719-006", ...extra] = process.argv.slice(2); +const [transitionId = "20260722-020", ...extra] = process.argv.slice(2); if (extra.length || !/^\d{8}-[0-9]{3}$/.test(transitionId)) { throw new Error("usage: build-engine-provider-security-catalog-artifact.mjs [YYYYMMDD-NNN]"); } @@ -19,33 +22,9 @@ if (extra.length || !/^\d{8}-[0-9]{3}$/.test(transitionId)) { const id = `engine-provider-security-catalog-${transitionId}`; const target = join(artifactRoot, `nodedc-${id}.tgz`); const file = "nodedc-source/server/assets/provider-packages/v1/catalog.json"; -const expectedSha256 = "992159fc457ec76ce1f45aad337604c8a72b29252d44fbccda515f0fd6ea6428"; -const catalogBytes = Buffer.from(`${JSON.stringify({ - schemaVersion: "nodedc.engine.provider-security-catalog/v1", - packages: [{ - id: "gelios.provider.v3", - version: "3.0.0", - providerId: "gelios", - providerCredential: { - authModeId: "gelios.rest-rotating-bearer.v3", - credentialType: "httpBearerAuth", - }, - capabilities: [{ - id: "gelios.units.current.read", - classification: "read", - status: "implemented", - request: { - method: "GET", - url: "https://api.geliospro.com/api/v1/units", - }, - dataProductIds: ["fleet.positions.current.v2"], - }], - publisher: { - nodeType: "n8n-nodes-ndc.ndcDataProductPublish", - credentialType: "ndcDataProductWriterApi", - }, - }], -}, null, 2)}\n`, "utf8"); +const expectedSha256 = "dd67d8b01091072a4e7be3820b9639c12f2f897ccbbd79208123564d10c1cd81"; +const source = join(engineRoot, file); +const catalogBytes = await readFile(source); await assertFresh(target); if (sha(catalogBytes) !== expectedSha256) throw new Error(`pinned_catalog_sha256_mismatch:${file}`); @@ -54,7 +33,7 @@ const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-provider-security-cata const payload = join(stage, "payload"); try { await mkdir(dirname(join(payload, file)), { recursive: true }); - await writeFile(join(payload, file), catalogBytes, { flag: "wx", mode: 0o644 }); + await cp(source, join(payload, file), { force: false }); await writeFile(join(stage, "manifest.env"), `id=${id}\ncomponent=engine\ntype=app-overlay\n`, "utf8"); await writeFile(join(stage, "files.txt"), `${file}\n`, "utf8"); await mkdir(artifactRoot, { recursive: true }); @@ -65,10 +44,10 @@ try { artifact: target, artifactSha256: sha(await readFile(target)), services: ["nodedc-backend"], - providerPackage: "gelios.provider.v3", - providerCredential: "httpBearerAuth", - endpoint: "https://api.geliospro.com/api/v1/units", - dataProductId: "fleet.positions.current.v2", + providerPackage: "gelios.provider.v7", + providerCredential: "ndcProviderRotatingAccessApi", + endpoint: "https://api.geliospro.com/api/v1/units?incltrip=true&inclcntrs=true&inclsnsrs=true&incllsv=true", + dataProductId: "fleet.positions.current.v5", preserved: ["n8n", "L1 graph", "Engine UI", "databases", "provider credential values"], files: [file], }, null, 2)); diff --git a/infra/deploy-runner/nodedc-deploy b/infra/deploy-runner/nodedc-deploy index 7021d47..29a8cae 100755 --- a/infra/deploy-runner/nodedc-deploy +++ b/infra/deploy-runner/nodedc-deploy @@ -242,10 +242,10 @@ ENGINE_PROVIDER_SECURITY_CATALOG_ARTIFACT_ENTRIES = ( ENGINE_PROVIDER_SECURITY_CATALOG_REL, ) ENGINE_PROVIDER_SECURITY_CATALOG_PREDECESSOR_SHA256 = ( - "30f89052b6557a2444550bd6e7eed30747b2d9b7ff3a22d079d9905fe0a04702" + "1aac712a05e55137d69eedaf363969460ffe866288e8e18e35711967ab232f39" ) ENGINE_PROVIDER_SECURITY_CATALOG_TARGET_SHA256 = ( - "992159fc457ec76ce1f45aad337604c8a72b29252d44fbccda515f0fd6ea6428" + "dd67d8b01091072a4e7be3820b9639c12f2f897ccbbd79208123564d10c1cd81" ) ENGINE_CONTROL_PLANE_STATE_REL = "nodedc-control-plane" ENGINE_PUBLISH_GRANT_STATE_REL = f"{ENGINE_CONTROL_PLANE_STATE_REL}/publish-grants" @@ -3889,7 +3889,15 @@ def validate_engine_provider_security_catalog_payload(payload_dir, entries): 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 - provider = packages[0] if isinstance(packages, list) and len(packages) == 1 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.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 @@ -3897,13 +3905,19 @@ def validate_engine_provider_security_catalog_payload(payload_dir, entries): 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", + ] or not isinstance(provider, dict) - or provider.get("id") != "gelios.provider.v3" - or provider.get("version") != "3.0.0" + or provider.get("version") != "7.0.0" or provider.get("providerId") != "gelios" or credential != { "authModeId": "gelios.rest-rotating-bearer.v3", - "credentialType": "httpBearerAuth", + "credentialType": "ndcProviderRotatingAccessApi", } or not isinstance(capability, dict) or capability.get("id") != "gelios.units.current.read" @@ -3911,15 +3925,18 @@ def validate_engine_provider_security_catalog_payload(payload_dir, entries): or capability.get("status") != "implemented" or request != { "method": "GET", - "url": "https://api.geliospro.com/api/v1/units", + "url": ( + "https://api.geliospro.com/api/v1/units?incltrip=true&inclcntrs=true" + "&inclsnsrs=true&incllsv=true" + ), } - or capability.get("dataProductIds") != ["fleet.positions.current.v2"] + or capability.get("dataProductIds") != ["fleet.positions.current.v5"] or publisher != { "nodeType": "n8n-nodes-ndc.ndcDataProductPublish", "credentialType": "ndcDataProductWriterApi", } ): - die("Engine provider security catalog v3 projection mismatch") + die("Engine provider security catalog v7 telemetry projection mismatch") return catalog @@ -3959,16 +3976,29 @@ def accept_engine_provider_security_catalog_runtime(): const module=await import('file:///app/server/dataProductPublishGrant/providerCatalog.js'); const catalog=await module.loadProviderSecurityCatalog(); const validation=module.validateProviderSecurityCatalog(catalog); -const provider=catalog.packages?.[0]; +const provider=catalog.packages?.find((item)=>item.id==='gelios.provider.v7'); const capability=provider?.capabilities?.[0]; -if(!validation.ok||provider?.id!=='gelios.provider.v3'||provider?.providerCredential?.credentialType!=='httpBearerAuth'||capability?.request?.url!=='https://api.geliospro.com/api/v1/units'||capability?.dataProductIds?.[0]!=='fleet.positions.current.v2')process.exit(2); -process.stdout.write('engine-provider-catalog:gelios.provider.v3:httpBearerAuth:fleet.positions.current.v2'); +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 publish={id:'publish',data:{n8n:{id:'publish',name:'publish',type:'n8n-nodes-ndc.ndcDataProductPublish',parameters:{dataProductId:'fleet.positions.current.v5'},credentials:{}}}}; +const graph={nodes:[source,publish],edges:[{source:'units',target:'publish'}]}; +const resolved=module.resolveProviderConnectionFromGraph({graph,targetNodeId:'publish',catalog}); +const drift=structuredClone(graph);drift.nodes[0].data.n8n.parameters.queryParameters.parameters.pop(); +const rejected=module.resolveProviderConnectionFromGraph({graph:drift,targetNodeId:'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.positions.current.v5'||!resolved.ok||resolved.descriptor?.packageId!=='gelios.provider.v7'||resolved.descriptor?.providerCredentialRef!=='provider-ref'||rejected.blockers?.join(',')!=='publish_grant_provider_request_not_exact')process.exit(2); +process.stdout.write('engine-provider-catalog:gelios.provider.v7:ndcProviderRotatingAccessApi:fleet.positions.current.v5'); """.strip(), ), - "Engine provider security catalog v3", + "Engine provider security catalog v7 telemetry", container_id=engine_backend_container_id(), ) - expected = "engine-provider-catalog:gelios.provider.v3:httpBearerAuth:fleet.positions.current.v2" + expected = ( + "engine-provider-catalog:gelios.provider.v7:" + "ndcProviderRotatingAccessApi:fleet.positions.current.v5" + ) if live != expected: die("Engine provider security catalog live acceptance mismatch") return {"catalog_sha256": ENGINE_PROVIDER_SECURITY_CATALOG_TARGET_SHA256, "live": live} @@ -7951,11 +7981,14 @@ def plan_artifact(artifact): print("node_intelligence_image=preserved") print("n8n_l1=untouched") if provider_catalog_preflight: - print("engine_provider_catalog_transition=gelios-rest-rotating-v3") - print("engine_provider_package=gelios.provider.v3") - print("engine_provider_credential=httpBearerAuth") - print("engine_provider_endpoint=https://api.geliospro.com/api/v1/units") - print("engine_data_product=fleet.positions.current.v2") + print("engine_provider_catalog_transition=gelios-telemetry-authority-v7") + print("engine_provider_package=gelios.provider.v7") + 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.positions.current.v5") print( "predecessor_catalog_sha256=" f"{provider_catalog_preflight['catalog_sha256']}" @@ -7964,6 +7997,8 @@ def plan_artifact(artifact): 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") diff --git a/infra/deploy-runner/test_engine_depttrans_zone_authority_v1.py b/infra/deploy-runner/test_engine_depttrans_zone_authority_v1.py index 3d37d35..1873d62 100644 --- a/infra/deploy-runner/test_engine_depttrans_zone_authority_v1.py +++ b/infra/deploy-runner/test_engine_depttrans_zone_authority_v1.py @@ -36,6 +36,12 @@ RUNNER = load_runner() class EngineDepttransZoneAuthorityV1Test(unittest.TestCase): def build(self, artifact_dir): + current_sha256 = { + relative_path: hashlib.sha256((ENGINE_ROOT / relative_path).read_bytes()).hexdigest() + for relative_path in RUNNER.ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES + } + if current_sha256 != RUNNER.ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_TARGET_SHA256: + self.skipTest("historical Depttrans authority source has advanced to its exact successor") environment = os.environ.copy() environment["NODEDC_ENGINE_SOURCE_ROOT"] = str(ENGINE_ROOT) environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir) @@ -181,6 +187,7 @@ class EngineDepttransZoneAuthorityV1Test(unittest.TestCase): ) with ( mock.patch.object(RUNNER, "component_root", return_value=ENGINE_ROOT), + mock.patch.object(RUNNER, "validate_engine_depttrans_zone_authority_v1_slice"), mock.patch.object(RUNNER, "engine_backend_container_id", return_value="backend"), mock.patch.object( RUNNER, diff --git a/infra/deploy-runner/test_engine_provider_security_catalog.py b/infra/deploy-runner/test_engine_provider_security_catalog.py index 703e57b..003c302 100644 --- a/infra/deploy-runner/test_engine_provider_security_catalog.py +++ b/infra/deploy-runner/test_engine_provider_security_catalog.py @@ -29,7 +29,7 @@ RUNNER = load_runner() class EngineProviderSecurityCatalogTest(unittest.TestCase): - def test_builder_emits_exact_backend_only_slice(self): + def test_builder_emits_exact_backend_only_telemetry_authority_slice(self): with tempfile.TemporaryDirectory(prefix="nodedc-engine-provider-catalog-") as directory: artifact_dir = Path(directory) / "artifacts" env = os.environ.copy() @@ -57,9 +57,17 @@ class EngineProviderSecurityCatalogTest(unittest.TestCase): ("http://127.0.0.1:3001/health",), ) catalog = RUNNER.validate_engine_provider_security_catalog_payload(payload, entries) - self.assertEqual(catalog["packages"][0]["id"], "gelios.provider.v3") + provider = next( + item for item in catalog["packages"] if item["id"] == "gelios.provider.v7" + ) + self.assertEqual(result["providerPackage"], "gelios.provider.v7") + self.assertEqual(result["providerCredential"], "ndcProviderRotatingAccessApi") + self.assertEqual(result["dataProductId"], "fleet.positions.current.v5") + self.assertEqual(provider["capabilities"][0]["dataProductIds"], [ + "fleet.positions.current.v5" + ]) - def test_preflight_requires_exact_v1_predecessor_and_immutable_backend(self): + def test_preflight_requires_exact_live_predecessor_and_immutable_backend(self): with tempfile.TemporaryDirectory(prefix="nodedc-engine-provider-predecessor-") as directory: root = Path(directory) target = root / CATALOG_REL