feat(deploy): add Gelios profile authority transition

This commit is contained in:
Codex 2026-07-23 07:48:17 +03:00
parent e843bbda66
commit 4cdc78d545
3 changed files with 71 additions and 26 deletions

View File

@ -14,7 +14,7 @@ const engineRoot = resolve(
const artifactRoot = resolve( const artifactRoot = resolve(
process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"), process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"),
); );
const [transitionId = "20260722-020", ...extra] = process.argv.slice(2); const [transitionId = "20260723-025", ...extra] = process.argv.slice(2);
if (extra.length || !/^\d{8}-[0-9]{3}$/.test(transitionId)) { if (extra.length || !/^\d{8}-[0-9]{3}$/.test(transitionId)) {
throw new Error("usage: build-engine-provider-security-catalog-artifact.mjs [YYYYMMDD-NNN]"); throw new Error("usage: build-engine-provider-security-catalog-artifact.mjs [YYYYMMDD-NNN]");
} }
@ -22,7 +22,7 @@ if (extra.length || !/^\d{8}-[0-9]{3}$/.test(transitionId)) {
const id = `engine-provider-security-catalog-${transitionId}`; const id = `engine-provider-security-catalog-${transitionId}`;
const target = join(artifactRoot, `nodedc-${id}.tgz`); const target = join(artifactRoot, `nodedc-${id}.tgz`);
const file = "nodedc-source/server/assets/provider-packages/v1/catalog.json"; const file = "nodedc-source/server/assets/provider-packages/v1/catalog.json";
const expectedSha256 = "dd67d8b01091072a4e7be3820b9639c12f2f897ccbbd79208123564d10c1cd81"; const expectedSha256 = "773335bb616a5c03eb2108c4f53ef092c02e75ee75f014511201bc12e2956b27";
const source = join(engineRoot, file); const source = join(engineRoot, file);
const catalogBytes = await readFile(source); const catalogBytes = await readFile(source);
@ -44,10 +44,10 @@ try {
artifact: target, artifact: target,
artifactSha256: sha(await readFile(target)), artifactSha256: sha(await readFile(target)),
services: ["nodedc-backend"], services: ["nodedc-backend"],
providerPackage: "gelios.provider.v7", providerPackage: "gelios.provider.v8",
providerCredential: "ndcProviderRotatingAccessApi", providerCredential: "ndcProviderRotatingAccessApi",
endpoint: "https://api.geliospro.com/api/v1/units?incltrip=true&inclcntrs=true&inclsnsrs=true&incllsv=true", endpoint: "https://api.geliospro.com/api/v1/units?incltrip=true&inclcntrs=true&inclsnsrs=true&incllsv=true",
dataProductId: "fleet.positions.current.v5", dataProductId: "fleet.units.profile.current.v1",
preserved: ["n8n", "L1 graph", "Engine UI", "databases", "provider credential values"], preserved: ["n8n", "L1 graph", "Engine UI", "databases", "provider credential values"],
files: [file], files: [file],
}, null, 2)); }, null, 2));

View File

@ -242,10 +242,10 @@ ENGINE_PROVIDER_SECURITY_CATALOG_ARTIFACT_ENTRIES = (
ENGINE_PROVIDER_SECURITY_CATALOG_REL, ENGINE_PROVIDER_SECURITY_CATALOG_REL,
) )
ENGINE_PROVIDER_SECURITY_CATALOG_PREDECESSOR_SHA256 = ( ENGINE_PROVIDER_SECURITY_CATALOG_PREDECESSOR_SHA256 = (
"1aac712a05e55137d69eedaf363969460ffe866288e8e18e35711967ab232f39" "dd67d8b01091072a4e7be3820b9639c12f2f897ccbbd79208123564d10c1cd81"
) )
ENGINE_PROVIDER_SECURITY_CATALOG_TARGET_SHA256 = ( ENGINE_PROVIDER_SECURITY_CATALOG_TARGET_SHA256 = (
"dd67d8b01091072a4e7be3820b9639c12f2f897ccbbd79208123564d10c1cd81" "773335bb616a5c03eb2108c4f53ef092c02e75ee75f014511201bc12e2956b27"
) )
ENGINE_CONTROL_PLANE_STATE_REL = "nodedc-control-plane" ENGINE_CONTROL_PLANE_STATE_REL = "nodedc-control-plane"
ENGINE_PUBLISH_GRANT_STATE_REL = f"{ENGINE_CONTROL_PLANE_STATE_REL}/publish-grants" ENGINE_PUBLISH_GRANT_STATE_REL = f"{ENGINE_CONTROL_PLANE_STATE_REL}/publish-grants"
@ -3892,6 +3892,13 @@ def validate_engine_provider_security_catalog_payload(payload_dir, entries):
package_ids = [item.get("id") for item in packages if isinstance(item, dict)] \ package_ids = [item.get("id") for item in packages if isinstance(item, dict)] \
if isinstance(packages, list) else None if isinstance(packages, list) else None
provider = next( 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 item for item in packages
if isinstance(item, dict) and item.get("id") == "gelios.provider.v7" if isinstance(item, dict) and item.get("id") == "gelios.provider.v7"
@ -3911,9 +3918,10 @@ def validate_engine_provider_security_catalog_payload(payload_dir, entries):
"gelios.provider.v5", "gelios.provider.v5",
"moscow-department-of-transport.pmd-slow-zones.v1", "moscow-department-of-transport.pmd-slow-zones.v1",
"gelios.provider.v7", "gelios.provider.v7",
"gelios.provider.v8",
] ]
or not isinstance(provider, dict) or not isinstance(provider, dict)
or provider.get("version") != "7.0.0" or provider.get("version") != "8.0.0"
or provider.get("providerId") != "gelios" or provider.get("providerId") != "gelios"
or credential != { or credential != {
"authModeId": "gelios.rest-rotating-bearer.v3", "authModeId": "gelios.rest-rotating-bearer.v3",
@ -3930,13 +3938,41 @@ def validate_engine_provider_security_catalog_payload(payload_dir, entries):
"&inclsnsrs=true&incllsv=true" "&inclsnsrs=true&incllsv=true"
), ),
} }
or capability.get("dataProductIds") != ["fleet.positions.current.v5"] or capability.get("dataProductIds") != ["fleet.units.profile.current.v1"]
or publisher != { or publisher != {
"nodeType": "n8n-nodes-ndc.ndcDataProductPublish", "nodeType": "n8n-nodes-ndc.ndcDataProductPublish",
"credentialType": "ndcDataProductWriterApi", "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 telemetry projection mismatch") die("Engine provider security catalog v7/v8 projection mismatch")
return catalog return catalog
@ -3976,28 +4012,31 @@ def accept_engine_provider_security_catalog_runtime():
const module=await import('file:///app/server/dataProductPublishGrant/providerCatalog.js'); const module=await import('file:///app/server/dataProductPublishGrant/providerCatalog.js');
const catalog=await module.loadProviderSecurityCatalog(); const catalog=await module.loadProviderSecurityCatalog();
const validation=module.validateProviderSecurityCatalog(catalog); const validation=module.validateProviderSecurityCatalog(catalog);
const provider=catalog.packages?.find((item)=>item.id==='gelios.provider.v7'); const provider=catalog.packages?.find((item)=>item.id==='gelios.provider.v8');
const capability=provider?.capabilities?.[0]; const capability=provider?.capabilities?.[0];
const slot='ndcProviderRotatingAccessApi'; const slot='ndcProviderRotatingAccessApi';
const credential={id:'native-provider',name:'provider',nodeDcCredentialId:'provider-ref'}; const credential={id:'native-provider',name:'provider',nodeDcCredentialId:'provider-ref'};
const policy={kind:'httpRequest',allowedHosts:['api.geliospro.com'],requireHttps:true,disableRedirects:true}; 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 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 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 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 graph={nodes:[source,publish],edges:[{source:'units',target:'publish'}]}; 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 resolved=module.resolveProviderConnectionFromGraph({graph,targetNodeId:'publish',catalog}); const profileGraph={nodes:[source,profilePublish],edges:[{source:'units',target:'profile-publish'}]};
const drift=structuredClone(graph);drift.nodes[0].data.n8n.parameters.queryParameters.parameters.pop(); const positionsGraph={nodes:[source,positionsPublish],edges:[{source:'units',target:'positions-publish'}]};
const rejected=module.resolveProviderConnectionFromGraph({graph:drift,targetNodeId:'publish',catalog}); const profileResolved=module.resolveProviderConnectionFromGraph({graph:profileGraph,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.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); const positionsResolved=module.resolveProviderConnectionFromGraph({graph:positionsGraph,targetNodeId:'positions-publish',catalog});
process.stdout.write('engine-provider-catalog:gelios.provider.v7:ndcProviderRotatingAccessApi:fleet.positions.current.v5'); 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(), """.strip(),
), ),
"Engine provider security catalog v7 telemetry", "Engine provider security catalog v8 unit profile",
container_id=engine_backend_container_id(), container_id=engine_backend_container_id(),
) )
expected = ( expected = (
"engine-provider-catalog:gelios.provider.v7:" "engine-provider-catalog:gelios.provider.v7:fleet.positions.current.v5|"
"ndcProviderRotatingAccessApi:fleet.positions.current.v5" "gelios.provider.v8:fleet.units.profile.current.v1"
) )
if live != expected: if live != expected:
die("Engine provider security catalog live acceptance mismatch") die("Engine provider security catalog live acceptance mismatch")
@ -7981,14 +8020,14 @@ def plan_artifact(artifact):
print("node_intelligence_image=preserved") print("node_intelligence_image=preserved")
print("n8n_l1=untouched") print("n8n_l1=untouched")
if provider_catalog_preflight: if provider_catalog_preflight:
print("engine_provider_catalog_transition=gelios-telemetry-authority-v7") print("engine_provider_catalog_transition=gelios-unit-profile-authority-v8")
print("engine_provider_package=gelios.provider.v7") print("engine_provider_package=gelios.provider.v8")
print("engine_provider_credential=ndcProviderRotatingAccessApi") print("engine_provider_credential=ndcProviderRotatingAccessApi")
print( print(
"engine_provider_endpoint=https://api.geliospro.com/api/v1/units?" "engine_provider_endpoint=https://api.geliospro.com/api/v1/units?"
"incltrip=true&inclcntrs=true&inclsnsrs=true&incllsv=true" "incltrip=true&inclcntrs=true&inclsnsrs=true&incllsv=true"
) )
print("engine_data_product=fleet.positions.current.v5") print("engine_data_product=fleet.units.profile.current.v1")
print( print(
"predecessor_catalog_sha256=" "predecessor_catalog_sha256="
f"{provider_catalog_preflight['catalog_sha256']}" f"{provider_catalog_preflight['catalog_sha256']}"

View File

@ -29,7 +29,7 @@ RUNNER = load_runner()
class EngineProviderSecurityCatalogTest(unittest.TestCase): class EngineProviderSecurityCatalogTest(unittest.TestCase):
def test_builder_emits_exact_backend_only_telemetry_authority_slice(self): def test_builder_emits_exact_backend_only_unit_profile_authority_slice(self):
with tempfile.TemporaryDirectory(prefix="nodedc-engine-provider-catalog-") as directory: with tempfile.TemporaryDirectory(prefix="nodedc-engine-provider-catalog-") as directory:
artifact_dir = Path(directory) / "artifacts" artifact_dir = Path(directory) / "artifacts"
env = os.environ.copy() env = os.environ.copy()
@ -58,12 +58,18 @@ class EngineProviderSecurityCatalogTest(unittest.TestCase):
) )
catalog = RUNNER.validate_engine_provider_security_catalog_payload(payload, entries) catalog = RUNNER.validate_engine_provider_security_catalog_payload(payload, entries)
provider = next( provider = next(
item for item in catalog["packages"] if item["id"] == "gelios.provider.v8"
)
previous_provider = next(
item for item in catalog["packages"] if item["id"] == "gelios.provider.v7" item for item in catalog["packages"] if item["id"] == "gelios.provider.v7"
) )
self.assertEqual(result["providerPackage"], "gelios.provider.v7") self.assertEqual(result["providerPackage"], "gelios.provider.v8")
self.assertEqual(result["providerCredential"], "ndcProviderRotatingAccessApi") self.assertEqual(result["providerCredential"], "ndcProviderRotatingAccessApi")
self.assertEqual(result["dataProductId"], "fleet.positions.current.v5") self.assertEqual(result["dataProductId"], "fleet.units.profile.current.v1")
self.assertEqual(provider["capabilities"][0]["dataProductIds"], [ self.assertEqual(provider["capabilities"][0]["dataProductIds"], [
"fleet.units.profile.current.v1"
])
self.assertEqual(previous_provider["capabilities"][0]["dataProductIds"], [
"fleet.positions.current.v5" "fleet.positions.current.v5"
]) ])