diff --git a/infra/deploy-runner/build-engine-provider-authority-diagnostics-artifact.mjs b/infra/deploy-runner/build-engine-provider-authority-diagnostics-artifact.mjs index 95e0caf..50155cf 100644 --- a/infra/deploy-runner/build-engine-provider-authority-diagnostics-artifact.mjs +++ b/infra/deploy-runner/build-engine-provider-authority-diagnostics-artifact.mjs @@ -14,6 +14,14 @@ const engineRoot = resolve( const artifactRoot = resolve( process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"), ); +const baselineArtifact = resolve( + here, + "../deploy-artifacts/nodedc-engine-mcp-control-plane-20260718-003.tgz", +); +const baselineArtifactSha256 = + "249aef9527666c562e9648b15737e66cc5c1dc7c0788b58ea714da270b5eb4ba"; +const projectionRel = "nodedc-source/server/nodeIntelligence/upstreamProjection.js"; +const descriptorRel = "nodedc-source/services/node-intelligence/activation.json"; const [patchId = "", ...extra] = process.argv.slice(2); if (extra.length || !/^engine-provider-authority-diagnostics-\d{8}-\d{3}$/.test(patchId)) { throw new Error( @@ -23,21 +31,32 @@ if (extra.length || !/^engine-provider-authority-diagnostics-\d{8}-\d{3}$/.test( } const targetSha256 = Object.freeze({ - "nodedc-source/server/nodeIntelligence/upstreamProjection.js": + [projectionRel]: "2dfa6b4f37d9bfa8b92a8109d4060d02dd2634ceb8f7924504b83ccf3fdd1523", + [descriptorRel]: + "a8adb8c03e1a82ff683c99a8c6482c78eca1b63422e7e7e417424363b995079e", }); const entries = Object.freeze(Object.keys(targetSha256)); const artifact = join(artifactRoot, `nodedc-${patchId}.tgz`); await assertFresh(artifact); await assertExactSources(); +const descriptorText = await buildTargetDescriptor(); const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-provider-authority-diagnostics-")); 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); + if (relativePath === descriptorRel) { + await writeFile(destination, descriptorText, { + encoding: "utf8", + flag: "wx", + mode: 0o644, + }); + } else { + await copyFile(join(engineRoot, relativePath), destination); + } } await writeFile( join(stage, "manifest.env"), @@ -71,6 +90,7 @@ try { async function assertExactSources() { for (const [relativePath, expected] of Object.entries(targetSha256)) { + if (relativePath === descriptorRel) continue; const sourcePath = join(engineRoot, relativePath); const info = await lstat(sourcePath); if (!info.isFile() || info.isSymbolicLink()) { @@ -97,6 +117,44 @@ async function assertExactSources() { } } +async function buildTargetDescriptor() { + const baselineBytes = await readFile(baselineArtifact); + if (digest(baselineBytes) !== baselineArtifactSha256) { + throw new Error("engine_node_intelligence_baseline_artifact_sha256_mismatch"); + } + const result = run("python3", [ + "-c", + [ + "import pathlib,sys,tarfile", + "p=pathlib.Path(sys.argv[1]); name=sys.argv[2]", + "with tarfile.open(p,'r:gz') as t:", + " m=t.getmember(name)", + " if not m.isfile(): raise SystemExit('member-not-file')", + " f=t.extractfile(m)", + " if f is None: raise SystemExit('member-unreadable')", + " sys.stdout.buffer.write(f.read())", + ].join("\n"), + baselineArtifact, + `payload/${descriptorRel}`, + ]); + const descriptor = JSON.parse(result.stdout); + if ( + descriptor?.schemaVersion !== "nodedc.engine-node-intelligence-transition/v1" + || descriptor?.action !== "activate" + || descriptor?.releaseId !== "2.33.2-974a9fb3492f" + || descriptor?.source?.upstreamProjectionSha256 + !== "761a874b102a938bc6018159ddacdaac71ad6ae08e9f0f8d7f3b58a0165a5131" + ) { + throw new Error("engine_node_intelligence_baseline_descriptor_mismatch"); + } + descriptor.source.upstreamProjectionSha256 = targetSha256[projectionRel]; + const text = `${JSON.stringify(descriptor, null, 2)}\n`; + if (digest(Buffer.from(text, "utf8")) !== targetSha256[descriptorRel]) { + throw new Error("engine_node_intelligence_target_descriptor_sha256_mismatch"); + } + return text; +} + async function assertFresh(path) { try { await lstat(path); @@ -136,4 +194,5 @@ function run(command, args) { if (result.status !== 0) { throw new Error(`${command}_failed:${result.stderr || result.stdout}`); } + return result; } diff --git a/infra/deploy-runner/nodedc-deploy b/infra/deploy-runner/nodedc-deploy index 8e31c8b..a97e1d6 100755 --- a/infra/deploy-runner/nodedc-deploy +++ b/infra/deploy-runner/nodedc-deploy @@ -418,12 +418,15 @@ ENGINE_PROVIDER_ROTATING_SLOT_TARGET_SHA256 = { } ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES = ( "nodedc-source/server/nodeIntelligence/upstreamProjection.js", + ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL, ) ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_PREDECESSOR_SHA256 = { "nodedc-source/server/nodeIntelligence/upstreamProjection.js": "761a874b102a938bc6018159ddacdaac71ad6ae08e9f0f8d7f3b58a0165a5131", + ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: "a9e280520bc487e0b421c548ec840eeb2b8389b6e04f764690250ea1d34cd23e", } ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256 = { "nodedc-source/server/nodeIntelligence/upstreamProjection.js": "2dfa6b4f37d9bfa8b92a8109d4060d02dd2634ceb8f7924504b83ccf3fdd1523", + ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: "a8adb8c03e1a82ff683c99a8c6482c78eca1b63422e7e7e417424363b995079e", } ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES = ( "nodedc-source/server/assets/provider-packages/v1/catalog.json", @@ -4638,6 +4641,19 @@ def validate_engine_provider_authority_diagnostics_slice(payload_dir, entries): ) if any(marker not in source for marker in required_markers): die("Engine private node validation reconciliation contract mismatch") + descriptor = read_engine_node_intelligence_descriptor( + payload_dir / ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL, + "Engine private node validation reconciliation descriptor", + ) + if ( + descriptor.get("action") != "activate" + or descriptor.get("releaseId") != "2.33.2-974a9fb3492f" + or descriptor.get("source", {}).get("upstreamProjectionSha256") + != ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256[ + "nodedc-source/server/nodeIntelligence/upstreamProjection.js" + ] + ): + die("Engine private node validation reconciliation descriptor mismatch") def validate_engine_depttrans_zone_authority_v1_slice(payload_dir, entries): diff --git a/infra/deploy-runner/test_engine_provider_authority_diagnostics.py b/infra/deploy-runner/test_engine_provider_authority_diagnostics.py index 8042aa6..7146671 100644 --- a/infra/deploy-runner/test_engine_provider_authority_diagnostics.py +++ b/infra/deploy-runner/test_engine_provider_authority_diagnostics.py @@ -34,11 +34,20 @@ RUNNER = load_runner() class EngineProviderAuthorityDiagnosticsTest(unittest.TestCase): def require_historical_target(self): + source_entries = [ + relative_path + for relative_path in RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES + if relative_path != RUNNER.ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL + ] current_sha256 = { relative_path: hashlib.sha256((ENGINE_ROOT / relative_path).read_bytes()).hexdigest() - for relative_path in RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES + for relative_path in source_entries } - if current_sha256 != RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256: + expected_sha256 = { + relative_path: RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256[relative_path] + for relative_path in source_entries + } + if current_sha256 != expected_sha256: self.skipTest("historical provider authority diagnostics source has advanced") def build(self, artifact_dir): @@ -96,14 +105,20 @@ class EngineProviderAuthorityDiagnosticsTest(unittest.TestCase): def test_preflight_requires_the_exact_deployed_predecessor(self): with tempfile.TemporaryDirectory(prefix="nodedc-provider-authority-preflight-") as directory: root = Path(directory) - relative_path = RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES[0] - path = root / relative_path - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text("predecessor\n", encoding="utf-8") - expected = RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_PREDECESSOR_SHA256[relative_path] + for relative_path in RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES: + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("predecessor\n", encoding="utf-8") + + def predecessor_sha256(path): + relative_path = path.relative_to(root).as_posix() + return RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_PREDECESSOR_SHA256[ + relative_path + ] + with ( mock.patch.object(RUNNER, "component_root", return_value=root), - mock.patch.object(RUNNER, "sha256_file", return_value=expected), + mock.patch.object(RUNNER, "sha256_file", side_effect=predecessor_sha256), mock.patch.object( RUNNER, "preflight_engine_credential_backend_runtime", @@ -123,10 +138,14 @@ class EngineProviderAuthorityDiagnosticsTest(unittest.TestCase): def test_healthchecks_dispatch_authority_diagnostics_acceptance(self): entries = RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES root = Path("/engine") - target_hash = next(iter(RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256.values())) + + def target_sha256(path): + relative_path = path.relative_to(root).as_posix() + return RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256[relative_path] + with ( mock.patch.object(RUNNER, "component_root", return_value=root), - mock.patch.object(RUNNER, "sha256_file", return_value=target_hash), + mock.patch.object(RUNNER, "sha256_file", side_effect=target_sha256), mock.patch.object(RUNNER, "healthcheck_compose_service"), mock.patch.object(RUNNER, "component_healthchecks", return_value=()), mock.patch.object( @@ -147,16 +166,23 @@ class EngineProviderAuthorityDiagnosticsTest(unittest.TestCase): def test_live_acceptance_uses_private_node_reconciliation_contract(self): self.require_historical_target() expected_live = "engine-private-node-reconciliation:exact-id-or-name:v1" - 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_provider_authority_diagnostics_runtime() + with tempfile.TemporaryDirectory(prefix="nodedc-provider-authority-live-") as directory: + root = Path(directory) + built = self.build(root / "artifact") + _manifest, _entries, payload = RUNNER.load_artifact( + Path(built["artifact"]), + root / "loaded", + ) + with ( + mock.patch.object(RUNNER, "component_root", return_value=payload), + 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_provider_authority_diagnostics_runtime() self.assertEqual(result["live"], expected_live) self.assertEqual(backend_probe.call_args.args[1], "Engine private node validation reconciliation")