fix(deploy): attest Engine telemetry gateway transition

This commit is contained in:
Codex 2026-07-23 16:24:52 +03:00
parent f97a9d924d
commit 6f9da1b677
3 changed files with 138 additions and 1 deletions

View File

@ -14,6 +14,22 @@ 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 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); const [patchId = "", ...extra] = process.argv.slice(2);
if ( if (
extra.length extra.length
@ -31,21 +47,33 @@ const targetSha256 = Object.freeze({
"nodedc-source/server/routes/n8n.js": "nodedc-source/server/routes/n8n.js":
"903245ae363e9b9ac161498f17988a38876a0e0ac8d80f3fa1b0112ceb7fe906", "903245ae363e9b9ac161498f17988a38876a0e0ac8d80f3fa1b0112ceb7fe906",
"nodedc-source/server/routes/engineAgentGateway.js": "nodedc-source/server/routes/engineAgentGateway.js":
"69bfc91e913a3fad04e13aca86efb9d62f73c0c7d1f8f7200907494b29fa9e8d", targetGatewaySha256,
[descriptorPath]: [descriptorPath]:
"b25ab8b6e6ad8ac24614c4630cc3635abe453464466d5a72238b05e48a24d882", "b25ab8b6e6ad8ac24614c4630cc3635abe453464466d5a72238b05e48a24d882",
[nodeIntelligenceDescriptorPath]:
targetNodeIntelligenceDescriptorSha256,
}); });
const entries = Object.freeze(Object.keys(targetSha256)); const entries = Object.freeze(Object.keys(targetSha256));
const artifact = join(artifactRoot, `nodedc-${patchId}.tgz`); const artifact = join(artifactRoot, `nodedc-${patchId}.tgz`);
await assertFresh(artifact); await assertFresh(artifact);
await assertExactSources(); await assertExactSources();
const targetNodeIntelligenceDescriptor =
await buildTargetNodeIntelligenceDescriptor();
const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-mcp-telemetry-catalog-")); const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-mcp-telemetry-catalog-"));
try { try {
const payload = join(stage, "payload"); const payload = join(stage, "payload");
for (const relativePath of entries) { for (const relativePath of entries) {
const destination = join(payload, relativePath); const destination = join(payload, relativePath);
await mkdir(dirname(destination), { recursive: true }); 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 copyFile(join(engineRoot, relativePath), destination);
} }
await writeFile( await writeFile(
@ -74,6 +102,11 @@ try {
mcpTool: "engine_get_telemetry_reading_catalog", mcpTool: "engine_get_telemetry_reading_catalog",
readingValuesIncluded: false, readingValuesIncluded: false,
rawExecutionDataIncluded: false, rawExecutionDataIncluded: false,
nodeIntelligenceRelease: "2.33.2-974a9fb3492f",
predecessorGatewaySha256,
targetGatewaySha256,
predecessorNodeIntelligenceDescriptorSha256,
targetNodeIntelligenceDescriptorSha256,
untouched: [ untouched: [
"L2 graph", "L2 graph",
"n8n", "n8n",
@ -92,6 +125,7 @@ try {
async function assertExactSources() { async function assertExactSources() {
for (const [relativePath, expected] of Object.entries(targetSha256)) { for (const [relativePath, expected] of Object.entries(targetSha256)) {
if (relativePath === nodeIntelligenceDescriptorPath) continue;
const sourcePath = join(engineRoot, relativePath); const sourcePath = join(engineRoot, relativePath);
const info = await lstat(sourcePath); const info = await lstat(sourcePath);
if (!info.isFile() || info.isSymbolicLink()) { 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) { async function assertFresh(path) {
try { try {
await lstat(path); await lstat(path);
@ -186,4 +263,5 @@ function run(command, args) {
if (result.status !== 0) { if (result.status !== 0) {
throw new Error(`${command}_failed:${result.stderr || result.stdout}`); throw new Error(`${command}_failed:${result.stderr || result.stdout}`);
} }
return result;
} }

View File

@ -554,10 +554,12 @@ ENGINE_MCP_TELEMETRY_CATALOG_ARTIFACT_ENTRIES = (
"nodedc-source/server/routes/n8n.js", "nodedc-source/server/routes/n8n.js",
"nodedc-source/server/routes/engineAgentGateway.js", "nodedc-source/server/routes/engineAgentGateway.js",
ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL, ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL,
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL,
) )
ENGINE_MCP_TELEMETRY_CATALOG_PREDECESSOR_SHA256 = { ENGINE_MCP_TELEMETRY_CATALOG_PREDECESSOR_SHA256 = {
"nodedc-source/server/routes/n8n.js": "1c2427c1d5830c40b1e8ae05f3d683fc39d07d7e0fe2431b0f6efbbb1d3fcb88", "nodedc-source/server/routes/n8n.js": "1c2427c1d5830c40b1e8ae05f3d683fc39d07d7e0fe2431b0f6efbbb1d3fcb88",
"nodedc-source/server/routes/engineAgentGateway.js": "4f600a2781be9118bec891fa2a6f20d7f55e0892ef2f06af24059193cebd28f4", "nodedc-source/server/routes/engineAgentGateway.js": "4f600a2781be9118bec891fa2a6f20d7f55e0892ef2f06af24059193cebd28f4",
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: "63e7619c6971d02102583bb5d80d33ece293b952f113200fa0b76f0e88c2dd32",
} }
ENGINE_MCP_TELEMETRY_CATALOG_FOUNDATION_SHA256 = { ENGINE_MCP_TELEMETRY_CATALOG_FOUNDATION_SHA256 = {
ENGINE_MCP_EXECUTION_PROFILE_DECODER_DESCRIPTOR_REL: "93e431902e9bcd3b828a82ed6b42b48d939051f21bf8824dafcf2addac8a711c", 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/n8n.js": "903245ae363e9b9ac161498f17988a38876a0e0ac8d80f3fa1b0112ceb7fe906",
"nodedc-source/server/routes/engineAgentGateway.js": "69bfc91e913a3fad04e13aca86efb9d62f73c0c7d1f8f7200907494b29fa9e8d", "nodedc-source/server/routes/engineAgentGateway.js": "69bfc91e913a3fad04e13aca86efb9d62f73c0c7d1f8f7200907494b29fa9e8d",
ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL: "b25ab8b6e6ad8ac24614c4630cc3635abe453464466d5a72238b05e48a24d882", ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL: "b25ab8b6e6ad8ac24614c4630cc3635abe453464466d5a72238b05e48a24d882",
ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: "25ed3efd858aaf82c242dba501f0acc0c6c3dc91e8845707a4d3325750eab59f",
} }
ENGINE_MCP_TELEMETRY_CATALOG_NEW_PATHS = ( ENGINE_MCP_TELEMETRY_CATALOG_NEW_PATHS = (
ENGINE_MCP_TELEMETRY_CATALOG_DESCRIPTOR_REL, 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") 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): 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_ontology_sdk_slice(manifest["component"], entries)
and not is_engine_mcp_autonomy_provider_v5_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_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( and not is_engine_provider_authority_diagnostics_slice(
manifest["component"], manifest["component"],
entries, entries,
@ -9113,6 +9137,9 @@ def plan_artifact(artifact):
print("engine_mcp_tool=engine_get_telemetry_reading_catalog") print("engine_mcp_tool=engine_get_telemetry_reading_catalog")
print("engine_mcp_reading_values_included=no") print("engine_mcp_reading_values_included=no")
print("engine_mcp_raw_execution_data_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 ( for foundation_path, foundation_sha256 in (
mcp_telemetry_catalog_preflight["foundation_sha256"].items() mcp_telemetry_catalog_preflight["foundation_sha256"].items()
): ):

View File

@ -39,6 +39,8 @@ class EngineMcpTelemetryCatalogTest(unittest.TestCase):
for relative_path, expected in ( for relative_path, expected in (
RUNNER.ENGINE_MCP_TELEMETRY_CATALOG_TARGET_SHA256.items() RUNNER.ENGINE_MCP_TELEMETRY_CATALOG_TARGET_SHA256.items()
): ):
if relative_path == RUNNER.ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL:
continue
path = ENGINE_ROOT / relative_path path = ENGINE_ROOT / relative_path
if ( if (
not path.is_file() not path.is_file()
@ -114,6 +116,27 @@ class EngineMcpTelemetryCatalogTest(unittest.TestCase):
("http://127.0.0.1:3001/health",), ("http://127.0.0.1:3001/health",),
) )
RUNNER.validate_engine_mcp_telemetry_catalog_slice(payload, entries) 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( with tempfile.TemporaryDirectory(
prefix="nodedc-engine-mcp-telemetry-load-" prefix="nodedc-engine-mcp-telemetry-load-"
) as load_directory: ) as load_directory:
@ -238,6 +261,11 @@ class EngineMcpTelemetryCatalogTest(unittest.TestCase):
plan, plan,
) )
self.assertIn("engine_mcp_reading_values_included=no", 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("mcp_nginx=untouched", plan)
self.assertIn("embedded_ai_workspace=untouched", plan) self.assertIn("embedded_ai_workspace=untouched", plan)
self.assertIn( self.assertIn(
@ -298,6 +326,10 @@ class EngineMcpTelemetryCatalogTest(unittest.TestCase):
) )
with ( with (
mock.patch.object(RUNNER, "component_root", return_value=ENGINE_ROOT), mock.patch.object(RUNNER, "component_root", return_value=ENGINE_ROOT),
mock.patch.object(
RUNNER,
"validate_engine_mcp_telemetry_catalog_slice",
),
mock.patch.object( mock.patch.object(
RUNNER, RUNNER,
"engine_backend_container_id", "engine_backend_container_id",