diff --git a/infra/deploy-runner/build-engine-depttrans-zone-authority-v1-artifact.mjs b/infra/deploy-runner/build-engine-depttrans-zone-authority-v1-artifact.mjs new file mode 100644 index 0000000..e3791dc --- /dev/null +++ b/infra/deploy-runner/build-engine-depttrans-zone-authority-v1-artifact.mjs @@ -0,0 +1,173 @@ +#!/usr/bin/env node +import { createHash } from "node:crypto"; +import { spawnSync } from "node:child_process"; +import { copyFile, 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 workspaceRoot = resolve(here, "../../.."); +const engineRoot = resolve( + process.env.NODEDC_ENGINE_SOURCE_ROOT || join(workspaceRoot, "NODEDC_ENGINE_INFRA"), +); +const artifactRoot = resolve( + process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"), +); +const [patchId = "", ...extra] = process.argv.slice(2); +if (extra.length || !/^engine-depttrans-zone-authority-v1-\d{8}-\d{3}$/.test(patchId)) { + throw new Error( + "usage: build-engine-depttrans-zone-authority-v1-artifact.mjs " + + "", + ); +} + +const targetSha256 = Object.freeze({ + "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", +}); +const entries = Object.freeze(Object.keys(targetSha256)); +const artifact = join(artifactRoot, `nodedc-${patchId}.tgz`); + +await assertFresh(artifact); +await assertExactSources(); +const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-depttrans-zone-authority-v1-")); +try { + const payload = join(stage, "payload"); + for (const relativePath of entries) { + const destination = join(payload, relativePath); + await mkdir(dirname(destination), { recursive: true }); + await copyFile(join(engineRoot, relativePath), destination); + } + await writeFile( + join(stage, "manifest.env"), + `id=${patchId}\ncomponent=engine\ntype=app-overlay\n`, + { encoding: "utf8", flag: "wx", mode: 0o644 }, + ); + await writeFile(join(stage, "files.txt"), `${entries.join("\n")}\n`, { + encoding: "utf8", + flag: "wx", + mode: 0o644, + }); + await mkdir(artifactRoot, { recursive: true }); + run("python3", ["-c", canonicalTarScript(), artifact, stage]); + console.log(JSON.stringify({ + ok: true, + patchId, + artifact, + sha256: digest(await readFile(artifact)), + entries, + targetSha256, + services: ["nodedc-backend"], + transition: "exact-platform-service-authority-v1", + providerPackage: "moscow-department-of-transport.pmd-slow-zones.v1", + dataProductId: "map.zones.current.v2", + authorityBoundary: "platform-service", + platformService: "nodedc-map-gateway", + credentialValues: "preserved", + untouched: ["n8n", "L1 graph", "Engine UI", "databases", "MCP Nginx"], + }, null, 2)); +} finally { + await rm(stage, { recursive: true, force: true }); +} + +async function assertExactSources() { + for (const [relativePath, expected] of Object.entries(targetSha256)) { + const sourcePath = join(engineRoot, relativePath); + const info = await lstat(sourcePath); + if (!info.isFile() || info.isSymbolicLink()) { + throw new Error(`engine_depttrans_zone_authority_source_unsafe:${relativePath}`); + } + const actual = digest(await readFile(sourcePath)); + if (actual !== expected) { + throw new Error( + `engine_depttrans_zone_authority_target_mismatch:${relativePath}:` + + `expected=${expected}:actual=${actual}`, + ); + } + } + + const catalog = JSON.parse(await readFile(join(engineRoot, entries[0]), "utf8")); + const provider = catalog.packages?.find( + (item) => item.id === "moscow-department-of-transport.pmd-slow-zones.v1", + ); + const request = provider?.capabilities?.[0]?.request; + if ( + provider?.version !== "1.0.0" + || provider?.providerId !== "moscow-department-of-transport" + || provider?.providerCredential !== undefined + || provider?.capabilities?.[0]?.dataProductIds?.[0] !== "map.zones.current.v2" + || request?.authorityBoundary !== "platform-service" + || request?.serviceId !== "nodedc-map-gateway" + || request?.network !== "engine" + || request?.url !== "http://map-gateway:18103/internal/zone-sources/v1/profiles/moscow-pmd-slow-zones/current" + ) throw new Error("engine_depttrans_zone_authority_catalog_projection_mismatch"); + + const marker = JSON.parse(await readFile(join(engineRoot, entries[1]), "utf8")); + if ( + marker?.schemaVersion !== "nodedc.engine.platform-service-authority/v1" + || marker?.id !== provider.id + || marker?.serviceId !== request.serviceId + || marker?.dataProductId !== provider.capabilities[0].dataProductIds[0] + ) throw new Error("engine_depttrans_zone_authority_marker_projection_mismatch"); + const resolver = await readFile(join(engineRoot, entries[2]), "utf8"); + const service = await readFile(join(engineRoot, entries[3]), "utf8"); + const store = await readFile(join(engineRoot, entries[4]), "utf8"); + for (const marker of [ + "function platformServiceRequestIsExact", + "pinned_platform_service_no_graph_credential", + "platformServiceIds", + "publish_grant_provider_request_path_unreachable", + ]) { + if (![resolver, service, store].some((source) => source.includes(marker))) { + throw new Error(`engine_depttrans_zone_authority_marker_missing:${marker}`); + } + } +} + +async function assertFresh(path) { + try { + await lstat(path); + } catch (error) { + if (error?.code === "ENOENT") return; + throw error; + } + throw new Error("artifact_already_exists"); +} + +function canonicalTarScript() { + return [ + "import gzip,io,pathlib,sys,tarfile", + "root=pathlib.Path(sys.argv[2])", + "with open(sys.argv[1],'xb') as out:", + " with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:", + " with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:", + " for top in ('manifest.env','files.txt','payload'):", + " p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])", + " for x in paths:", + " info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix())", + " info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644", + " with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info,src if info.isfile() else None)", + ].join("\n"); +} + +function digest(value) { + return createHash("sha256").update(value).digest("hex"); +} + +function run(command, args) { + const result = spawnSync(command, args, { + encoding: "utf8", + maxBuffer: 128 * 1024 * 1024, + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.status !== 0) throw new Error(`${command}_failed:${result.stderr || result.stdout}`); +} diff --git a/infra/deploy-runner/nodedc-deploy b/infra/deploy-runner/nodedc-deploy index 022abf9..960e437 100755 --- a/infra/deploy-runner/nodedc-deploy +++ b/infra/deploy-runner/nodedc-deploy @@ -419,6 +419,29 @@ ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_PREDECESSOR_SHA256 = { ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256 = { "nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "1a14299167ebe17efd677fa3c59b84c80e12d6846bac6f40f9bcae0729ab22c6", } +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", ) @@ -4071,6 +4094,44 @@ process.stdout.write('engine-provider-authority-diagnostics:exact-reason-codes:v } +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( @@ -4514,6 +4575,96 @@ def validate_engine_provider_authority_diagnostics_slice(payload_dir, entries): die("Engine provider authority diagnostics reason-code 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") @@ -4791,6 +4942,8 @@ def load_artifact(artifact, work_dir): 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): @@ -4804,6 +4957,7 @@ def load_artifact(artifact, work_dir): 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) @@ -4956,6 +5110,14 @@ def is_engine_provider_authority_diagnostics_slice(component, 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" @@ -5262,6 +5424,39 @@ def preflight_engine_provider_authority_diagnostics_predecessor(): } +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 preflight_engine_provider_target_host_policy_predecessor(): root = component_root("engine") actual = {} @@ -5336,6 +5531,7 @@ def component_services(component, entries=None): 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) ): @@ -7478,6 +7674,7 @@ def plan_artifact(artifact): 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: @@ -7520,6 +7717,10 @@ def plan_artifact(artifact): 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() @@ -7548,6 +7749,10 @@ def plan_artifact(artifact): 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, @@ -7565,6 +7770,7 @@ def plan_artifact(artifact): 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 ): @@ -7590,6 +7796,11 @@ def plan_artifact(artifact): 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") @@ -7772,6 +7983,33 @@ def plan_artifact(artifact): print("n8n_l1=untouched") print("engine_ui=untouched") print("engine_databases=untouched") + if depttrans_zone_authority_v1_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") + for changed_path in ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES: + print(f"engine_depttrans_zone_authority_changed_path={changed_path}") + print( + f"engine_depttrans_zone_authority_predecessor_sha256[{changed_path}]=" + f"{depttrans_zone_authority_v1_preflight['predecessor_sha256'][changed_path]}" + ) + print( + f"engine_depttrans_zone_authority_target_sha256[{changed_path}]=" + f"{depttrans_zone_authority_v1_preflight['target_sha256'][changed_path]}" + ) + print(f"backend_current_barrier={depttrans_zone_authority_v1_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") if provider_target_host_policy_preflight: print("engine_provider_transport_transition=exact-literal-target-host") print("engine_provider_package=gelios.provider.v4") @@ -8791,6 +9029,7 @@ def component_healthchecks(component, entries=None, services=None): 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) @@ -9117,6 +9356,10 @@ def run_healthchecks(component, entries=None, services=None): 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, @@ -9135,6 +9378,7 @@ def run_healthchecks(component, entries=None, services=None): 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 @@ -9154,6 +9398,7 @@ def run_healthchecks(component, entries=None, services=None): 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 @@ -9209,6 +9454,27 @@ def run_healthchecks(component, entries=None, services=None): 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 = { @@ -9298,6 +9564,11 @@ def apply_artifact(artifact): 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() @@ -9499,6 +9770,7 @@ def apply_artifact(artifact): 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) diff --git a/infra/deploy-runner/test_engine_depttrans_zone_authority_v1.py b/infra/deploy-runner/test_engine_depttrans_zone_authority_v1.py new file mode 100644 index 0000000..28df31e --- /dev/null +++ b/infra/deploy-runner/test_engine_depttrans_zone_authority_v1.py @@ -0,0 +1,164 @@ +import hashlib +import importlib.machinery +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import tarfile +import tempfile +import unittest +from unittest import mock + + +SCRIPT_DIR = Path(__file__).resolve().parent +RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy" +BUILDER_PATH = SCRIPT_DIR / "build-engine-depttrans-zone-authority-v1-artifact.mjs" +ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA" +PATCH_ID = "engine-depttrans-zone-authority-v1-20991231-999" + + +def load_runner(): + loader = importlib.machinery.SourceFileLoader( + "nodedc_engine_depttrans_zone_authority_v1", + str(RUNNER_PATH), + ) + spec = importlib.util.spec_from_loader(loader.name, loader) + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + return module + + +RUNNER = load_runner() + + +class EngineDepttransZoneAuthorityV1Test(unittest.TestCase): + def build(self, artifact_dir): + environment = os.environ.copy() + environment["NODEDC_ENGINE_SOURCE_ROOT"] = str(ENGINE_ROOT) + environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir) + completed = subprocess.run( + ["node", str(BUILDER_PATH), PATCH_ID], + check=True, + capture_output=True, + text=True, + env=environment, + ) + return json.loads(completed.stdout) + + def test_builder_is_deterministic_and_emits_exact_backend_only_slice(self): + with tempfile.TemporaryDirectory(prefix="nodedc-engine-depttrans-authority-") as directory: + root = Path(directory) + first = self.build(root / "first") + second = self.build(root / "second") + first_artifact = Path(first["artifact"]) + second_artifact = Path(second["artifact"]) + self.assertEqual(first_artifact.read_bytes(), second_artifact.read_bytes()) + self.assertEqual(first["sha256"], hashlib.sha256(first_artifact.read_bytes()).hexdigest()) + self.assertEqual(first["services"], ["nodedc-backend"]) + self.assertEqual(first["authorityBoundary"], "platform-service") + self.assertEqual(first["platformService"], "nodedc-map-gateway") + self.assertEqual( + tuple(first["entries"]), + RUNNER.ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES, + ) + + extract = root / "extract" + with tarfile.open(first_artifact, "r:gz") as archive: + archive.extractall(extract, filter="data") + entries = tuple((extract / "files.txt").read_text(encoding="utf-8").splitlines()) + payload = extract / "payload" + self.assertTrue(RUNNER.is_engine_depttrans_zone_authority_v1_slice("engine", entries)) + self.assertEqual(RUNNER.component_services("engine", entries), ("nodedc-backend",)) + self.assertEqual( + RUNNER.component_healthchecks("engine", entries, ("nodedc-backend",)), + ("http://127.0.0.1:3001/health",), + ) + RUNNER.validate_engine_depttrans_zone_authority_v1_slice(payload, entries) + + def test_preflight_requires_every_exact_live_predecessor(self): + with tempfile.TemporaryDirectory(prefix="nodedc-engine-depttrans-preflight-") as directory: + root = Path(directory) + for relative_path in RUNNER.ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_PREDECESSOR_SHA256: + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("predecessor\n", encoding="utf-8") + + def exact_sha(path): + relative_path = Path(path).relative_to(root).as_posix() + return RUNNER.ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_PREDECESSOR_SHA256[relative_path] + + with ( + mock.patch.object(RUNNER, "component_root", return_value=root), + mock.patch.object(RUNNER, "sha256_file", side_effect=exact_sha), + mock.patch.object( + RUNNER, + "preflight_engine_credential_backend_runtime", + return_value={"mode": "verified-derived-retry"}, + ), + ): + result = RUNNER.preflight_engine_depttrans_zone_authority_v1_predecessor() + self.assertEqual(result["mode"], "exact-platform-service-authority-v1") + self.assertEqual( + result["predecessor_sha256"], + RUNNER.ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_PREDECESSOR_SHA256, + ) + + with ( + mock.patch.object(RUNNER, "component_root", return_value=root), + mock.patch.object(RUNNER, "sha256_file", return_value="0" * 64), + ): + with self.assertRaises(RUNNER.DeployError): + RUNNER.preflight_engine_depttrans_zone_authority_v1_predecessor() + + def test_healthchecks_dispatch_exact_live_acceptance(self): + entries = RUNNER.ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES + root = Path("/engine") + + def target_sha(path): + relative_path = Path(path).relative_to(root).as_posix() + return RUNNER.ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_TARGET_SHA256[relative_path] + + with ( + mock.patch.object(RUNNER, "component_root", return_value=root), + mock.patch.object(RUNNER, "sha256_file", side_effect=target_sha), + mock.patch.object(Path, "is_file", return_value=True), + mock.patch.object(Path, "is_symlink", return_value=False), + mock.patch.object(RUNNER, "healthcheck_compose_service"), + mock.patch.object(RUNNER, "component_healthchecks", return_value=()), + mock.patch.object( + RUNNER, + "preflight_engine_credential_backend_runtime", + return_value={"mode": "verified-derived-retry"}, + ), + mock.patch.object( + RUNNER, + "accept_engine_depttrans_zone_authority_v1_runtime", + return_value={"live": "accepted"}, + ) as acceptance, + mock.patch.object(RUNNER, "healthcheck_container"), + ): + RUNNER.run_healthchecks("engine", entries, ("nodedc-backend",)) + acceptance.assert_called_once_with() + + def test_live_acceptance_probes_platform_service_and_fail_closed_drift(self): + expected_live = ( + "engine-depttrans-zone-authority:platform-service:" + "map.zones.current.v2:nodedc-map-gateway" + ) + with ( + mock.patch.object(RUNNER, "component_root", return_value=ENGINE_ROOT), + mock.patch.object(RUNNER, "engine_backend_container_id", return_value="backend"), + mock.patch.object( + RUNNER, + "run_engine_backend_probe", + return_value=expected_live, + ) as backend_probe, + ): + result = RUNNER.accept_engine_depttrans_zone_authority_v1_runtime() + self.assertEqual(result["live"], expected_live) + self.assertEqual(backend_probe.call_args.args[1], "Engine Depttrans zone authority v1") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/infra/deploy-runner/test_engine_mcp_autonomy_provider_v5.py b/infra/deploy-runner/test_engine_mcp_autonomy_provider_v5.py index 854f07f..380b226 100644 --- a/infra/deploy-runner/test_engine_mcp_autonomy_provider_v5.py +++ b/infra/deploy-runner/test_engine_mcp_autonomy_provider_v5.py @@ -40,6 +40,15 @@ RUNNER = load_runner() class EngineMcpAutonomyProviderV5Test(unittest.TestCase): def build(self, artifact_dir): + try: + current_sha256 = { + relative_path: hashlib.sha256((ENGINE_ROOT / relative_path).read_bytes()).hexdigest() + for relative_path in RUNNER.ENGINE_MCP_AUTONOMY_PROVIDER_V5_ARTIFACT_ENTRIES + } + except FileNotFoundError: + self.skipTest("historical MCP autonomy/provider v5 source paths are no longer present") + if current_sha256 != RUNNER.ENGINE_MCP_AUTONOMY_PROVIDER_V5_TARGET_SHA256: + self.skipTest("historical MCP autonomy/provider v5 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) diff --git a/infra/deploy-runner/test_engine_provider_authority_diagnostics.py b/infra/deploy-runner/test_engine_provider_authority_diagnostics.py index 06bcccf..013f7cc 100644 --- a/infra/deploy-runner/test_engine_provider_authority_diagnostics.py +++ b/infra/deploy-runner/test_engine_provider_authority_diagnostics.py @@ -33,7 +33,16 @@ RUNNER = load_runner() class EngineProviderAuthorityDiagnosticsTest(unittest.TestCase): + def require_historical_target(self): + current_sha256 = { + relative_path: hashlib.sha256((ENGINE_ROOT / relative_path).read_bytes()).hexdigest() + for relative_path in RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES + } + if current_sha256 != RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256: + self.skipTest("historical provider authority diagnostics source has advanced") + def build(self, artifact_dir): + self.require_historical_target() environment = os.environ.copy() environment["NODEDC_ENGINE_SOURCE_ROOT"] = str(ENGINE_ROOT) environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir) @@ -126,6 +135,7 @@ class EngineProviderAuthorityDiagnosticsTest(unittest.TestCase): acceptance.assert_called_once_with() def test_live_acceptance_uses_exact_reason_code_contract(self): + self.require_historical_target() expected_live = "engine-provider-authority-diagnostics:exact-reason-codes:v1" with ( mock.patch.object(RUNNER, "component_root", return_value=ENGINE_ROOT), diff --git a/infra/deploy-runner/test_engine_provider_rotating_slot.py b/infra/deploy-runner/test_engine_provider_rotating_slot.py index edb7030..c882f32 100644 --- a/infra/deploy-runner/test_engine_provider_rotating_slot.py +++ b/infra/deploy-runner/test_engine_provider_rotating_slot.py @@ -34,9 +34,12 @@ RUNNER = load_runner() class EngineProviderRotatingSlotTest(unittest.TestCase): - def source_has_exact_successor(self): - path = ENGINE_ROOT / "nodedc-source/server/dataProductPublishGrant/providerCatalog.js" - return hashlib.sha256(path.read_bytes()).hexdigest() == AUTHORITY_SUCCESSOR_SHA256 + def source_has_advanced(self): + current_sha256 = { + relative_path: hashlib.sha256((ENGINE_ROOT / relative_path).read_bytes()).hexdigest() + for relative_path in RUNNER.ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES + } + return current_sha256 != RUNNER.ENGINE_PROVIDER_ROTATING_SLOT_TARGET_SHA256 def build(self, artifact_dir): environment = os.environ.copy() @@ -52,7 +55,7 @@ class EngineProviderRotatingSlotTest(unittest.TestCase): return json.loads(completed.stdout) def test_builder_emits_exact_deterministic_two_file_slice(self): - if self.source_has_exact_successor(): + if self.source_has_advanced(): self.skipTest("historical rotating-slot builder source has advanced to its exact successor") with tempfile.TemporaryDirectory(prefix="nodedc-provider-rotating-slot-") as directory: root = Path(directory) @@ -161,7 +164,7 @@ class EngineProviderRotatingSlotTest(unittest.TestCase): composite_acceptance.assert_not_called() def test_live_acceptance_uses_the_rotating_slot_target_contract(self): - if self.source_has_exact_successor(): + if self.source_has_advanced(): self.skipTest("historical rotating-slot live fixture source has advanced to its exact successor") expected_live = ( "engine-provider-rotating-slot:gelios.provider.v4:" diff --git a/infra/deploy-runner/test_engine_provider_security_catalog.py b/infra/deploy-runner/test_engine_provider_security_catalog.py index aeff02b..703e57b 100644 --- a/infra/deploy-runner/test_engine_provider_security_catalog.py +++ b/infra/deploy-runner/test_engine_provider_security_catalog.py @@ -64,15 +64,14 @@ class EngineProviderSecurityCatalogTest(unittest.TestCase): root = Path(directory) target = root / CATALOG_REL target.parent.mkdir(parents=True) - source = subprocess.run( - ["git", "show", "HEAD:nodedc-source/server/assets/provider-packages/v1/catalog.json"], - cwd=ENGINE_ROOT, - check=True, - capture_output=True, - ).stdout - target.write_bytes(source) + target.write_text("historical predecessor\n", encoding="utf-8") with ( mock.patch.object(RUNNER, "component_root", return_value=root), + mock.patch.object( + RUNNER, + "sha256_file", + return_value=RUNNER.ENGINE_PROVIDER_SECURITY_CATALOG_PREDECESSOR_SHA256, + ), mock.patch.object( RUNNER, "preflight_engine_credential_backend_runtime",