diff --git a/infra/deploy-runner/build-engine-mcp-telemetry-catalog-artifact.mjs b/infra/deploy-runner/build-engine-mcp-telemetry-catalog-artifact.mjs index 55b624e..9d41407 100755 --- a/infra/deploy-runner/build-engine-mcp-telemetry-catalog-artifact.mjs +++ b/infra/deploy-runner/build-engine-mcp-telemetry-catalog-artifact.mjs @@ -14,6 +14,22 @@ const engineRoot = resolve( const artifactRoot = resolve( process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"), ); +const baselineArtifact = resolve( + here, + "../deploy-artifacts/nodedc-engine-l2-closed-loop-20260723-031.tgz", +); +const baselineArtifactSha256 = + "33103bcf8a5f020855f3a095306b94158d9a452344ac7313120b1624a745e47e"; +const nodeIntelligenceDescriptorPath = + "nodedc-source/services/node-intelligence/activation.json"; +const predecessorNodeIntelligenceDescriptorSha256 = + "63e7619c6971d02102583bb5d80d33ece293b952f113200fa0b76f0e88c2dd32"; +const targetNodeIntelligenceDescriptorSha256 = + "25ed3efd858aaf82c242dba501f0acc0c6c3dc91e8845707a4d3325750eab59f"; +const predecessorGatewaySha256 = + "4f600a2781be9118bec891fa2a6f20d7f55e0892ef2f06af24059193cebd28f4"; +const targetGatewaySha256 = + "69bfc91e913a3fad04e13aca86efb9d62f73c0c7d1f8f7200907494b29fa9e8d"; const [patchId = "", ...extra] = process.argv.slice(2); if ( extra.length @@ -31,21 +47,33 @@ const targetSha256 = Object.freeze({ "nodedc-source/server/routes/n8n.js": "903245ae363e9b9ac161498f17988a38876a0e0ac8d80f3fa1b0112ceb7fe906", "nodedc-source/server/routes/engineAgentGateway.js": - "69bfc91e913a3fad04e13aca86efb9d62f73c0c7d1f8f7200907494b29fa9e8d", + targetGatewaySha256, [descriptorPath]: "b25ab8b6e6ad8ac24614c4630cc3635abe453464466d5a72238b05e48a24d882", + [nodeIntelligenceDescriptorPath]: + targetNodeIntelligenceDescriptorSha256, }); const entries = Object.freeze(Object.keys(targetSha256)); const artifact = join(artifactRoot, `nodedc-${patchId}.tgz`); await assertFresh(artifact); await assertExactSources(); +const targetNodeIntelligenceDescriptor = + await buildTargetNodeIntelligenceDescriptor(); const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-mcp-telemetry-catalog-")); try { const payload = join(stage, "payload"); for (const relativePath of entries) { const destination = join(payload, relativePath); await mkdir(dirname(destination), { recursive: true }); + if (relativePath === nodeIntelligenceDescriptorPath) { + await writeFile(destination, targetNodeIntelligenceDescriptor, { + encoding: "utf8", + flag: "wx", + mode: 0o644, + }); + continue; + } await copyFile(join(engineRoot, relativePath), destination); } await writeFile( @@ -74,6 +102,11 @@ try { mcpTool: "engine_get_telemetry_reading_catalog", readingValuesIncluded: false, rawExecutionDataIncluded: false, + nodeIntelligenceRelease: "2.33.2-974a9fb3492f", + predecessorGatewaySha256, + targetGatewaySha256, + predecessorNodeIntelligenceDescriptorSha256, + targetNodeIntelligenceDescriptorSha256, untouched: [ "L2 graph", "n8n", @@ -92,6 +125,7 @@ try { async function assertExactSources() { for (const [relativePath, expected] of Object.entries(targetSha256)) { + if (relativePath === nodeIntelligenceDescriptorPath) continue; const sourcePath = join(engineRoot, relativePath); const info = await lstat(sourcePath); if (!info.isFile() || info.isSymbolicLink()) { @@ -147,6 +181,49 @@ async function assertExactSources() { } } +async function buildTargetNodeIntelligenceDescriptor() { + const baselineBytes = await readFile(baselineArtifact); + if (digest(baselineBytes) !== baselineArtifactSha256) { + throw new Error("engine_mcp_telemetry_catalog_baseline_artifact_mismatch"); + } + const baseline = run("tar", [ + "-xOf", + baselineArtifact, + `payload/${nodeIntelligenceDescriptorPath}`, + ]).stdout; + if ( + digest(Buffer.from(baseline, "utf8")) + !== predecessorNodeIntelligenceDescriptorSha256 + ) { + throw new Error( + "engine_mcp_telemetry_catalog_baseline_descriptor_mismatch", + ); + } + const descriptor = JSON.parse(baseline); + if ( + descriptor?.schemaVersion + !== "nodedc.engine-node-intelligence-transition/v1" + || descriptor?.action !== "activate" + || descriptor?.releaseId !== "2.33.2-974a9fb3492f" + || descriptor?.source?.gatewaySha256 !== predecessorGatewaySha256 + ) { + throw new Error( + "engine_mcp_telemetry_catalog_baseline_descriptor_contract_mismatch", + ); + } + descriptor.source.gatewaySha256 = targetGatewaySha256; + const rendered = `${JSON.stringify(descriptor, null, 2)}\n`; + if ( + digest(Buffer.from(rendered, "utf8")) + !== targetNodeIntelligenceDescriptorSha256 + ) { + throw new Error( + "engine_mcp_telemetry_catalog_target_descriptor_mismatch", + ); + } + return rendered; +} + async function assertFresh(path) { try { await lstat(path); @@ -186,4 +263,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 f0f3a98..0b6fcce 100755 --- a/infra/deploy-runner/nodedc-deploy +++ b/infra/deploy-runner/nodedc-deploy @@ -554,10 +554,12 @@ ENGINE_MCP_TELEMETRY_CATALOG_ARTIFACT_ENTRIES = ( "nodedc-source/server/routes/n8n.js", "nodedc-source/server/routes/engineAgentGateway.js", ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL, + ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL, ) ENGINE_MCP_TELEMETRY_CATALOG_PREDECESSOR_SHA256 = { "nodedc-source/server/routes/n8n.js": "1c2427c1d5830c40b1e8ae05f3d683fc39d07d7e0fe2431b0f6efbbb1d3fcb88", "nodedc-source/server/routes/engineAgentGateway.js": "4f600a2781be9118bec891fa2a6f20d7f55e0892ef2f06af24059193cebd28f4", + ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: "63e7619c6971d02102583bb5d80d33ece293b952f113200fa0b76f0e88c2dd32", } ENGINE_MCP_TELEMETRY_CATALOG_FOUNDATION_SHA256 = { ENGINE_MCP_EXECUTION_PROFILE_DECODER_DESCRIPTOR_REL: "93e431902e9bcd3b828a82ed6b42b48d939051f21bf8824dafcf2addac8a711c", @@ -566,6 +568,7 @@ ENGINE_MCP_TELEMETRY_CATALOG_TARGET_SHA256 = { "nodedc-source/server/routes/n8n.js": "903245ae363e9b9ac161498f17988a38876a0e0ac8d80f3fa1b0112ceb7fe906", "nodedc-source/server/routes/engineAgentGateway.js": "69bfc91e913a3fad04e13aca86efb9d62f73c0c7d1f8f7200907494b29fa9e8d", ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL: "b25ab8b6e6ad8ac24614c4630cc3635abe453464466d5a72238b05e48a24d882", + ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: "25ed3efd858aaf82c242dba501f0acc0c6c3dc91e8845707a4d3325750eab59f", } ENGINE_MCP_TELEMETRY_CATALOG_NEW_PATHS = ( ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL, @@ -5156,6 +5159,23 @@ def validate_engine_mcp_telemetry_catalog_slice(payload_dir, entries): ) ): die("Engine MCP telemetry catalog gateway contract mismatch") + node_intelligence_descriptor = read_engine_node_intelligence_descriptor( + payload_dir / ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL, + "Engine MCP telemetry catalog node-intelligence descriptor", + ) + if ( + node_intelligence_descriptor.get("action") != "activate" + or node_intelligence_descriptor.get("releaseId") + != ENGINE_NODE_INTELLIGENCE_RELEASE_ID + or node_intelligence_descriptor.get("source", {}).get("gatewaySha256") + != ENGINE_MCP_TELEMETRY_CATALOG_TARGET_SHA256[ + ENGINE_NODE_INTELLIGENCE_GATEWAY_REL + ] + ): + die( + "Engine MCP telemetry catalog node-intelligence attestation " + "contract mismatch" + ) def validate_engine_agent_full_grant_migration_slice(payload_dir, entries): @@ -5577,6 +5597,10 @@ def load_artifact(artifact, work_dir): 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_l2_closed_loop_slice(manifest["component"], entries) + and not is_engine_mcp_telemetry_catalog_slice( + manifest["component"], + entries, + ) and not is_engine_provider_authority_diagnostics_slice( manifest["component"], entries, @@ -9113,6 +9137,9 @@ def plan_artifact(artifact): print("engine_mcp_tool=engine_get_telemetry_reading_catalog") print("engine_mcp_reading_values_included=no") print("engine_mcp_raw_execution_data_included=no") + print("node_intelligence_descriptor=attested-gateway-successor") + print("node_intelligence_release=2.33.2-974a9fb3492f") + print("node_intelligence_image=preserved") for foundation_path, foundation_sha256 in ( mcp_telemetry_catalog_preflight["foundation_sha256"].items() ): diff --git a/infra/deploy-runner/test_engine_mcp_telemetry_catalog.py b/infra/deploy-runner/test_engine_mcp_telemetry_catalog.py index 765af63..7822f89 100644 --- a/infra/deploy-runner/test_engine_mcp_telemetry_catalog.py +++ b/infra/deploy-runner/test_engine_mcp_telemetry_catalog.py @@ -39,6 +39,8 @@ class EngineMcpTelemetryCatalogTest(unittest.TestCase): for relative_path, expected in ( RUNNER.ENGINE_MCP_TELEMETRY_CATALOG_TARGET_SHA256.items() ): + if relative_path == RUNNER.ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: + continue path = ENGINE_ROOT / relative_path if ( not path.is_file() @@ -114,6 +116,27 @@ class EngineMcpTelemetryCatalogTest(unittest.TestCase): ("http://127.0.0.1:3001/health",), ) RUNNER.validate_engine_mcp_telemetry_catalog_slice(payload, entries) + node_intelligence_descriptor = json.loads( + ( + payload / RUNNER.ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL + ).read_text(encoding="utf-8") + ) + self.assertEqual( + node_intelligence_descriptor["source"]["gatewaySha256"], + RUNNER.ENGINE_MCP_TELEMETRY_CATALOG_TARGET_SHA256[ + RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL + ], + ) + self.assertEqual( + hashlib.sha256( + ( + payload / RUNNER.ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL + ).read_bytes() + ).hexdigest(), + RUNNER.ENGINE_MCP_TELEMETRY_CATALOG_TARGET_SHA256[ + RUNNER.ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL + ], + ) with tempfile.TemporaryDirectory( prefix="nodedc-engine-mcp-telemetry-load-" ) as load_directory: @@ -238,6 +261,11 @@ class EngineMcpTelemetryCatalogTest(unittest.TestCase): plan, ) self.assertIn("engine_mcp_reading_values_included=no", plan) + self.assertIn( + "node_intelligence_descriptor=attested-gateway-successor", + plan, + ) + self.assertIn("node_intelligence_image=preserved", plan) self.assertIn("mcp_nginx=untouched", plan) self.assertIn("embedded_ai_workspace=untouched", plan) self.assertIn( @@ -298,6 +326,10 @@ class EngineMcpTelemetryCatalogTest(unittest.TestCase): ) with ( mock.patch.object(RUNNER, "component_root", return_value=ENGINE_ROOT), + mock.patch.object( + RUNNER, + "validate_engine_mcp_telemetry_catalog_slice", + ), mock.patch.object( RUNNER, "engine_backend_container_id",