From e9e03143cd07d1adb8f179b5935d934af593cc0b Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 25 Jul 2026 15:39:23 +0300 Subject: [PATCH] feat(engine): register MCP profiles and provider transitions --- ...ine-l1-agent-init-concurrency-artifact.mjs | 276 +++++ ...p-gelios-units-items-envelope-artifact.mjs | 230 ++++ ...registered-execution-profiles-artifact.mjs | 279 +++++ .../generate-engine-gelios-v12-catalogs.mjs | 33 + .../generate-engine-gelios-v13-catalogs.mjs | 195 +++ ...engine-gelios-v12-units-items-envelope.mjs | 208 ++++ infra/deploy-runner/nodedc-deploy | 1050 +++++++++++++++++ ...gine_l1_agent_init_concurrency_artifact.py | 130 ++ ..._engine_mcp_gelios_units_items_envelope.py | 284 +++++ ...ngine_mcp_registered_execution_profiles.py | 316 +++++ .../test_engine_node_intelligence.py | 6 +- .../providers/gelios/index.mjs | 1 + .../providers/gelios/v12/package.mjs | 32 +- .../providers/gelios/v13/index.mjs | 10 + .../providers/gelios/v13/package.mjs | 318 +++++ .../src/l2-execution-plan.mjs | 3 +- .../src/provider-package.mjs | 102 ++ .../test/l2-execution-plan.test.mjs | 6 +- .../test/provider-package.test.mjs | 49 +- .../fleet.units.contacts.current.v2.json | 45 + .../test/definitions.test.mjs | 28 +- 21 files changed, 3573 insertions(+), 28 deletions(-) create mode 100644 infra/deploy-runner/build-engine-l1-agent-init-concurrency-artifact.mjs create mode 100644 infra/deploy-runner/build-engine-mcp-gelios-units-items-envelope-artifact.mjs create mode 100644 infra/deploy-runner/build-engine-mcp-registered-execution-profiles-artifact.mjs create mode 100644 infra/deploy-runner/generate-engine-gelios-v13-catalogs.mjs create mode 100644 infra/deploy-runner/migrate-engine-gelios-v12-units-items-envelope.mjs create mode 100644 infra/deploy-runner/test_engine_l1_agent_init_concurrency_artifact.py create mode 100644 infra/deploy-runner/test_engine_mcp_gelios_units_items_envelope.py create mode 100644 infra/deploy-runner/test_engine_mcp_registered_execution_profiles.py create mode 100644 packages/external-provider-contract/providers/gelios/v13/index.mjs create mode 100644 packages/external-provider-contract/providers/gelios/v13/package.mjs create mode 100644 services/external-data-plane/definitions/fleet.units.contacts.current.v2.json diff --git a/infra/deploy-runner/build-engine-l1-agent-init-concurrency-artifact.mjs b/infra/deploy-runner/build-engine-l1-agent-init-concurrency-artifact.mjs new file mode 100644 index 0000000..54f5fc6 --- /dev/null +++ b/infra/deploy-runner/build-engine-l1-agent-init-concurrency-artifact.mjs @@ -0,0 +1,276 @@ +#!/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 scriptDir = dirname(fileURLToPath(import.meta.url)); +const workspaceRoot = resolve(scriptDir, "../../.."); +const engineRoot = resolve( + process.env.NODEDC_ENGINE_SOURCE_ROOT + || join(workspaceRoot, "NODEDC_ENGINE_INFRA"), +); +const artifactDir = resolve( + process.env.NODEDC_DEPLOY_ARTIFACT_DIR + || resolve(scriptDir, "../deploy-artifacts"), +); +const [patchId = "engine-l1-agent-init-concurrency-20260724-042", ...extra] = + process.argv.slice(2); + +if ( + extra.length + || !/^engine-l1-agent-init-concurrency-\d{8}-\d{3}$/.test(patchId) +) { + throw new Error( + "usage: build-engine-l1-agent-init-concurrency-artifact.mjs " + + "[engine-l1-agent-init-concurrency-YYYYMMDD-NNN]", + ); +} + +const sourceCommit = "c4077fe0d8d824fa58616eeb986dc78ca43070da"; +const predecessorSha256 = Object.freeze({ + "nodedc-source/src/App.tsx": + "b3e61a89330b774f309660208d05143d299ab582f18765c1c14e2122a62c4d5e", + "nodedc-source/src/driveinspector/nodes/N8n.definition.ts": + "a4890cca12b269643ec3255c2fc6b7e2be96aa042ffc6087772e63cbf8323c3a", + "nodedc-source/src/nodes/N8nNode.tsx": + "a125f7cde66085008dafe5a340a1143094c59769573815851ce6c8adfe4c0220", + "nodedc-source/src/store.ts": + "2128cd582a947582bb7736792c3a71551712262ef04882c189220ec79f16fb8e", + "nodedc-source/dist/index.html": + "90d72f8790dc8cf951066b1210447c84d640373117bae23f3a4cb3227fb96d6d", + "nodedc-source/dist/assets/index-CqvJfRRS.js": + "06e6c23b03ea2ba8a4890e1f1714fd08ebaf18d735834200af6ab430af360ca8", +}); +const targetSha256 = Object.freeze({ + "nodedc-source/src/App.tsx": + "fc580dc3a1a97393af81dd75c54ae8b37db1f6fda8364042198493493b15f2c2", + "nodedc-source/src/driveinspector/nodes/N8n.definition.ts": + "945839541547b2fd02b5424954e00483d6c76f82a333d6a7a652ccd1b74d04cf", + "nodedc-source/src/nodes/N8nNode.tsx": + "1130e355d86e06a18df775b2d3517bfdbf98fdc494ca707d23c78648fbfd3cb4", + "nodedc-source/src/store.ts": + "46884488254c16cd3af9b25556dd0db4f5a3a20047abdb496db831f204e40673", + "nodedc-source/src/utils/workflowWriteQueue.js": + "41f73ccb5e1dafbdae501b7c0b9fa5c47fdbd52564061b9c4ca6ca1c75ceccdf", + "nodedc-source/dist/index.html": + "1554e2418aead13b56328c5721dd677400d84ee087559a88fe60b295b7ff5412", + "nodedc-source/dist/assets/index-r1_2ECzJ.js": + "0d25dd843da7078b8dbad5cb4b78724bf1ca8aa067e9d0add1e09ce8f421b822", +}); +const entries = Object.freeze(Object.keys(targetSha256)); +const artifact = join(artifactDir, `nodedc-${patchId}.tgz`); +const checksum = `${artifact}.sha256`; + +assertSourceCommit(); +await assertPredecessorSources(); +await assertExactTargets(); +await assertRuntimeContract(); +await assertFresh(artifact); + +const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-l1-agent-init-")); +try { + for (const relativePath of entries) { + const destination = join(stage, "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(artifactDir, { recursive: true }); + run("python3", ["-c", canonicalTarScript(), artifact, stage]); + const sha256 = digest(await readFile(artifact)); + await writeFile(checksum, `${sha256} ${artifact.split("/").at(-1)}\n`, { + encoding: "utf8", + flag: "wx", + mode: 0o644, + }); + console.log(JSON.stringify({ + ok: true, + patchId, + artifact, + checksum, + sha256, + sourceCommit, + predecessorSha256, + targetSha256, + entries, + services: ["nodedc-backend", "app"], + healthchecks: [ + "http://127.0.0.1:8080/", + "http://127.0.0.1:3001/health", + ], + concurrencyContract: "per-workflow-fifo-latest-revision-v1", + n8nCoreChanged: false, + untouched: [ + "n8n runtime", + "L1 workflow data", + "L2 workflow data", + "credentials", + "databases", + "MCP catalogs", + ], + }, null, 2)); +} finally { + await rm(stage, { recursive: true, force: true }); +} + +function assertSourceCommit() { + const actual = run("git", ["-C", engineRoot, "rev-parse", "HEAD"]).stdout.trim(); + if (actual !== sourceCommit) { + throw new Error( + `engine_l1_agent_init_source_commit_mismatch:` + + `expected=${sourceCommit}:actual=${actual}`, + ); + } +} + +async function assertPredecessorSources() { + for (const [relativePath, expected] of Object.entries(predecessorSha256)) { + const result = run("git", [ + "-C", + engineRoot, + "show", + `${sourceCommit}:${relativePath}`, + ]); + const actual = digest(result.stdoutBuffer); + if (actual !== expected) { + throw new Error( + `engine_l1_agent_init_predecessor_mismatch:${relativePath}:` + + `expected=${expected}:actual=${actual}`, + ); + } + } + const newFile = "nodedc-source/src/utils/workflowWriteQueue.js"; + const probe = spawnSync( + "git", + ["-C", engineRoot, "cat-file", "-e", `${sourceCommit}:${newFile}`], + { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, + ); + if (probe.status === 0) { + throw new Error("engine_l1_agent_init_queue_not_new"); + } +} + +async function assertExactTargets() { + for (const [relativePath, expected] of Object.entries(targetSha256)) { + const source = join(engineRoot, relativePath); + const info = await lstat(source); + if (!info.isFile() || info.isSymbolicLink()) { + throw new Error(`engine_l1_agent_init_source_unsafe:${relativePath}`); + } + const actual = digest(await readFile(source)); + if (actual !== expected) { + throw new Error( + `engine_l1_agent_init_target_mismatch:${relativePath}:` + + `expected=${expected}:actual=${actual}`, + ); + } + } +} + +async function assertRuntimeContract() { + const app = await readFile(join(engineRoot, "nodedc-source/src/App.tsx"), "utf8"); + const definition = await readFile( + join(engineRoot, "nodedc-source/src/driveinspector/nodes/N8n.definition.ts"), + "utf8", + ); + const store = await readFile(join(engineRoot, "nodedc-source/src/store.ts"), "utf8"); + const node = await readFile( + join(engineRoot, "nodedc-source/src/nodes/N8nNode.tsx"), + "utf8", + ); + const queue = await readFile( + join(engineRoot, "nodedc-source/src/utils/workflowWriteQueue.js"), + "utf8", + ); + const index = await readFile( + join(engineRoot, "nodedc-source/dist/index.html"), + "utf8", + ); + + if ( + !app.includes("createWorkflowWriteQueue") + || !app.includes("putExistingWorkflow") + || !app.includes("expectedRevision: currentWorkflowRevisionRef.current") + || !queue.includes("previous.catch(() => undefined).then(() => operation())") + || !definition.includes("nodeName: 'NDC'") + || !definition.includes("default: 'NDC'") + || !definition.includes("data?.title || data?.nodeName || 'NDC'") + || !store.includes("nodeName: 'NDC'") + || !node.includes("data?.nodeName ?? 'NDC'") + || !index.includes("/assets/index-r1_2ECzJ.js") + || /gelios|robot2b/i.test(queue) + ) { + throw new Error("engine_l1_agent_init_runtime_contract_invalid"); + } +} + +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: null, + stdio: ["ignore", "pipe", "pipe"], + maxBuffer: 128 * 1024 * 1024, + }); + if (result.status !== 0) { + throw new Error( + `${command}_failed:${Buffer.from(result.stderr || "").toString("utf8")}` + + `${Buffer.from(result.stdout || "").toString("utf8")}`, + ); + } + return { + stdoutBuffer: Buffer.from(result.stdout || ""), + stderrBuffer: Buffer.from(result.stderr || ""), + stdout: Buffer.from(result.stdout || "").toString("utf8"), + stderr: Buffer.from(result.stderr || "").toString("utf8"), + }; +} diff --git a/infra/deploy-runner/build-engine-mcp-gelios-units-items-envelope-artifact.mjs b/infra/deploy-runner/build-engine-mcp-gelios-units-items-envelope-artifact.mjs new file mode 100644 index 0000000..0fa9aa5 --- /dev/null +++ b/infra/deploy-runner/build-engine-mcp-gelios-units-items-envelope-artifact.mjs @@ -0,0 +1,230 @@ +#!/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 scriptDir = dirname(fileURLToPath(import.meta.url)); +const workspaceRoot = resolve(scriptDir, "../../.."); +const engineRoot = resolve( + process.env.NODEDC_ENGINE_SOURCE_ROOT + || join(workspaceRoot, "NODEDC_ENGINE_INFRA"), +); +const artifactDir = resolve( + process.env.NODEDC_DEPLOY_ARTIFACT_DIR + || resolve(scriptDir, "../deploy-artifacts"), +); +const [ + patchId = "engine-mcp-gelios-units-items-envelope-20260724-049", + ...extra +] = process.argv.slice(2); + +if ( + extra.length + || !/^engine-mcp-gelios-units-items-envelope-\d{8}-\d{3}$/.test(patchId) +) { + throw new Error( + "usage: build-engine-mcp-gelios-units-items-envelope-artifact.mjs " + + "[engine-mcp-gelios-units-items-envelope-YYYYMMDD-NNN]", + ); +} + +const expectedSha256 = Object.freeze({ + "nodedc-source/server/assets/execution-plans/v1/catalog.json": + "d0cc95fa5e55bda315947fdb13bd8a0d8931b9484677e4c4cc0dcb0e9614e215", + "nodedc-source/server/assets/provider-packages/v1/catalog.json": + "1f1b866b6f837bffc6d529dfffd8bfa9c87495bfcd9ae3515a79765143d94b1d", + "nodedc-source/server/deployTransitions/geliosUnitsItemsEnvelopeV12Patch1.json": + "7a1594573e98342d3a3edcc0dc4f663f06b30ca24cc20adb5f36d50591066d96", +}); +const files = Object.freeze(Object.keys(expectedSha256)); +const artifact = join(artifactDir, `nodedc-${patchId}.tgz`); +const checksum = `${artifact}.sha256`; +const stage = await mkdtemp( + join(tmpdir(), "nodedc-engine-gelios-units-items-envelope-"), +); + +await assertFresh(artifact); +await assertExactSources(); + +try { + for (const relativePath of files) { + const destination = join(stage, "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`, + "utf8", + ); + await writeFile(join(stage, "files.txt"), `${files.join("\n")}\n`, "utf8"); + await mkdir(artifactDir, { recursive: true }); + run("python3", ["-c", canonicalTarScript(), artifact, stage]); + const sha256 = digest(await readFile(artifact)); + await writeFile( + checksum, + `${sha256} ${artifact.split("/").at(-1)}\n`, + "utf8", + ); + console.log(JSON.stringify({ + ok: true, + patchId, + artifact, + checksum, + sha256, + services: ["nodedc-backend"], + transition: + "registered-profiles-v2-to-gelios-v12.0.1-units-items-envelope", + mcpVersion: "0.12.0", + providerPackage: { + id: "gelios.provider.v12", + predecessorVersion: "12.0.0", + targetVersion: "12.0.1", + }, + registeredProfile: "gelios.units.profile.cold.v1", + responseCollectionPath: "items", + existingMaterializerReused: true, + compilerVersion: "1.4.0", + engineRuntimeCodeChanged: false, + n8nCoreChanged: false, + l1Changed: false, + l2GraphChanged: false, + engineUiChanged: false, + databasesChanged: false, + credentialsChanged: false, + foundryChanged: false, + ontologyChanged: false, + providerEndpointsChanged: false, + files, + }, null, 2)); +} finally { + await rm(stage, { recursive: true, force: true }); +} + +async function assertExactSources() { + for (const [relativePath, expected] of Object.entries(expectedSha256)) { + const sourcePath = join(engineRoot, relativePath); + const info = await lstat(sourcePath); + if (!info.isFile() || info.isSymbolicLink()) { + throw new Error( + `engine_gelios_units_items_source_unsafe:${relativePath}`, + ); + } + const actual = digest(await readFile(sourcePath)); + if (actual !== expected) { + throw new Error( + `engine_gelios_units_items_target_mismatch:${relativePath}:` + + `expected=${expected}:actual=${actual}`, + ); + } + } + + const execution = JSON.parse(await readFile( + join( + engineRoot, + "nodedc-source/server/assets/execution-plans/v1/catalog.json", + ), + "utf8", + )); + const security = JSON.parse(await readFile( + join( + engineRoot, + "nodedc-source/server/assets/provider-packages/v1/catalog.json", + ), + "utf8", + )); + const descriptor = JSON.parse(await readFile( + join( + engineRoot, + "nodedc-source/server/deployTransitions/" + + "geliosUnitsItemsEnvelopeV12Patch1.json", + ), + "utf8", + )); + const providerPackage = execution.packages.find( + (item) => item.id === "gelios.provider.v12", + ); + const profile = providerPackage?.profiles.find( + (item) => item.id === "gelios.units.profile.cold.v1", + ); + const extract = profile?.executionPlanTemplate?.steps.find( + (step) => ( + step.kind === "extract_items" + && step.config?.capabilityId === "gelios.units.current.read" + ), + ); + const securityPackage = security.packages.find( + (item) => item.id === "gelios.provider.v12", + ); + if ( + providerPackage?.version !== "12.0.1" + || securityPackage?.version !== "12.0.1" + || profile?.dataProductId !== "fleet.units.profile.current.v1" + || profile?.executionPlanTemplate?.compilerVersion !== "1.4.0" + || extract?.config?.response?.collectionPaths?.[0] !== "items" + || descriptor?.id + !== "engine-mcp-gelios-units-items-envelope-v12-patch1" + || descriptor?.predecessor + !== "engine-mcp-registered-execution-profiles-v2" + || descriptor?.mcpVersion !== "0.12.0" + || descriptor?.registeredProfile?.materializer + !== "existing-immutable-two-phase" + || Object.values(descriptor?.boundaries || {}).some(Boolean) + ) { + throw new Error("engine_gelios_units_items_boundary_invalid"); + } +} + +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, + }); + if (result.status !== 0) { + throw new Error(`${command}_failed:${result.stderr || result.stdout}`); + } +} diff --git a/infra/deploy-runner/build-engine-mcp-registered-execution-profiles-artifact.mjs b/infra/deploy-runner/build-engine-mcp-registered-execution-profiles-artifact.mjs new file mode 100644 index 0000000..a9ab76c --- /dev/null +++ b/infra/deploy-runner/build-engine-mcp-registered-execution-profiles-artifact.mjs @@ -0,0 +1,279 @@ +#!/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 scriptDir = dirname(fileURLToPath(import.meta.url)); +const workspaceRoot = resolve(scriptDir, "../../.."); +const engineRoot = resolve( + process.env.NODEDC_ENGINE_SOURCE_ROOT + || join(workspaceRoot, "NODEDC_ENGINE_INFRA"), +); +const artifactDir = resolve( + process.env.NODEDC_DEPLOY_ARTIFACT_DIR + || resolve(scriptDir, "../deploy-artifacts"), +); +const [ + patchId = "engine-mcp-registered-execution-profiles-20260724-048", + ...extra +] = process.argv.slice(2); + +if ( + extra.length + || !/^engine-mcp-registered-execution-profiles-\d{8}-\d{3}$/.test(patchId) +) { + throw new Error( + "usage: build-engine-mcp-registered-execution-profiles-artifact.mjs " + + "[engine-mcp-registered-execution-profiles-YYYYMMDD-NNN]", + ); +} + +const expectedSha256 = Object.freeze({ + "nodedc-source/server/assets/execution-plans/v1/catalog.json": + "940510499a9260dbff718b7b1f96f72f8e9ac593b460805af3007731578a7668", + "nodedc-source/server/l2ExecutionPlan/catalog.js": + "fec56b154ae483ad21bbaaeb4c55707bffce223c27874ac135ce58a9203259a0", + "nodedc-source/server/l2ExecutionPlan/registeredProfiles.js": + "3c521c1652c61c9d757197c76e053fd3ec602e13ef7f6a4856835ecc1a8a61d1", + "nodedc-source/server/routes/engineAgentGateway.js": + "8f04edc11251de86b825351c92338be9537207887cf802474b6b6d8c3cce4077", + "nodedc-source/services/node-intelligence/activation.json": + "3d72709e40b79c01a62a6af4bde911fca4f609d5ac487d284cf97fd8ebd73686", + "nodedc-source/server/deployTransitions/registeredExecutionProfilesV2.json": + "1db523f035a47c6b00b41b26efe1390ad2d24acece9d30dd039eff56626e934d", +}); +const files = Object.freeze(Object.keys(expectedSha256)); +const artifact = join(artifactDir, `nodedc-${patchId}.tgz`); +const checksum = `${artifact}.sha256`; +const stage = await mkdtemp( + join(tmpdir(), "nodedc-engine-registered-execution-profiles-"), +); + +await assertFresh(artifact); +await assertExactSources(); + +try { + for (const relativePath of files) { + const destination = join(stage, "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`, + "utf8", + ); + await writeFile(join(stage, "files.txt"), `${files.join("\n")}\n`, "utf8"); + await mkdir(artifactDir, { recursive: true }); + run("python3", ["-c", canonicalTarScript(), artifact, stage]); + const sha256 = digest(await readFile(artifact)); + await writeFile( + checksum, + `${sha256} ${artifact.split("/").at(-1)}\n`, + "utf8", + ); + console.log(JSON.stringify({ + ok: true, + patchId, + artifact, + checksum, + sha256, + services: ["nodedc-backend"], + mcpVersion: "0.12.0", + transition: + "gelios-items-envelope-v12-to-registered-profiles-v2-attested", + tools: [ + "engine_list_l2_execution_profiles", + "engine_plan_registered_l2_execution", + ], + registeredProfiles: 6, + existingMaterializerReused: true, + n8nCoreChanged: false, + l1Changed: false, + l2GraphChanged: false, + engineUiChanged: false, + databasesChanged: false, + credentialsChanged: false, + foundryChanged: false, + ontologyChanged: false, + nodeIntelligenceAttestationChanged: true, + nodeIntelligenceImageChanged: false, + nodeIntelligenceSourceChanged: false, + rawProviderValuesIncluded: false, + files, + }, null, 2)); +} finally { + await rm(stage, { recursive: true, force: true }); +} + +async function assertExactSources() { + for (const [relativePath, expected] of Object.entries(expectedSha256)) { + const sourcePath = join(engineRoot, relativePath); + const info = await lstat(sourcePath); + if (!info.isFile() || info.isSymbolicLink()) { + throw new Error( + `engine_registered_execution_profiles_source_unsafe:${relativePath}`, + ); + } + const actual = digest(await readFile(sourcePath)); + if (actual !== expected) { + throw new Error( + `engine_registered_execution_profiles_target_mismatch:${relativePath}:` + + `expected=${expected}:actual=${actual}`, + ); + } + } + + const catalog = JSON.parse(await readFile( + join( + engineRoot, + "nodedc-source/server/assets/execution-plans/v1/catalog.json", + ), + "utf8", + )); + const descriptor = JSON.parse(await readFile( + join( + engineRoot, + "nodedc-source/server/deployTransitions/" + + "registeredExecutionProfilesV2.json", + ), + "utf8", + )); + const registered = catalog.packages.flatMap( + (providerPackage) => (providerPackage.profiles || []).filter( + (profile) => profile.executionPlanTemplate, + ), + ); + if ( + registered.length !== 6 + || registered.some((profile) => ( + !profile.dataClass + || !profile.cadence + || profile.executionPlanTemplate?.compilerVersion !== "1.4.0" + || profile.executionPlanTemplate?.connection?.collectionProfileId + !== profile.id + )) + || descriptor?.id !== "engine-mcp-registered-execution-profiles-v2" + || descriptor?.predecessor !== "engine-mcp-gelios-items-envelope-v12" + || descriptor?.mcpVersion !== "0.12.0" + || descriptor?.tools?.apply + !== "engine_apply_l2_execution_plan_materialization" + || descriptor?.nodeIntelligenceAttestation?.targetGatewaySha256 + !== expectedSha256[ + "nodedc-source/server/routes/engineAgentGateway.js" + ] + || descriptor?.nodeIntelligenceAttestation?.sidecarImageChanged !== false + || descriptor?.nodeIntelligenceAttestation + ?.nodeIntelligenceSourceChanged !== false + || descriptor?.boundaries?.nodeIntelligenceAttestationChanged !== true + || Object.entries(descriptor?.boundaries || {}).some( + ([key, value]) => ( + key !== "nodeIntelligenceAttestationChanged" && value !== false + ), + ) + ) { + throw new Error("engine_registered_execution_profiles_boundary_invalid"); + } + + const gateway = await readFile( + join(engineRoot, "nodedc-source/server/routes/engineAgentGateway.js"), + "utf8", + ); + const resolver = await readFile( + join( + engineRoot, + "nodedc-source/server/l2ExecutionPlan/registeredProfiles.js", + ), + "utf8", + ); + const nodeIntelligence = JSON.parse(await readFile( + join( + engineRoot, + "nodedc-source/services/node-intelligence/activation.json", + ), + "utf8", + )); + if ( + nodeIntelligence?.releaseId !== "2.33.2-974a9fb3492f" + || nodeIntelligence?.source?.gatewaySha256 + !== expectedSha256[ + "nodedc-source/server/routes/engineAgentGateway.js" + ] + ) { + throw new Error( + "engine_registered_execution_profiles_attestation_invalid", + ); + } + for (const marker of [ + "engine_list_l2_execution_profiles", + "engine_plan_registered_l2_execution", + "const ENGINE_AGENT_MCP_VERSION = '0.12.0'", + ]) { + if (!gateway.includes(marker)) { + throw new Error( + `engine_registered_execution_profiles_gateway_marker_missing:${marker}`, + ); + } + } + if ( + !resolver.includes("registered_execution_profile_ref_scope_denied") + || !resolver.includes("targetBoundPlan") + || /gelios|robot2b/i.test(resolver) + ) { + throw new Error("engine_registered_execution_profiles_resolver_invalid"); + } +} + +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, + }); + if (result.status !== 0) { + throw new Error(`${command}_failed:${result.stderr || result.stdout}`); + } +} diff --git a/infra/deploy-runner/generate-engine-gelios-v12-catalogs.mjs b/infra/deploy-runner/generate-engine-gelios-v12-catalogs.mjs index 78c6ad9..5f3178e 100644 --- a/infra/deploy-runner/generate-engine-gelios-v12-catalogs.mjs +++ b/infra/deploy-runner/generate-engine-gelios-v12-catalogs.mjs @@ -12,6 +12,33 @@ import { geliosTelemetryFieldRegistryV5, } from "../../packages/external-provider-contract/providers/gelios/v12/index.mjs"; +const PUBLIC_PROFILE_METADATA = Object.freeze({ + "gelios.positions.current.realtime.v7": { + cadence: "hot", + dataClass: "operational", + }, + "gelios.positions.current.manual.v7": { + cadence: "on_demand", + dataClass: "operational", + }, + "gelios.geozones.current.realtime.v1": { + cadence: "cold", + dataClass: "operational", + }, + "gelios.units.profile.cold.v1": { + cadence: "cold", + dataClass: "operational", + }, + "gelios.units.contacts.cold.v1": { + cadence: "cold", + dataClass: "restricted", + }, + "gelios.units.identity.warm.v1": { + cadence: "warm", + dataClass: "restricted", + }, +}); + const engineRoot = resolve( process.env.NODEDC_ENGINE_SOURCE_ROOT || "../NODEDC_ENGINE_INFRA", ); @@ -72,9 +99,14 @@ function executionCatalogEntry() { : {}, ); const { packageDigest: _packageDigest, ...artifacts } = plan.artifacts; + const publicMetadata = PUBLIC_PROFILE_METADATA[profile.id]; + if (!publicMetadata) { + throw new Error(`gelios_v12_public_profile_metadata_missing:${profile.id}`); + } return { id: profile.id, dataProductId: profile.dataProductId, + ...publicMetadata, capabilityIds: [...profile.capabilityIds], stepSignatures: plan.steps.map((step) => ({ id: step.id, @@ -87,6 +119,7 @@ function executionCatalogEntry() { ...(step.config.nodeType ? { nodeType: step.config.nodeType } : {}), })), artifacts, + executionPlanTemplate: plan, }; }); return { diff --git a/infra/deploy-runner/generate-engine-gelios-v13-catalogs.mjs b/infra/deploy-runner/generate-engine-gelios-v13-catalogs.mjs new file mode 100644 index 0000000..6031dc1 --- /dev/null +++ b/infra/deploy-runner/generate-engine-gelios-v13-catalogs.mjs @@ -0,0 +1,195 @@ +#!/usr/bin/env node +import { createHash } from "node:crypto"; +import { readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +import { + compileL2ExecutionPlan, + instantiateL2Connection, +} from "../../packages/external-provider-contract/src/index.mjs"; +import { + GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID, + geliosProviderPackageV13, +} from "../../packages/external-provider-contract/providers/gelios/v13/index.mjs"; + +const engineRoot = resolve( + process.env.NODEDC_ENGINE_SOURCE_ROOT || "../NODEDC_ENGINE_INFRA", +); +const executionCatalogPath = resolve( + engineRoot, + "nodedc-source/server/assets/execution-plans/v1/catalog.json", +); +const securityCatalogPath = resolve( + engineRoot, + "nodedc-source/server/assets/provider-packages/v1/catalog.json", +); + +const executionCatalog = JSON.parse( + await readFile(executionCatalogPath, "utf8"), +); +for (const value of ["1.5.0"]) { + if (!executionCatalog.runtime.compilerVersions.includes(value)) { + executionCatalog.runtime.compilerVersions.push(value); + } +} +for (const value of ["bounded_response_lookup"]) { + if (!executionCatalog.runtime.derivationKinds.includes(value)) { + executionCatalog.runtime.derivationKinds.push(value); + } +} +for (const value of ["response.lookup.first", "response.lookup.list"]) { + if (!executionCatalog.runtime.ruleIds.includes(value)) { + executionCatalog.runtime.ruleIds.push(value); + } +} +executionCatalog.runtime.compilerVersions.sort(); +executionCatalog.runtime.derivationKinds.sort(); +executionCatalog.runtime.ruleIds.sort(); +executionCatalog.packages = executionCatalog.packages.filter( + (providerPackage) => providerPackage.id !== geliosProviderPackageV13.id, +); +executionCatalog.packages.push(executionCatalogEntry()); +await writeFile(executionCatalogPath, `${JSON.stringify(executionCatalog, null, 2)}\n`); + +const securityCatalog = JSON.parse( + await readFile(securityCatalogPath, "utf8"), +); +securityCatalog.packages = securityCatalog.packages.filter( + (providerPackage) => providerPackage.id !== geliosProviderPackageV13.id, +); +securityCatalog.packages.push(securityCatalogEntry()); +await writeFile(securityCatalogPath, `${JSON.stringify(securityCatalog, null, 2)}\n`); + +console.log(JSON.stringify({ + ok: true, + providerPackage: geliosProviderPackageV13.id, + executionCatalogPath, + securityCatalogPath, + historicalExecutionPackagePreserved: executionCatalog.packages.some( + (providerPackage) => providerPackage.id === "gelios.provider.v12", + ), + historicalSecurityAuthorityPreserved: securityCatalog.packages.some( + (providerPackage) => providerPackage.id === "gelios.provider.v12", + ), + activeContactsAuthority: geliosProviderPackageV13.id, +}, null, 2)); + +function selectedProfiles() { + return geliosProviderPackageV13.collectionProfiles.filter( + (profile) => profile.dataProductId === GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID, + ); +} + +function selectedCapabilityIds() { + return new Set(selectedProfiles().flatMap((profile) => profile.capabilityIds)); +} + +function executionCatalogEntry() { + const profiles = selectedProfiles().map((profile) => { + const connection = instantiateL2Connection(geliosProviderPackageV13, { + tenantId: "tenant-catalog-build", + connectionId: `catalog-${profile.id.replaceAll(".", "-")}`, + collectionProfileId: profile.id, + providerCredentialRef: "ndc-credref:catalog-build-provider-v13", + }); + const plan = compileL2ExecutionPlan(geliosProviderPackageV13, connection); + const { packageDigest: _packageDigest, ...artifacts } = plan.artifacts; + return { + id: profile.id, + dataProductId: profile.dataProductId, + capabilityIds: [...profile.capabilityIds], + stepSignatures: plan.steps.map((step) => ({ + id: step.id, + kind: step.kind, + ...(step.config.capabilityId ? { capabilityId: step.config.capabilityId } : {}), + ...(step.config.mappingContractId ? { + mappingContractId: step.config.mappingContractId, + } : {}), + ...(step.config.dataProductId ? { dataProductId: step.config.dataProductId } : {}), + ...(step.config.nodeType ? { nodeType: step.config.nodeType } : {}), + })), + artifacts, + }; + }); + const capabilityIds = selectedCapabilityIds(); + return { + id: geliosProviderPackageV13.id, + providerId: geliosProviderPackageV13.providerId, + version: geliosProviderPackageV13.version, + contractDigest: canonicalDigest(geliosProviderPackageV13), + providerCredential: { + authModeId: "gelios.rest-rotating-bearer.v3", + credentialType: "ndcProviderRotatingAccessApi", + }, + publisher: { + nodeType: "n8n-nodes-ndc.ndcDataProductPublish", + credentialType: "ndcDataProductWriterApi", + }, + capabilities: geliosProviderPackageV13.capabilities + .filter((capability) => capabilityIds.has(capability.id)) + .map((capability) => ({ + id: capability.id, + contractDigest: canonicalDigest(capability), + requestDigest: canonicalDigest(capability.request), + method: capability.request.method, + url: requestUrl(capability.request), + })), + profiles, + }; +} + +function securityCatalogEntry() { + const productsByCapability = new Map( + geliosProviderPackageV13.capabilities.map((capability) => [capability.id, new Set()]), + ); + for (const profile of selectedProfiles()) { + for (const capabilityId of profile.capabilityIds) { + productsByCapability.get(capabilityId)?.add(profile.dataProductId); + } + } + return { + id: geliosProviderPackageV13.id, + version: geliosProviderPackageV13.version, + providerId: geliosProviderPackageV13.providerId, + providerCredential: { + authModeId: "gelios.rest-rotating-bearer.v3", + credentialType: "ndcProviderRotatingAccessApi", + }, + capabilities: geliosProviderPackageV13.capabilities + .filter((capability) => productsByCapability.get(capability.id)?.size) + .map((capability) => ({ + id: capability.id, + classification: capability.classification, + status: capability.status, + request: { + method: capability.request.method, + url: requestUrl(capability.request), + }, + dataProductIds: [...productsByCapability.get(capability.id)].sort(), + })), + publisher: { + nodeType: "n8n-nodes-ndc.ndcDataProductPublish", + credentialType: "ndcDataProductWriterApi", + }, + }; +} + +function requestUrl(request) { + const url = new URL(request.path, request.baseUrl); + for (const [name, value] of Object.entries(request.query || {})) { + url.searchParams.append(name, String(value)); + } + return url.toString(); +} + +function canonicalDigest(value) { + return `sha256:${createHash("sha256").update(JSON.stringify(stableValue(value)), "utf8").digest("hex")}`; +} + +function stableValue(value) { + if (Array.isArray(value)) return value.map(stableValue); + if (!value || typeof value !== "object") return value; + return Object.fromEntries( + Object.keys(value).sort().map((key) => [key, stableValue(value[key])]), + ); +} diff --git a/infra/deploy-runner/migrate-engine-gelios-v12-units-items-envelope.mjs b/infra/deploy-runner/migrate-engine-gelios-v12-units-items-envelope.mjs new file mode 100644 index 0000000..ad0ac29 --- /dev/null +++ b/infra/deploy-runner/migrate-engine-gelios-v12-units-items-envelope.mjs @@ -0,0 +1,208 @@ +#!/usr/bin/env node + +import { createHash } from "node:crypto"; +import { readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +import { validateL2ExecutionPlan } from "../../packages/external-provider-contract/src/index.mjs"; +import { geliosProviderPackageV12 } from "../../packages/external-provider-contract/providers/gelios/v12/index.mjs"; + +const PACKAGE_ID = "gelios.provider.v12"; +const PREDECESSOR_VERSION = "12.0.0"; +const TARGET_VERSION = "12.0.1"; +const UNITS_CAPABILITY_ID = "gelios.units.current.read"; +const engineRoot = resolve( + process.env.NODEDC_ENGINE_SOURCE_ROOT || "../NODEDC_ENGINE_INFRA", +); +const executionCatalogPath = resolve( + engineRoot, + "nodedc-source/server/assets/execution-plans/v1/catalog.json", +); +const securityCatalogPath = resolve( + engineRoot, + "nodedc-source/server/assets/provider-packages/v1/catalog.json", +); + +if ( + geliosProviderPackageV12.id !== PACKAGE_ID + || geliosProviderPackageV12.version !== TARGET_VERSION +) { + throw new Error("gelios_v12_units_items_source_version_mismatch"); +} + +const sourceCapabilities = new Map( + geliosProviderPackageV12.capabilities.map((capability) => [ + capability.id, + capability, + ]), +); +const unitsCapability = sourceCapabilities.get(UNITS_CAPABILITY_ID); +if ( + !unitsCapability + || unitsCapability.request.response.collectionPaths[0] !== "items" +) { + throw new Error("gelios_v12_units_items_source_contract_invalid"); +} + +const executionCatalog = JSON.parse( + await readFile(executionCatalogPath, "utf8"), +); +const executionPackage = requirePackage(executionCatalog, "execution"); +assertMigratableVersion(executionPackage.version, "execution"); +executionPackage.version = TARGET_VERSION; +executionPackage.contractDigest = canonicalDigest(geliosProviderPackageV12); +executionPackage.capabilities = geliosProviderPackageV12.capabilities.map( + (capability) => ({ + id: capability.id, + contractDigest: canonicalDigest(capability), + requestDigest: canonicalDigest(capability.request), + method: capability.request.method, + url: requestUrl(capability.request), + }), +); + +for (const profile of executionPackage.profiles) { + const plan = profile.executionPlanTemplate; + if (!plan) throw new Error(`gelios_v12_registered_plan_missing:${profile.id}`); + if (plan.compilerVersion !== "1.4.0") { + throw new Error(`gelios_v12_registered_plan_compiler_drift:${profile.id}`); + } + plan.package.version = TARGET_VERSION; + plan.artifacts.packageDigest = canonicalDigest(geliosProviderPackageV12); + + for (const step of plan.steps) { + const capabilityId = step.config?.capabilityId; + if (!capabilityId) continue; + const capability = sourceCapabilities.get(capabilityId); + if (!capability) { + throw new Error( + `gelios_v12_registered_plan_capability_missing:${profile.id}:${capabilityId}`, + ); + } + step.config.capabilityDigest = canonicalDigest(capability); + if (step.kind === "provider_request") { + step.config.request = structuredClone(capability.request); + } else if (step.kind === "extract_items") { + step.config.response = structuredClone(capability.request.response); + } + } + + const stepsById = new Map(plan.steps.map((step) => [step.id, step])); + for (const node of plan.graphBlueprint.nodes) { + const step = stepsById.get(node.stepId); + if (!step) { + throw new Error( + `gelios_v12_registered_plan_blueprint_step_missing:${profile.id}:${node.stepId}`, + ); + } + node.configDigest = canonicalDigest({ + stepConfig: step.config, + ...(node.adapterConfig ? { adapterConfig: node.adapterConfig } : {}), + }); + } + plan.graphBlueprint.blueprintDigest = digestWithout( + plan.graphBlueprint, + "blueprintDigest", + ); + plan.executionPlanDigest = digestWithout(plan, "executionPlanDigest"); + + const validation = validateL2ExecutionPlan(plan); + if (!validation.ok) { + throw new Error( + `gelios_v12_registered_plan_invalid:${profile.id}:` + + validation.errors.join(","), + ); + } +} + +const technicalProfile = executionPackage.profiles.find( + (profile) => profile.id === "gelios.units.profile.cold.v1", +); +const technicalExtract = technicalProfile?.executionPlanTemplate?.steps.find( + (step) => ( + step.kind === "extract_items" + && step.config?.capabilityId === UNITS_CAPABILITY_ID + ), +); +if (technicalExtract?.config?.response?.collectionPaths?.[0] !== "items") { + throw new Error("gelios_v12_technical_profile_items_envelope_missing"); +} + +const securityCatalog = JSON.parse( + await readFile(securityCatalogPath, "utf8"), +); +const securityPackage = requirePackage(securityCatalog, "security"); +assertMigratableVersion(securityPackage.version, "security"); +securityPackage.version = TARGET_VERSION; + +await writeFile( + executionCatalogPath, + `${JSON.stringify(executionCatalog, null, 2)}\n`, +); +await writeFile( + securityCatalogPath, + `${JSON.stringify(securityCatalog, null, 2)}\n`, +); + +console.log(JSON.stringify({ + ok: true, + packageId: PACKAGE_ID, + predecessorVersion: PREDECESSOR_VERSION, + targetVersion: TARGET_VERSION, + executionCatalogPath, + securityCatalogPath, + registeredProfiles: executionPackage.profiles.length, + technicalProfile: technicalProfile.id, + collectionPaths: technicalExtract.config.response.collectionPaths, + compilerVersion: technicalProfile.executionPlanTemplate.compilerVersion, +}, null, 2)); + +function requirePackage(catalog, kind) { + const matches = catalog.packages.filter( + (providerPackage) => providerPackage.id === PACKAGE_ID, + ); + if (matches.length !== 1) { + throw new Error(`gelios_v12_${kind}_package_cardinality_invalid`); + } + return matches[0]; +} + +function assertMigratableVersion(version, kind) { + if (![PREDECESSOR_VERSION, TARGET_VERSION].includes(version)) { + throw new Error( + `gelios_v12_${kind}_package_version_drift:${String(version)}`, + ); + } +} + +function requestUrl(request) { + const url = new URL(request.path, request.baseUrl); + for (const [name, value] of Object.entries(request.query || {})) { + url.searchParams.append(name, String(value)); + } + return url.toString(); +} + +function digestWithout(value, key) { + const descriptor = structuredClone(value); + delete descriptor[key]; + return canonicalDigest(descriptor); +} + +function canonicalDigest(value) { + return `sha256:${createHash("sha256").update( + JSON.stringify(stableValue(value)), + "utf8", + ).digest("hex")}`; +} + +function stableValue(value) { + if (Array.isArray(value)) return value.map(stableValue); + if (!value || typeof value !== "object") return value; + return Object.fromEntries( + Object.keys(value) + .filter((key) => value[key] !== undefined) + .sort() + .map((key) => [key, stableValue(value[key])]), + ); +} diff --git a/infra/deploy-runner/nodedc-deploy b/infra/deploy-runner/nodedc-deploy index 2e38da8..20b839e 100755 --- a/infra/deploy-runner/nodedc-deploy +++ b/infra/deploy-runner/nodedc-deploy @@ -886,6 +886,98 @@ ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_TARGET_SHA256 = { ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_NEW_PATHS = ( ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_DESCRIPTOR_REL, ) +ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_DESCRIPTOR_REL = ( + "nodedc-source/server/deployTransitions/registeredExecutionProfilesV2.json" +) +ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_ARTIFACT_ENTRIES = ( + "nodedc-source/server/assets/execution-plans/v1/catalog.json", + "nodedc-source/server/l2ExecutionPlan/catalog.js", + "nodedc-source/server/l2ExecutionPlan/registeredProfiles.js", + "nodedc-source/server/routes/engineAgentGateway.js", + ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL, + ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_DESCRIPTOR_REL, +) +ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_PREDECESSOR_SHA256 = { + "nodedc-source/server/assets/execution-plans/v1/catalog.json": + "eb0c32fa0e8017b23e2d225fcb2d5805aa6dd1bf674e8aa248afc6a4079a7403", + "nodedc-source/server/l2ExecutionPlan/catalog.js": + "c35b4c7ad9319aabbf1366a11ff52a6e99d961893bafdfcb4e84fc5f24fc04be", + "nodedc-source/server/routes/engineAgentGateway.js": + "4a9524fd954320277042c783b7b19cbb2172f075f27652c0eebfd743ffc47872", + ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: + "3e7aeb1d28eb291461f79cd656ece6488bc6d372124088e92f53bf89c3373f61", +} +ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FOUNDATION_SHA256 = { + "nodedc-source/server/l2ExecutionPlan/compiler.js": + "64f5196a83018505c6dac77a0f8674c27941257a874eed9b024c1c94f169be2b", + "nodedc-source/server/l2ExecutionPlan/materializer.js": + "dabf0073520049d7a04d1962b29e591ae092b87b287a50534ad2d98b03ae683c", + ENGINE_MCP_GELIOS_ITEMS_ENVELOPE_DESCRIPTOR_REL: + "16e39f54ebae776de9acfdf0078c79291310eeca5a1f720eb8b82496f4d419a3", +} +ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256 = { + "nodedc-source/server/assets/execution-plans/v1/catalog.json": + "940510499a9260dbff718b7b1f96f72f8e9ac593b460805af3007731578a7668", + "nodedc-source/server/l2ExecutionPlan/catalog.js": + "fec56b154ae483ad21bbaaeb4c55707bffce223c27874ac135ce58a9203259a0", + "nodedc-source/server/l2ExecutionPlan/registeredProfiles.js": + "3c521c1652c61c9d757197c76e053fd3ec602e13ef7f6a4856835ecc1a8a61d1", + "nodedc-source/server/routes/engineAgentGateway.js": + "8f04edc11251de86b825351c92338be9537207887cf802474b6b6d8c3cce4077", + ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: + "3d72709e40b79c01a62a6af4bde911fca4f609d5ac487d284cf97fd8ebd73686", + ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_DESCRIPTOR_REL: + "1db523f035a47c6b00b41b26efe1390ad2d24acece9d30dd039eff56626e934d", +} +ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_NEW_PATHS = ( + "nodedc-source/server/l2ExecutionPlan/registeredProfiles.js", + ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_DESCRIPTOR_REL, +) +ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FAILED_PATHS = ( + "nodedc-source/server/deployTransitions/registeredExecutionProfilesV1.json", +) +ENGINE_MCP_GELIOS_UNITS_ITEMS_DESCRIPTOR_REL = ( + "nodedc-source/server/deployTransitions/" + "geliosUnitsItemsEnvelopeV12Patch1.json" +) +ENGINE_MCP_GELIOS_UNITS_ITEMS_ARTIFACT_ENTRIES = ( + "nodedc-source/server/assets/execution-plans/v1/catalog.json", + "nodedc-source/server/assets/provider-packages/v1/catalog.json", + ENGINE_MCP_GELIOS_UNITS_ITEMS_DESCRIPTOR_REL, +) +ENGINE_MCP_GELIOS_UNITS_ITEMS_PREDECESSOR_SHA256 = { + "nodedc-source/server/assets/execution-plans/v1/catalog.json": + "940510499a9260dbff718b7b1f96f72f8e9ac593b460805af3007731578a7668", + "nodedc-source/server/assets/provider-packages/v1/catalog.json": + "42104d1267ca7840446c0c02edd3f9ecb29f8da8eb3e8f384cbd6dca0f676c4c", +} +ENGINE_MCP_GELIOS_UNITS_ITEMS_FOUNDATION_SHA256 = { + "nodedc-source/server/l2ExecutionPlan/catalog.js": + "fec56b154ae483ad21bbaaeb4c55707bffce223c27874ac135ce58a9203259a0", + "nodedc-source/server/l2ExecutionPlan/compiler.js": + "64f5196a83018505c6dac77a0f8674c27941257a874eed9b024c1c94f169be2b", + "nodedc-source/server/l2ExecutionPlan/materializer.js": + "dabf0073520049d7a04d1962b29e591ae092b87b287a50534ad2d98b03ae683c", + "nodedc-source/server/l2ExecutionPlan/registeredProfiles.js": + "3c521c1652c61c9d757197c76e053fd3ec602e13ef7f6a4856835ecc1a8a61d1", + "nodedc-source/server/routes/engineAgentGateway.js": + "8f04edc11251de86b825351c92338be9537207887cf802474b6b6d8c3cce4077", + ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL: + "3d72709e40b79c01a62a6af4bde911fca4f609d5ac487d284cf97fd8ebd73686", + ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_DESCRIPTOR_REL: + "1db523f035a47c6b00b41b26efe1390ad2d24acece9d30dd039eff56626e934d", +} +ENGINE_MCP_GELIOS_UNITS_ITEMS_TARGET_SHA256 = { + "nodedc-source/server/assets/execution-plans/v1/catalog.json": + "d0cc95fa5e55bda315947fdb13bd8a0d8931b9484677e4c4cc0dcb0e9614e215", + "nodedc-source/server/assets/provider-packages/v1/catalog.json": + "1f1b866b6f837bffc6d529dfffd8bfa9c87495bfcd9ae3515a79765143d94b1d", + ENGINE_MCP_GELIOS_UNITS_ITEMS_DESCRIPTOR_REL: + "7a1594573e98342d3a3edcc0dc4f663f06b30ca24cc20adb5f36d50591066d96", +} +ENGINE_MCP_GELIOS_UNITS_ITEMS_NEW_PATHS = ( + ENGINE_MCP_GELIOS_UNITS_ITEMS_DESCRIPTOR_REL, +) ENGINE_AGENT_FULL_GRANT_MIGRATION_ARTIFACT_ENTRIES = ( ENGINE_AGENT_FULL_GRANT_MIGRATION_STORE_REL, ) @@ -3587,6 +3679,7 @@ def validate_engine_node_intelligence_transition(payload_dir, entries): "const ENGINE_AGENT_MCP_VERSION = '0.9.0'", "const ENGINE_AGENT_MCP_VERSION = '0.10.0'", "const ENGINE_AGENT_MCP_VERSION = '0.11.0'", + "const ENGINE_AGENT_MCP_VERSION = '0.12.0'", ) ): die("Engine node-intelligence gateway MCP version is not registered") @@ -4991,6 +5084,96 @@ process.stdout.write('engine-mcp-gelios-items-envelope:0.11.0:v11-history:v12-au } +def accept_engine_mcp_registered_execution_profiles_runtime(): + root = component_root("engine") + validate_engine_mcp_registered_execution_profiles_slice( + root, + ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_ARTIFACT_ENTRIES, + ) + live = run_engine_backend_probe( + ( + "node", + "--input-type=module", + "-e", + """ +const gateway=await import('file:///app/server/routes/engineAgentGateway.js'); +const catalogModule=await import('file:///app/server/l2ExecutionPlan/catalog.js'); +const registeredModule=await import('file:///app/server/l2ExecutionPlan/registeredProfiles.js'); +const catalog=await catalogModule.loadExecutionPlanCatalog(); +let captured=null; +const service=registeredModule.createRegisteredExecutionProfileService({ + loadCatalog:async()=>catalog, + planMaterialization:async(input)=>{captured=input;return {ok:true,status:'planned',planRef:'emplan_probe',providerAuthRefs:[]};}, +}); +const target={workflowId:'workflow-probe',nodeId:'node-probe'}; +const listed=await service.list({actorKey:'actor-probe',target}); +const profile=listed.profiles.find(({profile})=>profile.id==='gelios.units.profile.cold.v1'); +if(gateway.ENGINE_AGENT_MCP_VERSION!=='0.12.0'||!gateway.engineAgentTools.some(({name})=>name==='engine_list_l2_execution_profiles')||!gateway.engineAgentTools.some(({name})=>name==='engine_plan_registered_l2_execution')||listed.count!==6||!profile||JSON.stringify(listed).includes('executionPlanTemplate'))process.exit(2); +await service.plan({actorKey:'actor-probe',target,profileRef:profile.profileRef,expectedRevision:'revision-probe',strategy:'create_or_reconcile_owned',expectedScope:{tenantId:'tenant-probe',connectionId:'connection-probe'}}); +if(captured?.executionPlan?.connection?.tenantId!=='tenant-probe'||captured?.executionPlan?.connection?.connectionId!=='connection-probe'||!String(captured?.executionPlan?.bindings?.provider?.reference||'').startsWith('ndc-credref:engine-target-'))process.exit(3); +process.stdout.write('engine-mcp-registered-execution-profiles:0.12.0:profiles-6:target-scope:existing-materializer'); +""".strip(), + ), + "Engine MCP registered execution profiles", + container_id=engine_backend_container_id(), + ) + expected = ( + "engine-mcp-registered-execution-profiles:" + "0.12.0:profiles-6:target-scope:existing-materializer" + ) + if live != expected: + die("Engine MCP registered execution profiles live acceptance mismatch") + return { + "target_sha256": dict( + ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256 + ), + "live": live, + } + + +def accept_engine_mcp_gelios_units_items_runtime(): + root = component_root("engine") + validate_engine_mcp_gelios_units_items_slice( + root, + ENGINE_MCP_GELIOS_UNITS_ITEMS_ARTIFACT_ENTRIES, + ) + live = run_engine_backend_probe( + ( + "node", + "--input-type=module", + "-e", + """ +const gateway=await import('file:///app/server/routes/engineAgentGateway.js'); +const executionModule=await import('file:///app/server/l2ExecutionPlan/catalog.js'); +const securityModule=await import('file:///app/server/dataProductPublishGrant/providerCatalog.js'); +const execution=await executionModule.loadExecutionPlanCatalog(); +const security=await securityModule.loadProviderSecurityCatalog(); +const provider=execution.packages.find((item)=>item.id==='gelios.provider.v12'); +const profile=provider?.profiles.find((item)=>item.id==='gelios.units.profile.cold.v1'); +const plan=profile?.executionPlanTemplate; +const extract=plan?.steps.find((step)=>step.kind==='extract_items'&&step.config?.capabilityId==='gelios.units.current.read'); +const secureProvider=security.packages.find((item)=>item.id==='gelios.provider.v12'); +if(gateway.ENGINE_AGENT_MCP_VERSION!=='0.12.0'||!gateway.engineAgentTools.some(({name})=>name==='engine_list_l2_execution_profiles')||!gateway.engineAgentTools.some(({name})=>name==='engine_plan_registered_l2_execution')||provider?.version!=='12.0.1'||secureProvider?.version!=='12.0.1'||plan?.compilerVersion!=='1.4.0'||extract?.config?.response?.collectionPaths?.[0]!=='items')process.exit(2); +process.stdout.write('engine-mcp-gelios-units-items:0.12.0:gelios.provider.v12@12.0.1:technical-profile:items'); +""".strip(), + ), + "Engine MCP Gelios units items envelope", + container_id=engine_backend_container_id(), + ) + expected = ( + "engine-mcp-gelios-units-items:0.12.0:" + "gelios.provider.v12@12.0.1:technical-profile:items" + ) + if live != expected: + die("Engine MCP Gelios units items live acceptance mismatch") + return { + "target_sha256": dict( + ENGINE_MCP_GELIOS_UNITS_ITEMS_TARGET_SHA256 + ), + "live": live, + } + + def accept_engine_mcp_l1_credential_reuse_runtime(): root = component_root("engine") validate_engine_mcp_l1_credential_reuse_slice( @@ -6771,6 +6954,340 @@ def validate_engine_mcp_gelios_items_envelope_slice(payload_dir, entries): die("Engine MCP Gelios items envelope catalog authority mismatch") +def validate_engine_mcp_registered_execution_profiles_slice( + payload_dir, + entries, +): + if ( + tuple(entries) + != ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_ARTIFACT_ENTRIES + ): + die( + "Engine MCP registered execution profiles files.txt exact " + "set/order mismatch" + ) + for rel, expected_sha256 in ( + ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256.items() + ): + path = payload_dir / rel + try: + path_stat = path.lstat() + except FileNotFoundError: + die( + "Engine MCP registered execution profiles target is missing: " + f"{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( + "Engine MCP registered execution profiles target sha256 " + f"mismatch: {rel}" + ) + + descriptor = read_strict_json( + payload_dir + / ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_DESCRIPTOR_REL, + "Engine MCP registered execution profiles descriptor", + max_bytes=32 * 1024, + ) + expected_descriptor = { + "schemaVersion": "nodedc.engine.deploy-transition/v1", + "id": "engine-mcp-registered-execution-profiles-v2", + "component": "engine", + "predecessor": "engine-mcp-gelios-items-envelope-v12", + "scope": + "external-mcp-registered-execution-profiles-with-node-intelligence-attestation", + "mcpVersion": "0.12.0", + "catalog": { + "schemaVersion": "nodedc.engine.execution-plan-catalog/v1", + "registeredProfiles": 6, + "profileRefs": "actor-and-target-scoped-opaque-ttl", + "executionPlanDescriptorsIncluded": False, + "providerEndpointsIncluded": False, + "customerScopeIncluded": False, + "credentialIdentitiesIncluded": False, + "credentialValuesIncluded": False, + }, + "tools": { + "discovery": "engine_list_l2_execution_profiles", + "planning": "engine_plan_registered_l2_execution", + "apply": "engine_apply_l2_execution_plan_materialization", + }, + "planning": { + "packageResolution": "server-owned-catalog", + "targetScope": "server-derived", + "digests": "server-derived", + "providerAuth": "target-local-compatible-refs", + "materializer": "existing-immutable-two-phase", + }, + "nodeIntelligenceAttestation": { + "releaseId": ENGINE_NODE_INTELLIGENCE_RELEASE_ID, + "previousGatewaySha256": + "4a9524fd954320277042c783b7b19cbb2172f075f27652c0eebfd743ffc47872", + "targetGatewaySha256": + "8f04edc11251de86b825351c92338be9537207887cf802474b6b6d8c3cce4077", + "sidecarImageChanged": False, + "nodeIntelligenceSourceChanged": False, + }, + "foundationSha256": { + "nodedc-source/server/l2ExecutionPlan/compiler.js": + "64f5196a83018505c6dac77a0f8674c27941257a874eed9b024c1c94f169be2b", + "nodedc-source/server/l2ExecutionPlan/materializer.js": + "dabf0073520049d7a04d1962b29e591ae092b87b287a50534ad2d98b03ae683c", + "nodedc-source/server/deployTransitions/geliosItemsEnvelopeV12.json": + "16e39f54ebae776de9acfdf0078c79291310eeca5a1f720eb8b82496f4d419a3", + }, + "boundaries": { + "n8nCoreChanged": False, + "l1Changed": False, + "l2GraphChanged": False, + "engineUiChanged": False, + "databaseChanged": False, + "credentialsChanged": False, + "foundryChanged": False, + "ontologyChanged": False, + "mcpNginxChanged": False, + "embeddedAiWorkspaceChanged": False, + "nodeIntelligenceAttestationChanged": True, + "nodeIntelligenceImageChanged": False, + "nodeIntelligenceSourceChanged": False, + }, + } + if descriptor != expected_descriptor: + die("Engine MCP registered execution profiles descriptor mismatch") + + node_intelligence_descriptor = read_engine_node_intelligence_descriptor( + payload_dir / ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL, + "Engine MCP registered execution profiles 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_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256[ + ENGINE_NODE_INTELLIGENCE_GATEWAY_REL + ] + ): + die( + "Engine MCP registered execution profiles node-intelligence " + "attestation mismatch" + ) + + catalog = read_strict_json( + payload_dir + / "nodedc-source/server/assets/execution-plans/v1/catalog.json", + "Engine MCP registered execution profiles catalog", + max_bytes=4 * 1024 * 1024, + ) + registered = [ + (provider_package, profile) + for provider_package in catalog.get("packages", []) + if isinstance(provider_package, dict) + for profile in provider_package.get("profiles", []) + if ( + isinstance(profile, dict) + and isinstance(profile.get("executionPlanTemplate"), dict) + ) + ] + if len(registered) != 6: + die("Engine MCP registered execution profiles count mismatch") + for provider_package, profile in registered: + template = profile["executionPlanTemplate"] + if ( + profile.get("cadence") + not in {"hot", "warm", "cold", "on_demand"} + or profile.get("dataClass") + not in {"operational", "restricted", "internal", "unclassified"} + or template.get("compilerVersion") != "1.4.0" + or template.get("package", {}).get("id") + != provider_package.get("id") + or template.get("connection", {}).get("collectionProfileId") + != profile.get("id") + or template.get("collectionProfile", {}).get("dataProductId") + != profile.get("dataProductId") + or not str( + template.get("bindings", {}) + .get("provider", {}) + .get("reference", "") + ).startswith("ndc-credref:catalog-build-") + ): + die("Engine MCP registered execution profile template mismatch") + + gateway = ( + payload_dir + / "nodedc-source/server/routes/engineAgentGateway.js" + ).read_text(encoding="utf-8") + resolver = ( + payload_dir + / "nodedc-source/server/l2ExecutionPlan/registeredProfiles.js" + ).read_text(encoding="utf-8") + for marker in ( + "const ENGINE_AGENT_MCP_VERSION = '0.12.0'", + "engine_list_l2_execution_profiles", + "engine_plan_registered_l2_execution", + ): + if marker not in gateway: + die( + "Engine MCP registered execution profiles gateway marker " + f"missing: {marker}" + ) + if ( + "registered_execution_profile_ref_scope_denied" not in resolver + or "targetBoundPlan" not in resolver + or re.search(r"gelios|robot2b", resolver, re.IGNORECASE) + ): + die("Engine MCP registered execution profiles resolver boundary mismatch") + + +def validate_engine_mcp_gelios_units_items_slice(payload_dir, entries): + if tuple(entries) != ENGINE_MCP_GELIOS_UNITS_ITEMS_ARTIFACT_ENTRIES: + die( + "Engine MCP Gelios units items envelope files.txt exact " + "set/order mismatch" + ) + for rel, expected_sha256 in ( + ENGINE_MCP_GELIOS_UNITS_ITEMS_TARGET_SHA256.items() + ): + path = payload_dir / rel + try: + path_stat = path.lstat() + except FileNotFoundError: + die(f"Engine MCP Gelios units items envelope target 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( + "Engine MCP Gelios units items envelope target sha256 " + f"mismatch: {rel}" + ) + + descriptor = read_strict_json( + payload_dir / ENGINE_MCP_GELIOS_UNITS_ITEMS_DESCRIPTOR_REL, + "Engine MCP Gelios units items envelope descriptor", + max_bytes=32 * 1024, + ) + if descriptor != { + "schemaVersion": "nodedc.engine.deploy-transition/v1", + "id": "engine-mcp-gelios-units-items-envelope-v12-patch1", + "component": "engine", + "predecessor": "engine-mcp-registered-execution-profiles-v2", + "scope": "server-owned-registered-profile-envelope-correction", + "mcpVersion": "0.12.0", + "providerPackage": { + "id": "gelios.provider.v12", + "predecessorVersion": "12.0.0", + "targetVersion": "12.0.1", + "capabilityId": "gelios.units.current.read", + "responseCollectionPath": "items", + }, + "registeredProfile": { + "id": "gelios.units.profile.cold.v1", + "dataProductId": "fleet.units.profile.current.v1", + "compilerVersion": "1.4.0", + "materializer": "existing-immutable-two-phase", + }, + "foundationSha256": dict( + ENGINE_MCP_GELIOS_UNITS_ITEMS_FOUNDATION_SHA256 + ), + "boundaries": { + "n8nCoreChanged": False, + "l1Changed": False, + "l2GraphChanged": False, + "engineRuntimeCodeChanged": False, + "engineUiChanged": False, + "databaseChanged": False, + "credentialsChanged": False, + "foundryChanged": False, + "ontologyChanged": False, + "mcpNginxChanged": False, + "embeddedAiWorkspaceChanged": False, + "providerEndpointsChanged": False, + }, + }: + die("Engine MCP Gelios units items envelope descriptor mismatch") + + execution_catalog = read_strict_json( + payload_dir + / "nodedc-source/server/assets/execution-plans/v1/catalog.json", + "Engine MCP Gelios units items envelope execution catalog", + max_bytes=4 * 1024 * 1024, + ) + execution_packages = [ + item for item in execution_catalog.get("packages", []) + if item.get("id") == "gelios.provider.v12" + ] + if len(execution_packages) != 1: + die("Engine MCP Gelios units items execution package cardinality mismatch") + provider_package = execution_packages[0] + profiles = provider_package.get("profiles", []) + technical_profiles = [ + item for item in profiles + if item.get("id") == "gelios.units.profile.cold.v1" + ] + units_profiles = [ + item for item in profiles + if "gelios.units.current.read" in item.get("capabilityIds", []) + ] + if ( + provider_package.get("version") != "12.0.1" + or len(profiles) != 6 + or len(technical_profiles) != 1 + or len(units_profiles) != 4 + ): + die("Engine MCP Gelios units items registered profile mismatch") + for profile in profiles: + template = profile.get("executionPlanTemplate", {}) + if template.get("compilerVersion") != "1.4.0": + die("Engine MCP Gelios units items compiler version drift") + for profile in units_profiles: + extract_steps = [ + step for step in ( + profile.get("executionPlanTemplate", {}).get("steps", []) + ) + if ( + step.get("kind") == "extract_items" + and step.get("config", {}).get("capabilityId") + == "gelios.units.current.read" + ) + ] + if ( + len(extract_steps) != 1 + or extract_steps[0].get("config", {}) + .get("response", {}).get("collectionPaths", [None])[:1] + != ["items"] + ): + die( + "Engine MCP Gelios units items response envelope mismatch: " + f"{profile.get('id')}" + ) + + security_catalog = read_strict_json( + payload_dir + / "nodedc-source/server/assets/provider-packages/v1/catalog.json", + "Engine MCP Gelios units items security catalog", + max_bytes=2 * 1024 * 1024, + ) + security_packages = [ + item for item in security_catalog.get("packages", []) + if item.get("id") == "gelios.provider.v12" + ] + if ( + len(security_packages) != 1 + or security_packages[0].get("version") != "12.0.1" + or [ + capability.get("id") + for capability in security_packages[0].get("capabilities", []) + ] != ["gelios.units.identity.read"] + ): + die("Engine MCP Gelios units items security authority mismatch") + + def validate_engine_agent_full_grant_migration_slice(payload_dir, entries): if tuple(entries) != ENGINE_AGENT_FULL_GRANT_MIGRATION_ARTIFACT_ENTRIES: die("Engine agent full grant migration files.txt exact set/order mismatch") @@ -7206,6 +7723,10 @@ def load_artifact(artifact, work_dir): manifest["component"], entries, ) + and not is_engine_mcp_registered_execution_profiles_slice( + manifest["component"], + entries, + ) and not is_engine_provider_authority_diagnostics_slice( manifest["component"], entries, @@ -7303,6 +7824,22 @@ def load_artifact(artifact, work_dir): payload_dir, entries, ) + if is_engine_mcp_registered_execution_profiles_slice( + manifest["component"], + entries, + ): + validate_engine_mcp_registered_execution_profiles_slice( + payload_dir, + entries, + ) + if is_engine_mcp_gelios_units_items_slice( + manifest["component"], + entries, + ): + validate_engine_mcp_gelios_units_items_slice( + payload_dir, + entries, + ) if is_engine_provider_security_catalog_slice(manifest["component"], entries): validate_engine_provider_security_catalog_payload(payload_dir, entries) if touches_engine_credential_sink(manifest["component"], entries): @@ -7356,6 +7893,14 @@ def load_artifact(artifact, work_dir): manifest["component"], entries, ) + and not is_engine_mcp_registered_execution_profiles_slice( + manifest["component"], + entries, + ) + and not is_engine_mcp_gelios_units_items_slice( + manifest["component"], + entries, + ) and ( touches_engine_data_product_publish_grant(entries) or ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL in entries @@ -7621,6 +8166,24 @@ def is_engine_mcp_gelios_items_envelope_slice(component, entries): ) +def is_engine_mcp_registered_execution_profiles_slice(component, entries): + return ( + component == "engine" + and entries is not None + and tuple(entries) + == ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_ARTIFACT_ENTRIES + ) + + +def is_engine_mcp_gelios_units_items_slice(component, entries): + return ( + component == "engine" + and entries is not None + and tuple(entries) + == ENGINE_MCP_GELIOS_UNITS_ITEMS_ARTIFACT_ENTRIES + ) + + def is_engine_agent_full_grant_migration_slice(component, entries): return ( component == "engine" @@ -8735,6 +9298,161 @@ def preflight_engine_mcp_gelios_items_envelope_predecessor(): } +def preflight_engine_mcp_registered_execution_profiles_predecessor(): + root = component_root("engine") + actual = {} + for rel, expected_sha256 in ( + ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_PREDECESSOR_SHA256.items() + ): + path = root / rel + try: + path_stat = path.lstat() + except FileNotFoundError: + die( + "Engine MCP registered execution profiles predecessor is " + f"missing: {rel}" + ) + if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode): + die( + "Engine MCP registered execution profiles predecessor is " + f"unsafe: {rel}" + ) + actual_sha256 = sha256_file(path) + if actual_sha256 != expected_sha256: + die( + "Engine MCP registered execution profiles predecessor drift " + f"detected: path={rel} expected={expected_sha256} " + f"actual={actual_sha256}" + ) + actual[rel] = actual_sha256 + + foundation = {} + for rel, expected_sha256 in ( + ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FOUNDATION_SHA256.items() + ): + path = root / rel + try: + path_stat = path.lstat() + except FileNotFoundError: + die( + "Engine MCP registered execution profiles foundation is " + f"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( + "Engine MCP registered execution profiles foundation drift: " + f"{rel}" + ) + foundation[rel] = expected_sha256 + + for rel in ( + *ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_NEW_PATHS, + *ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FAILED_PATHS, + ): + path = root / rel + if path.exists() or path.is_symlink(): + die( + "Engine MCP registered execution profiles new path already " + f"exists: {rel}" + ) + + backend = preflight_engine_credential_backend_runtime() + if backend["mode"] != "verified-derived-retry": + die( + "Engine MCP registered execution profiles requires the active " + "immutable backend" + ) + return { + "mode": + "gelios-items-envelope-v12-to-registered-profiles-v2-attested", + "predecessor_sha256": actual, + "foundation_sha256": foundation, + "target_sha256": dict( + ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256 + ), + "new_paths": tuple( + ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_NEW_PATHS + ), + "backend_mode": backend["mode"], + } + + +def preflight_engine_mcp_gelios_units_items_predecessor(): + root = component_root("engine") + actual = {} + for rel, expected_sha256 in ( + ENGINE_MCP_GELIOS_UNITS_ITEMS_PREDECESSOR_SHA256.items() + ): + path = root / rel + try: + path_stat = path.lstat() + except FileNotFoundError: + die( + "Engine MCP Gelios units items predecessor is missing: " + f"{rel}" + ) + if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISREG(path_stat.st_mode): + die(f"Engine MCP Gelios units items predecessor is unsafe: {rel}") + actual_sha256 = sha256_file(path) + if actual_sha256 != expected_sha256: + die( + "Engine MCP Gelios units items predecessor drift detected: " + f"path={rel} expected={expected_sha256} " + f"actual={actual_sha256}" + ) + actual[rel] = actual_sha256 + + foundation = {} + for rel, expected_sha256 in ( + ENGINE_MCP_GELIOS_UNITS_ITEMS_FOUNDATION_SHA256.items() + ): + path = root / rel + try: + path_stat = path.lstat() + except FileNotFoundError: + die( + "Engine MCP Gelios units items foundation is missing: " + f"{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 MCP Gelios units items foundation drift: {rel}") + foundation[rel] = expected_sha256 + + for rel in ENGINE_MCP_GELIOS_UNITS_ITEMS_NEW_PATHS: + path = root / rel + if path.exists() or path.is_symlink(): + die( + "Engine MCP Gelios units items new path already exists: " + f"{rel}" + ) + + backend = preflight_engine_credential_backend_runtime() + if backend["mode"] != "verified-derived-retry": + die( + "Engine MCP Gelios units items requires the active immutable " + "backend" + ) + return { + "mode": + "registered-profiles-v2-to-gelios-v12.0.1-units-items-envelope", + "predecessor_sha256": actual, + "foundation_sha256": foundation, + "target_sha256": dict( + ENGINE_MCP_GELIOS_UNITS_ITEMS_TARGET_SHA256 + ), + "new_paths": tuple(ENGINE_MCP_GELIOS_UNITS_ITEMS_NEW_PATHS), + "backend_mode": backend["mode"], + } + + def preflight_engine_agent_full_grant_migration_predecessor(): root = component_root("engine") store_path = root / ENGINE_AGENT_FULL_GRANT_MIGRATION_STORE_REL @@ -8798,6 +9516,8 @@ def component_services(component, entries=None): or is_engine_mcp_l1_credential_provenance_slice(component, entries) or is_engine_mcp_execution_plan_sandbox_runtime_slice(component, entries) or is_engine_mcp_gelios_items_envelope_slice(component, entries) + or is_engine_mcp_registered_execution_profiles_slice(component, entries) + or is_engine_mcp_gelios_units_items_slice(component, entries) or is_engine_provider_security_catalog_slice(component, entries) ): # This slice updates only the existing Engine backend control plane. @@ -11016,6 +11736,8 @@ def plan_artifact(artifact): mcp_l1_credential_provenance_preflight = None mcp_execution_plan_sandbox_runtime_preflight = None mcp_gelios_items_envelope_preflight = None + mcp_registered_execution_profiles_preflight = None + mcp_gelios_units_items_preflight = None provider_catalog_preflight = None with tempfile.TemporaryDirectory(prefix="plan-", dir=TMP_DIR) as tmp: manifest, entries, payload_dir = load_artifact(artifact, Path(tmp)) @@ -11137,6 +11859,20 @@ def plan_artifact(artifact): mcp_gelios_items_envelope_preflight = ( preflight_engine_mcp_gelios_items_envelope_predecessor() ) + if is_engine_mcp_registered_execution_profiles_slice( + manifest["component"], + entries, + ): + mcp_registered_execution_profiles_preflight = ( + preflight_engine_mcp_registered_execution_profiles_predecessor() + ) + if is_engine_mcp_gelios_units_items_slice( + manifest["component"], + entries, + ): + mcp_gelios_units_items_preflight = ( + preflight_engine_mcp_gelios_units_items_predecessor() + ) if is_engine_provider_security_catalog_slice(manifest["component"], entries): provider_catalog_preflight = preflight_engine_provider_security_catalog_predecessor() @@ -11215,6 +11951,12 @@ def plan_artifact(artifact): touches_mcp_gelios_items_envelope = ( is_engine_mcp_gelios_items_envelope_slice(component, entries) ) + touches_mcp_registered_execution_profiles = ( + is_engine_mcp_registered_execution_profiles_slice(component, entries) + ) + touches_mcp_gelios_units_items = ( + is_engine_mcp_gelios_units_items_slice(component, entries) + ) touches_agent_grant_migration = is_engine_agent_full_grant_migration_slice( component, entries, @@ -11240,6 +11982,8 @@ def plan_artifact(artifact): or touches_mcp_l1_credential_provenance or touches_mcp_execution_plan_sandbox_runtime or touches_mcp_gelios_items_envelope + or touches_mcp_registered_execution_profiles + or touches_mcp_gelios_units_items or touches_agent_grant_migration ): credential_backend_preflight = preflight_engine_credential_backend_runtime() @@ -11363,6 +12107,24 @@ def plan_artifact(artifact): "Engine MCP Gelios items envelope requires the active " "immutable credential backend" ) + if touches_mcp_registered_execution_profiles: + if mcp_registered_execution_profiles_preflight is None: + die( + "Engine MCP registered execution profiles preflight is missing" + ) + if credential_backend_preflight["mode"] != "verified-derived-retry": + die( + "Engine MCP registered execution profiles requires the active " + "immutable credential backend" + ) + if touches_mcp_gelios_units_items: + if mcp_gelios_units_items_preflight is None: + die("Engine MCP Gelios units items preflight is missing") + if credential_backend_preflight["mode"] != "verified-derived-retry": + die( + "Engine MCP Gelios units items requires the active immutable " + "credential backend" + ) if touches_agent_grant_migration: agent_grant_migration_predecessor_sha256 = ( preflight_engine_agent_full_grant_migration_predecessor() @@ -12304,6 +13066,155 @@ def plan_artifact(artifact): print("credentials=preserved") print("mcp_nginx=untouched") print("embedded_ai_workspace=untouched") + if mcp_registered_execution_profiles_preflight: + print( + "engine_mcp_registered_execution_profiles_transition=" + f"{mcp_registered_execution_profiles_preflight['mode']}" + ) + print("engine_mcp_version=0.12.0") + print( + "engine_mcp_tool=engine_list_l2_execution_profiles," + "engine_plan_registered_l2_execution" + ) + print("engine_mcp_registered_profiles=6") + print("engine_mcp_profile_refs=actor-and-target-scoped-opaque-ttl") + print("engine_mcp_target_scope=server-derived") + print("engine_mcp_execution_plan_digests=server-derived") + print("engine_mcp_existing_materializer=reused") + print("engine_mcp_provider_hardcode=no") + print("engine_mcp_internal_descriptors_included=no") + print("engine_mcp_provider_endpoints_included=no") + print("engine_mcp_customer_scope_included=no") + print("engine_mcp_credential_ids_included=no") + print("engine_mcp_credential_values_included=no") + print("engine_node_intelligence_attestation=updated") + print("engine_node_intelligence_image=untouched") + print("engine_node_intelligence_source=untouched") + for foundation_path, foundation_sha256 in ( + mcp_registered_execution_profiles_preflight[ + "foundation_sha256" + ].items() + ): + print( + "engine_mcp_registered_execution_profiles_foundation_sha256" + f"[{foundation_path}]={foundation_sha256}" + ) + for changed_path in ( + ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_ARTIFACT_ENTRIES + ): + print( + "engine_mcp_registered_execution_profiles_changed_path=" + f"{changed_path}" + ) + if changed_path in ( + mcp_registered_execution_profiles_preflight[ + "predecessor_sha256" + ] + ): + print( + "engine_mcp_registered_execution_profiles_predecessor_sha256" + f"[{changed_path}]=" + f"{mcp_registered_execution_profiles_preflight['predecessor_sha256'][changed_path]}" + ) + elif changed_path in ( + mcp_registered_execution_profiles_preflight["new_paths"] + ): + print( + "engine_mcp_registered_execution_profiles_predecessor_state" + f"[{changed_path}]=absent" + ) + else: + die( + "Engine MCP registered execution profiles plan has no " + f"predecessor state: {changed_path}" + ) + print( + "engine_mcp_registered_execution_profiles_target_sha256" + f"[{changed_path}]=" + f"{mcp_registered_execution_profiles_preflight['target_sha256'][changed_path]}" + ) + print( + "backend_current_barrier=" + f"{mcp_registered_execution_profiles_preflight['backend_mode']}" + ) + print("backend_force_recreate=yes") + print("backend_pull=never") + print("l2_graph=untouched") + print("n8n_l1=untouched") + print("n8n_core=untouched") + print("engine_ui=untouched") + print("engine_databases=untouched") + print("credentials=preserved") + print("foundry=untouched") + print("ontology=untouched") + print("mcp_nginx=untouched") + print("embedded_ai_workspace=untouched") + if mcp_gelios_units_items_preflight: + print( + "engine_mcp_gelios_units_items_transition=" + f"{mcp_gelios_units_items_preflight['mode']}" + ) + print("engine_mcp_version=0.12.0") + print("engine_mcp_provider_package=gelios.provider.v12@12.0.1") + print("engine_mcp_registered_profile=gelios.units.profile.cold.v1") + print("engine_mcp_data_product=fleet.units.profile.current.v1") + print("engine_mcp_response_collection_path=items") + print("engine_mcp_execution_plan_compiler_version=1.4.0") + print("engine_mcp_existing_materializer=reused") + print("engine_mcp_runtime_code_changed=no") + print("engine_mcp_provider_endpoints_changed=no") + print("engine_mcp_raw_provider_values_included=no") + for foundation_path, foundation_sha256 in ( + mcp_gelios_units_items_preflight["foundation_sha256"].items() + ): + print( + "engine_mcp_gelios_units_items_foundation_sha256" + f"[{foundation_path}]={foundation_sha256}" + ) + for changed_path in ENGINE_MCP_GELIOS_UNITS_ITEMS_ARTIFACT_ENTRIES: + print( + "engine_mcp_gelios_units_items_changed_path=" + f"{changed_path}" + ) + if changed_path in ( + mcp_gelios_units_items_preflight["predecessor_sha256"] + ): + print( + "engine_mcp_gelios_units_items_predecessor_sha256" + f"[{changed_path}]=" + f"{mcp_gelios_units_items_preflight['predecessor_sha256'][changed_path]}" + ) + elif changed_path in mcp_gelios_units_items_preflight["new_paths"]: + print( + "engine_mcp_gelios_units_items_predecessor_state" + f"[{changed_path}]=absent" + ) + else: + die( + "Engine MCP Gelios units items plan has no predecessor " + f"state: {changed_path}" + ) + print( + "engine_mcp_gelios_units_items_target_sha256" + f"[{changed_path}]=" + f"{mcp_gelios_units_items_preflight['target_sha256'][changed_path]}" + ) + print( + "backend_current_barrier=" + f"{mcp_gelios_units_items_preflight['backend_mode']}" + ) + print("backend_force_recreate=yes") + print("backend_pull=never") + print("l2_graph=untouched") + print("n8n_l1=untouched") + print("n8n_core=untouched") + print("engine_ui=untouched") + print("engine_databases=untouched") + print("credentials=preserved") + print("foundry=untouched") + print("ontology=untouched") + print("mcp_nginx=untouched") + print("embedded_ai_workspace=untouched") if transition_descriptor: print(f"n8n_transition={transition_descriptor['action']}") print(f"n8n_version={transition_descriptor['n8nVersion']}") @@ -13448,6 +14359,11 @@ def component_healthchecks(component, entries=None, services=None): entries, ) or is_engine_mcp_gelios_items_envelope_slice(component, entries) + or is_engine_mcp_registered_execution_profiles_slice( + component, + entries, + ) + or is_engine_mcp_gelios_units_items_slice(component, entries) or is_engine_agent_full_grant_migration_slice(component, entries) or is_engine_mcp_control_plane_slice(component, entries) or is_engine_mcp_ontology_sdk_slice(component, entries) @@ -13901,6 +14817,12 @@ def run_healthchecks(component, entries=None, services=None): touches_mcp_gelios_items_envelope = ( is_engine_mcp_gelios_items_envelope_slice(component, entries) ) + touches_mcp_registered_execution_profiles = ( + is_engine_mcp_registered_execution_profiles_slice(component, entries) + ) + touches_mcp_gelios_units_items = ( + is_engine_mcp_gelios_units_items_slice(component, entries) + ) touches_agent_grant_migration = is_engine_agent_full_grant_migration_slice( component, entries, @@ -13927,6 +14849,8 @@ def run_healthchecks(component, entries=None, services=None): or touches_mcp_l1_credential_provenance or touches_mcp_execution_plan_sandbox_runtime or touches_mcp_gelios_items_envelope + or touches_mcp_registered_execution_profiles + or touches_mcp_gelios_units_items or touches_agent_grant_migration or touches_mcp_control_plane or touches_mcp_ontology_sdk @@ -13957,6 +14881,8 @@ def run_healthchecks(component, entries=None, services=None): or touches_mcp_l1_credential_provenance or touches_mcp_execution_plan_sandbox_runtime or touches_mcp_gelios_items_envelope + or touches_mcp_registered_execution_profiles + or touches_mcp_gelios_units_items or touches_agent_grant_migration or touches_mcp_control_plane or touches_mcp_ontology_sdk @@ -14438,6 +15364,90 @@ def run_healthchecks(component, entries=None, services=None): "Engine MCP Gelios items envelope installed state is neither " "target nor rollback predecessor" ) + if touches_mcp_registered_execution_profiles: + 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_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256.items() + ) + ) and all( + (root / rel).is_file() + and not (root / rel).is_symlink() + and sha256_file(root / rel) == expected + for rel, expected in ( + ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FOUNDATION_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_MCP_REGISTERED_EXECUTION_PROFILES_PREDECESSOR_SHA256.items() + ) + ) and all( + (root / rel).is_file() + and not (root / rel).is_symlink() + and sha256_file(root / rel) == expected + for rel, expected in ( + ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FOUNDATION_SHA256.items() + ) + ) and all( + not (root / rel).exists() and not (root / rel).is_symlink() + for rel in ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_NEW_PATHS + ) + if target_state: + accept_engine_mcp_registered_execution_profiles_runtime() + elif not predecessor_state: + die( + "Engine MCP registered execution profiles installed state is " + "neither target nor rollback predecessor" + ) + if touches_mcp_gelios_units_items: + 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_MCP_GELIOS_UNITS_ITEMS_TARGET_SHA256.items() + ) + ) and all( + (root / rel).is_file() + and not (root / rel).is_symlink() + and sha256_file(root / rel) == expected + for rel, expected in ( + ENGINE_MCP_GELIOS_UNITS_ITEMS_FOUNDATION_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_MCP_GELIOS_UNITS_ITEMS_PREDECESSOR_SHA256.items() + ) + ) and all( + (root / rel).is_file() + and not (root / rel).is_symlink() + and sha256_file(root / rel) == expected + for rel, expected in ( + ENGINE_MCP_GELIOS_UNITS_ITEMS_FOUNDATION_SHA256.items() + ) + ) and all( + not (root / rel).exists() and not (root / rel).is_symlink() + for rel in ENGINE_MCP_GELIOS_UNITS_ITEMS_NEW_PATHS + ) + if target_state: + accept_engine_mcp_gelios_units_items_runtime() + elif not predecessor_state: + die( + "Engine MCP Gelios units items installed state is neither " + "target nor rollback predecessor" + ) container_name = COMPONENTS[component].get("health_container") if container_name: healthcheck_container(container_name) @@ -14683,6 +15693,38 @@ def apply_artifact(artifact): "Engine MCP Gelios items envelope requires the " "active immutable credential backend" ) + if is_engine_mcp_registered_execution_profiles_slice( + component, + entries, + ): + preflight_engine_mcp_registered_execution_profiles_predecessor() + registered_profiles_backend_preflight = ( + preflight_engine_credential_backend_runtime() + ) + if ( + registered_profiles_backend_preflight["mode"] + != "verified-derived-retry" + ): + die( + "Engine MCP registered execution profiles requires " + "the active immutable credential backend" + ) + if is_engine_mcp_gelios_units_items_slice( + component, + entries, + ): + preflight_engine_mcp_gelios_units_items_predecessor() + gelios_units_items_backend_preflight = ( + preflight_engine_credential_backend_runtime() + ) + if ( + gelios_units_items_backend_preflight["mode"] + != "verified-derived-retry" + ): + die( + "Engine MCP Gelios units items requires the active " + "immutable credential backend" + ) if is_engine_agent_full_grant_migration_slice(component, entries): preflight_engine_agent_full_grant_migration_predecessor() migration_backend_preflight = preflight_engine_credential_backend_runtime() @@ -14972,6 +16014,14 @@ def apply_artifact(artifact): component, entries, ) + or is_engine_mcp_registered_execution_profiles_slice( + component, + entries, + ) + or is_engine_mcp_gelios_units_items_slice( + component, + entries, + ) or is_engine_agent_full_grant_migration_slice(component, entries) or is_engine_mcp_control_plane_slice(component, entries) or is_engine_mcp_ontology_sdk_slice(component, entries) diff --git a/infra/deploy-runner/test_engine_l1_agent_init_concurrency_artifact.py b/infra/deploy-runner/test_engine_l1_agent_init_concurrency_artifact.py new file mode 100644 index 0000000..4b05cc2 --- /dev/null +++ b/infra/deploy-runner/test_engine_l1_agent_init_concurrency_artifact.py @@ -0,0 +1,130 @@ +import hashlib +import importlib.machinery +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import tarfile +import tempfile +import unittest + + +SCRIPT_DIR = Path(__file__).resolve().parent +RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy" +BUILDER_PATH = SCRIPT_DIR / "build-engine-l1-agent-init-concurrency-artifact.mjs" +ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA" +PATCH_ID = "engine-l1-agent-init-concurrency-20991231-999" +SOURCE_COMMIT = "c4077fe0d8d824fa58616eeb986dc78ca43070da" + + +def load_runner(): + loader = importlib.machinery.SourceFileLoader( + "nodedc_engine_l1_agent_init_concurrency", + 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 EngineL1AgentInitConcurrencyArtifactTest(unittest.TestCase): + def require_target_commit(self): + current = subprocess.run( + ["git", "-C", str(ENGINE_ROOT), "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + if current != SOURCE_COMMIT: + self.skipTest("historical Engine L1 agent-init source has advanced") + + def build(self, artifact_dir): + self.require_target_commit() + 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_artifact_is_exact_deterministic_and_runner_accepted(self): + with tempfile.TemporaryDirectory( + prefix="nodedc-engine-l1-agent-init-", + ) 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["sourceCommit"], SOURCE_COMMIT) + self.assertEqual(first_artifact.read_bytes(), second_artifact.read_bytes()) + self.assertEqual( + first["sha256"], + hashlib.sha256(first_artifact.read_bytes()).hexdigest(), + ) + self.assertFalse(first["n8nCoreChanged"]) + + manifest, entries, payload = RUNNER.load_artifact( + first_artifact, + root / "loaded", + ) + self.assertEqual(manifest["component"], "engine") + self.assertEqual(manifest["type"], "app-overlay") + self.assertEqual(tuple(entries), tuple(first["entries"])) + self.assertEqual( + RUNNER.component_services("engine", entries), + ("nodedc-backend", "app"), + ) + self.assertEqual( + RUNNER.component_healthchecks( + "engine", + entries, + ("nodedc-backend", "app"), + ), + ( + "http://127.0.0.1:8080/", + "http://127.0.0.1:3001/health", + ), + ) + self.assertTrue(RUNNER.component_publish_dist("engine", entries)) + self.assertEqual(RUNNER.component_builds("engine", entries), ()) + + payload_files = { + path.relative_to(payload).as_posix() + for path in payload.rglob("*") + if path.is_file() + } + self.assertEqual(payload_files, set(first["targetSha256"])) + for relative_path, expected in first["targetSha256"].items(): + self.assertEqual( + hashlib.sha256((payload / relative_path).read_bytes()).hexdigest(), + expected, + ) + self.assertTrue(all("/server/data/" not in path for path in payload_files)) + self.assertTrue(all("/tests/" not in path for path in payload_files)) + self.assertTrue(all(not path.endswith(".env") for path in payload_files)) + self.assertTrue(all("n8n-data" not in path for path in payload_files)) + + with tarfile.open(first_artifact, "r:gz") as archive: + for member in archive.getmembers(): + self.assertIn(member.type, (tarfile.REGTYPE, tarfile.DIRTYPE)) + self.assertEqual(member.uid, 0) + self.assertEqual(member.gid, 0) + self.assertEqual(member.mtime, 0) + self.assertNotIn(".DS_Store", member.name) + self.assertNotIn("__MACOSX", member.name) + + +if __name__ == "__main__": + unittest.main() diff --git a/infra/deploy-runner/test_engine_mcp_gelios_units_items_envelope.py b/infra/deploy-runner/test_engine_mcp_gelios_units_items_envelope.py new file mode 100644 index 0000000..eff956c --- /dev/null +++ b/infra/deploy-runner/test_engine_mcp_gelios_units_items_envelope.py @@ -0,0 +1,284 @@ +import hashlib +import importlib.machinery +import importlib.util +import io +import json +import os +from pathlib import Path +import subprocess +import tarfile +import tempfile +import unittest +from contextlib import redirect_stdout +from unittest import mock + + +SCRIPT_DIR = Path(__file__).resolve().parent +RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy" +BUILDER_PATH = ( + SCRIPT_DIR + / "build-engine-mcp-gelios-units-items-envelope-artifact.mjs" +) +ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA" +PATCH_ID = "engine-mcp-gelios-units-items-envelope-20991231-999" + + +def load_runner(): + loader = importlib.machinery.SourceFileLoader( + "nodedc_engine_mcp_gelios_units_items", + 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 EngineMcpGeliosUnitsItemsEnvelopeTest(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_emits_exact_deterministic_catalog_slice(self): + with tempfile.TemporaryDirectory( + prefix="nodedc-engine-gelios-units-items-builder-" + ) 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["mcpVersion"], "0.12.0") + self.assertEqual( + first["registeredProfile"], + "gelios.units.profile.cold.v1", + ) + self.assertEqual(first["responseCollectionPath"], "items") + self.assertEqual(first["existingMaterializerReused"], True) + self.assertEqual(first["engineRuntimeCodeChanged"], False) + self.assertEqual(first["n8nCoreChanged"], False) + self.assertEqual(first["l2GraphChanged"], False) + + 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() + ) + self.assertEqual( + entries, + RUNNER.ENGINE_MCP_GELIOS_UNITS_ITEMS_ARTIFACT_ENTRIES, + ) + self.assertTrue( + RUNNER.is_engine_mcp_gelios_units_items_slice( + "engine", + entries, + ) + ) + self.assertEqual( + RUNNER.component_services("engine", entries), + ("nodedc-backend",), + ) + RUNNER.validate_engine_mcp_gelios_units_items_slice( + extract / "payload", + entries, + ) + + def test_preflight_requires_exact_registered_profiles_predecessor(self): + with tempfile.TemporaryDirectory( + prefix="nodedc-engine-gelios-units-items-preflight-" + ) as directory: + root = Path(directory) + all_live = { + **RUNNER.ENGINE_MCP_GELIOS_UNITS_ITEMS_PREDECESSOR_SHA256, + **RUNNER.ENGINE_MCP_GELIOS_UNITS_ITEMS_FOUNDATION_SHA256, + } + for relative_path in all_live: + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("live\n", encoding="utf-8") + + def exact_hash(path): + relative_path = Path(path).relative_to(root).as_posix() + return all_live[relative_path] + + with ( + mock.patch.object(RUNNER, "component_root", return_value=root), + mock.patch.object(RUNNER, "sha256_file", side_effect=exact_hash), + mock.patch.object( + RUNNER, + "preflight_engine_credential_backend_runtime", + return_value={"mode": "verified-derived-retry"}, + ), + ): + result = ( + RUNNER + .preflight_engine_mcp_gelios_units_items_predecessor() + ) + self.assertEqual( + result["mode"], + ( + "registered-profiles-v2-to-" + "gelios-v12.0.1-units-items-envelope" + ), + ) + self.assertEqual( + result["new_paths"], + RUNNER.ENGINE_MCP_GELIOS_UNITS_ITEMS_NEW_PATHS, + ) + + changed = next(iter( + RUNNER.ENGINE_MCP_GELIOS_UNITS_ITEMS_PREDECESSOR_SHA256 + )) + with ( + mock.patch.object(RUNNER, "component_root", return_value=root), + mock.patch.object( + RUNNER, + "sha256_file", + side_effect=lambda path: ( + "0" * 64 + if Path(path).relative_to(root).as_posix() == changed + else exact_hash(path) + ), + ), + ): + with self.assertRaises(RUNNER.DeployError): + ( + RUNNER + .preflight_engine_mcp_gelios_units_items_predecessor() + ) + + def test_plan_renders_exact_provider_package_patch_boundary(self): + with tempfile.TemporaryDirectory( + prefix="nodedc-engine-gelios-units-items-plan-" + ) as directory: + root = Path(directory) + built = self.build(root / "artifacts") + artifact = Path(built["artifact"]) + live_root = root / "live" + live_root.mkdir() + preflight = { + "mode": ( + "registered-profiles-v2-to-" + "gelios-v12.0.1-units-items-envelope" + ), + "predecessor_sha256": dict( + RUNNER + .ENGINE_MCP_GELIOS_UNITS_ITEMS_PREDECESSOR_SHA256 + ), + "foundation_sha256": dict( + RUNNER + .ENGINE_MCP_GELIOS_UNITS_ITEMS_FOUNDATION_SHA256 + ), + "target_sha256": dict( + RUNNER.ENGINE_MCP_GELIOS_UNITS_ITEMS_TARGET_SHA256 + ), + "new_paths": ( + RUNNER.ENGINE_MCP_GELIOS_UNITS_ITEMS_NEW_PATHS + ), + "backend_mode": "verified-derived-retry", + } + output = io.StringIO() + with ( + mock.patch.object(RUNNER, "validate_artifact_location"), + mock.patch.object(RUNNER, "ensure_layout"), + mock.patch.object(RUNNER, "TMP_DIR", root), + mock.patch.object( + RUNNER, + "component_root", + return_value=live_root, + ), + mock.patch.object( + RUNNER, + "component_compose_root", + return_value=live_root, + ), + mock.patch.object( + RUNNER, + "preflight_engine_mcp_gelios_units_items_predecessor", + return_value=preflight, + ), + mock.patch.object( + RUNNER, + "preflight_engine_credential_backend_runtime", + return_value={"mode": "verified-derived-retry"}, + ), + mock.patch.object(RUNNER, "state_has_sha", return_value=False), + mock.patch.object( + RUNNER, + "state_has_patch_id", + return_value=False, + ), + redirect_stdout(output), + ): + RUNNER.plan_artifact(artifact) + plan = output.getvalue() + self.assertIn("services=nodedc-backend", plan) + self.assertIn("engine_mcp_version=0.12.0", plan) + self.assertIn( + "engine_mcp_provider_package=gelios.provider.v12@12.0.1", + plan, + ) + self.assertIn( + "engine_mcp_registered_profile=gelios.units.profile.cold.v1", + plan, + ) + self.assertIn("engine_mcp_response_collection_path=items", plan) + self.assertIn("engine_mcp_existing_materializer=reused", plan) + self.assertIn("engine_mcp_runtime_code_changed=no", plan) + self.assertIn("engine_mcp_provider_endpoints_changed=no", plan) + self.assertIn("l2_graph=untouched", plan) + self.assertIn("n8n_core=untouched", plan) + self.assertIn("foundry=untouched", plan) + self.assertIn("state=new", plan) + + def test_live_acceptance_probes_catalog_and_tools(self): + expected_live = ( + "engine-mcp-gelios-units-items:0.12.0:" + "gelios.provider.v12@12.0.1:technical-profile:items" + ) + 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_mcp_gelios_units_items_runtime() + self.assertEqual(result["live"], expected_live) + probe_source = backend_probe.call_args.args[0][-1] + self.assertIn("engine_list_l2_execution_profiles", probe_source) + self.assertIn("engine_plan_registered_l2_execution", probe_source) + self.assertIn("gelios.units.profile.cold.v1", probe_source) + self.assertIn("collectionPaths?.[0]!=='items'", probe_source) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/infra/deploy-runner/test_engine_mcp_registered_execution_profiles.py b/infra/deploy-runner/test_engine_mcp_registered_execution_profiles.py new file mode 100644 index 0000000..6ed2f9a --- /dev/null +++ b/infra/deploy-runner/test_engine_mcp_registered_execution_profiles.py @@ -0,0 +1,316 @@ +import hashlib +import importlib.machinery +import importlib.util +import io +import json +import os +from pathlib import Path +import subprocess +import tarfile +import tempfile +import unittest +from contextlib import redirect_stdout +from unittest import mock + + +SCRIPT_DIR = Path(__file__).resolve().parent +RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy" +BUILDER_PATH = ( + SCRIPT_DIR + / "build-engine-mcp-registered-execution-profiles-artifact.mjs" +) +ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA" +PATCH_ID = "engine-mcp-registered-execution-profiles-20991231-999" + + +def load_runner(): + loader = importlib.machinery.SourceFileLoader( + "nodedc_engine_mcp_registered_execution_profiles", + 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 EngineMcpRegisteredExecutionProfilesTest(unittest.TestCase): + def require_current_target_source(self): + for relative_path, expected in ( + RUNNER.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256.items() + ): + path = ENGINE_ROOT / relative_path + if ( + not path.is_file() + or hashlib.sha256(path.read_bytes()).hexdigest() != expected + ): + self.skipTest("Registered execution profiles source advanced") + + 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_emits_exact_deterministic_backend_slice(self): + self.require_current_target_source() + with tempfile.TemporaryDirectory( + prefix="nodedc-engine-registered-profiles-builder-" + ) 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["mcpVersion"], "0.12.0") + self.assertEqual(first["registeredProfiles"], 6) + self.assertEqual(first["existingMaterializerReused"], True) + self.assertEqual( + first["nodeIntelligenceAttestationChanged"], + True, + ) + self.assertEqual(first["nodeIntelligenceImageChanged"], False) + self.assertEqual(first["nodeIntelligenceSourceChanged"], False) + self.assertEqual(first["n8nCoreChanged"], False) + self.assertEqual(first["l2GraphChanged"], False) + + 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() + ) + self.assertEqual( + entries, + RUNNER.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_ARTIFACT_ENTRIES, + ) + self.assertTrue( + RUNNER.is_engine_mcp_registered_execution_profiles_slice( + "engine", + entries, + ) + ) + self.assertEqual( + RUNNER.component_services("engine", entries), + ("nodedc-backend",), + ) + RUNNER.validate_engine_mcp_registered_execution_profiles_slice( + extract / "payload", + entries, + ) + + def test_preflight_requires_exact_046_predecessor_and_absent_new_paths(self): + with tempfile.TemporaryDirectory( + prefix="nodedc-engine-registered-profiles-preflight-" + ) as directory: + root = Path(directory) + all_live = { + **RUNNER.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_PREDECESSOR_SHA256, + **RUNNER.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FOUNDATION_SHA256, + } + for relative_path in all_live: + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("live\n", encoding="utf-8") + + def exact_hash(path): + relative_path = Path(path).relative_to(root).as_posix() + return all_live[relative_path] + + with ( + mock.patch.object(RUNNER, "component_root", return_value=root), + mock.patch.object(RUNNER, "sha256_file", side_effect=exact_hash), + mock.patch.object( + RUNNER, + "preflight_engine_credential_backend_runtime", + return_value={"mode": "verified-derived-retry"}, + ), + ): + result = ( + RUNNER + .preflight_engine_mcp_registered_execution_profiles_predecessor() + ) + self.assertEqual( + result["mode"], + "gelios-items-envelope-v12-to-registered-profiles-v2-attested", + ) + self.assertEqual( + result["new_paths"], + RUNNER.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_NEW_PATHS, + ) + + new_path = ( + root + / RUNNER.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_NEW_PATHS[0] + ) + new_path.parent.mkdir(parents=True, exist_ok=True) + new_path.write_text("unexpected\n", encoding="utf-8") + with ( + mock.patch.object(RUNNER, "component_root", return_value=root), + mock.patch.object(RUNNER, "sha256_file", side_effect=exact_hash), + ): + with self.assertRaises(RUNNER.DeployError): + ( + RUNNER + .preflight_engine_mcp_registered_execution_profiles_predecessor() + ) + + def test_plan_renders_exact_server_side_planning_boundary(self): + self.require_current_target_source() + with tempfile.TemporaryDirectory( + prefix="nodedc-engine-registered-profiles-plan-" + ) as directory: + root = Path(directory) + built = self.build(root / "artifacts") + artifact = Path(built["artifact"]) + live_root = root / "live" + live_root.mkdir() + preflight = { + "mode": + "gelios-items-envelope-v12-to-registered-profiles-v2-attested", + "predecessor_sha256": dict( + RUNNER + .ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_PREDECESSOR_SHA256 + ), + "foundation_sha256": dict( + RUNNER + .ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FOUNDATION_SHA256 + ), + "target_sha256": dict( + RUNNER + .ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256 + ), + "new_paths": ( + RUNNER + .ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_NEW_PATHS + ), + "backend_mode": "verified-derived-retry", + } + output = io.StringIO() + with ( + mock.patch.object(RUNNER, "validate_artifact_location"), + mock.patch.object(RUNNER, "ensure_layout"), + mock.patch.object(RUNNER, "TMP_DIR", root), + mock.patch.object( + RUNNER, + "component_root", + return_value=live_root, + ), + mock.patch.object( + RUNNER, + "component_compose_root", + return_value=live_root, + ), + mock.patch.object( + RUNNER, + "preflight_engine_mcp_registered_execution_profiles_predecessor", + return_value=preflight, + ), + mock.patch.object( + RUNNER, + "preflight_engine_credential_backend_runtime", + return_value={"mode": "verified-derived-retry"}, + ), + mock.patch.object(RUNNER, "state_has_sha", return_value=False), + mock.patch.object( + RUNNER, + "state_has_patch_id", + return_value=False, + ), + redirect_stdout(output), + ): + RUNNER.plan_artifact(artifact) + plan = output.getvalue() + self.assertIn("services=nodedc-backend", plan) + self.assertIn("engine_mcp_version=0.12.0", plan) + self.assertIn("engine_mcp_registered_profiles=6", plan) + self.assertIn("engine_mcp_target_scope=server-derived", plan) + self.assertIn("engine_mcp_existing_materializer=reused", plan) + self.assertIn("engine_mcp_provider_endpoints_included=no", plan) + self.assertIn( + "engine_node_intelligence_attestation=updated", + plan, + ) + self.assertIn("engine_node_intelligence_image=untouched", plan) + self.assertIn("l2_graph=untouched", plan) + self.assertIn("n8n_core=untouched", plan) + self.assertIn("foundry=untouched", plan) + self.assertIn("state=new", plan) + + def test_live_acceptance_probes_tools_catalog_and_target_rebinding(self): + self.require_current_target_source() + expected_live = ( + "engine-mcp-registered-execution-profiles:" + "0.12.0:profiles-6:target-scope:existing-materializer" + ) + 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_mcp_registered_execution_profiles_runtime() + ) + self.assertEqual(result["live"], expected_live) + probe_source = backend_probe.call_args.args[0][-1] + self.assertIn("engine_list_l2_execution_profiles", probe_source) + self.assertIn("engine_plan_registered_l2_execution", probe_source) + self.assertIn("tenant-probe", probe_source) + self.assertIn("ndc-credref:engine-target-", probe_source) + + def test_target_gateway_matches_node_intelligence_attestation(self): + descriptor_path = ( + ENGINE_ROOT + / RUNNER.ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL + ) + descriptor = RUNNER.read_engine_node_intelligence_descriptor( + descriptor_path, + "registered profiles target node-intelligence descriptor", + ) + with mock.patch.object( + RUNNER, + "component_root", + return_value=ENGINE_ROOT, + ): + gateway_sha256 = ( + RUNNER.validate_installed_engine_node_intelligence_source( + descriptor, + ) + ) + self.assertEqual( + gateway_sha256, + RUNNER.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256[ + RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL + ], + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/infra/deploy-runner/test_engine_node_intelligence.py b/infra/deploy-runner/test_engine_node_intelligence.py index 3b7c0f5..3774c0b 100644 --- a/infra/deploy-runner/test_engine_node_intelligence.py +++ b/infra/deploy-runner/test_engine_node_intelligence.py @@ -147,15 +147,15 @@ class EngineNodeIntelligenceTest(unittest.TestCase): ) self.assertEqual(actual, descriptor) - def test_gateway_accepts_registered_0_11_and_rejects_unknown_successors(self): + def test_gateway_accepts_registered_0_12_and_rejects_unknown_successors(self): with tempfile.TemporaryDirectory() as directory: payload, descriptor = self.make_activation_payload(Path(directory)) gateway = payload / RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL gateway_text = gateway.read_text(encoding="utf-8") - self.assertIn("const ENGINE_AGENT_MCP_VERSION = '0.11.0'", gateway_text) + self.assertIn("const ENGINE_AGENT_MCP_VERSION = '0.12.0'", gateway_text) gateway.write_text( gateway_text.replace( - "const ENGINE_AGENT_MCP_VERSION = '0.11.0'", + "const ENGINE_AGENT_MCP_VERSION = '0.12.0'", "const ENGINE_AGENT_MCP_VERSION = '9.9.9'", ), encoding="utf-8", diff --git a/packages/external-provider-contract/providers/gelios/index.mjs b/packages/external-provider-contract/providers/gelios/index.mjs index 398a102..fcb5f5a 100644 --- a/packages/external-provider-contract/providers/gelios/index.mjs +++ b/packages/external-provider-contract/providers/gelios/index.mjs @@ -7,3 +7,4 @@ export * from "./v9/index.mjs"; export { geliosProviderPackageV10 } from "./v10/index.mjs"; export { geliosProviderPackageV11 } from "./v11/index.mjs"; export { geliosProviderPackageV12 } from "./v12/index.mjs"; +export { geliosProviderPackageV13 } from "./v13/index.mjs"; diff --git a/packages/external-provider-contract/providers/gelios/v12/package.mjs b/packages/external-provider-contract/providers/gelios/v12/package.mjs index 33c7cad..a6c3c73 100644 --- a/packages/external-provider-contract/providers/gelios/v12/package.mjs +++ b/packages/external-provider-contract/providers/gelios/v12/package.mjs @@ -1,12 +1,14 @@ import { geliosProviderPackageV11 } from "../v11/package.mjs"; export const GELIOS_PROVIDER_PACKAGE_ID = "gelios.provider.v12"; -export const GELIOS_PROVIDER_PACKAGE_VERSION = "12.0.0"; +export const GELIOS_PROVIDER_PACKAGE_VERSION = "12.0.1"; export const GELIOS_UNIT_IDENTITY_DATA_PRODUCT_ID = "fleet.units.identity.current.v1"; export const GELIOS_UNIT_IDENTITY_DATA_PRODUCT_VERSION = "1.0.0"; export const GELIOS_UNIT_IDENTITY_ONTOLOGY_REVISION = "ontology.map.moving_object.v3"; +const IDENTITY_CONTRACT_VERSION = "12.0.0"; const IDENTITY_CAPABILITY_ID = "gelios.units.identity.read"; +const UNITS_CAPABILITY_ID = "gelios.units.current.read"; const IDENTITY_PROFILE_ID = "gelios.units.identity.warm.v1"; const IDENTITY_MAPPING_ID = "gelios.units.to.fleet.units.identity.current.v1"; const IDENTITY_TEMPLATE_ID = "gelios.units.identity.l2.v1"; @@ -24,33 +26,43 @@ const identityCapability = value.capabilities.find( (capability) => capability.id === IDENTITY_CAPABILITY_ID, ); if (!identityCapability) throw new Error("gelios_v12_identity_capability_missing"); -identityCapability.request.response.collectionPaths = [ - "items", - ...identityCapability.request.response.collectionPaths.filter( - (path) => path !== "items", - ), -]; +prependItemsCollectionPath(identityCapability); + +const unitsCapability = value.capabilities.find( + (capability) => capability.id === UNITS_CAPABILITY_ID, +); +if (!unitsCapability) throw new Error("gelios_v12_units_capability_missing"); +prependItemsCollectionPath(unitsCapability); const identityProfile = value.collectionProfiles.find( (profile) => profile.id === IDENTITY_PROFILE_ID, ); if (!identityProfile) throw new Error("gelios_v12_identity_profile_missing"); -identityProfile.version = GELIOS_PROVIDER_PACKAGE_VERSION; +identityProfile.version = IDENTITY_CONTRACT_VERSION; const identityMapping = value.mappingContracts.find( (mapping) => mapping.id === IDENTITY_MAPPING_ID, ); if (!identityMapping) throw new Error("gelios_v12_identity_mapping_missing"); -identityMapping.version = GELIOS_PROVIDER_PACKAGE_VERSION; +identityMapping.version = IDENTITY_CONTRACT_VERSION; const identityTemplate = value.l2Templates.find( (template) => template.id === IDENTITY_TEMPLATE_ID, ); if (!identityTemplate) throw new Error("gelios_v12_identity_template_missing"); -identityTemplate.version = GELIOS_PROVIDER_PACKAGE_VERSION; +identityTemplate.version = IDENTITY_CONTRACT_VERSION; export const geliosProviderPackageV12 = deepFreeze(value); +function prependItemsCollectionPath(capability) { + capability.request.response.collectionPaths = [ + "items", + ...capability.request.response.collectionPaths.filter( + (path) => path !== "items", + ), + ]; +} + function deepFreeze(input) { if (!input || typeof input !== "object" || Object.isFrozen(input)) return input; Object.freeze(input); diff --git a/packages/external-provider-contract/providers/gelios/v13/index.mjs b/packages/external-provider-contract/providers/gelios/v13/index.mjs new file mode 100644 index 0000000..cc80a82 --- /dev/null +++ b/packages/external-provider-contract/providers/gelios/v13/index.mjs @@ -0,0 +1,10 @@ +export { + GELIOS_PROVIDER_PACKAGE_ID, + GELIOS_PROVIDER_PACKAGE_VERSION, + GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID, + GELIOS_UNIT_CONTACTS_DATA_PRODUCT_VERSION, + GELIOS_UNIT_CONTACTS_ONTOLOGY_REVISION, + geliosProviderPackageV13, +} from "./package.mjs"; +export { geliosTelemetryFieldRegistryV5 } from "../v12/index.mjs"; +export { geliosCapabilityCatalogV3 } from "../capability-catalog-v3.mjs"; diff --git a/packages/external-provider-contract/providers/gelios/v13/package.mjs b/packages/external-provider-contract/providers/gelios/v13/package.mjs new file mode 100644 index 0000000..6a0557c --- /dev/null +++ b/packages/external-provider-contract/providers/gelios/v13/package.mjs @@ -0,0 +1,318 @@ +import { geliosProviderPackageV12 } from "../v12/package.mjs"; + +export const GELIOS_PROVIDER_PACKAGE_ID = "gelios.provider.v13"; +export const GELIOS_PROVIDER_PACKAGE_VERSION = "13.0.0"; +export const GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID = "fleet.units.contacts.current.v2"; +export const GELIOS_UNIT_CONTACTS_DATA_PRODUCT_VERSION = "2.0.0"; +export const GELIOS_UNIT_CONTACTS_ONTOLOGY_REVISION = "ontology.map.moving_object.v3"; + +const UNITS_CAPABILITY_ID = "gelios.units.identity.read"; +const USERS_CAPABILITY_ID = "gelios.users.directory.read"; +const FIELD_POLICY_ID = "gelios.units.contacts.fields.v2"; +const COLLECTION_PROFILE_ID = "gelios.units.contacts.warm.v2"; +const MAPPING_ID = "gelios.units.to.fleet.units.contacts.current.v2"; +const TEMPLATE_ID = "gelios.units.contacts.l2.v2"; + +const contactFields = Object.freeze([ + "available_user_emails", + "available_user_ids", + "available_user_legal_names", + "available_user_logins", + "available_user_phones", + "device_imei", + "device_phone_primary", + "device_phone_secondary", + "display_name", + "provider_creator_email", + "provider_creator_id", + "provider_creator_legal_name", + "provider_creator_login", + "provider_creator_phone", +]); + +const value = structuredClone(geliosProviderPackageV12); +const unitsCapability = value.capabilities.find( + (capability) => capability.id === UNITS_CAPABILITY_ID, +); +const templateBase = value.l2Templates.find( + (template) => template.id === "gelios.units.identity.l2.v1", +); +if (!unitsCapability) throw new Error("gelios_v13_units_capability_missing"); +if (!templateBase) throw new Error("gelios_v13_template_base_missing"); + +value.id = GELIOS_PROVIDER_PACKAGE_ID; +value.version = GELIOS_PROVIDER_PACKAGE_VERSION; +value.manifest = { + ...value.manifest, + id: "gelios.provider.manifest.v13", + version: GELIOS_PROVIDER_PACKAGE_VERSION, + capabilityIds: [...value.manifest.capabilityIds, USERS_CAPABILITY_ID], + fieldPolicyIds: [...value.manifest.fieldPolicyIds, FIELD_POLICY_ID], + collectionProfileIds: [...value.manifest.collectionProfileIds, COLLECTION_PROFILE_ID], + dataProductIds: [...value.manifest.dataProductIds, GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID], + mappingContractIds: [...value.manifest.mappingContractIds, MAPPING_ID], + l2TemplateIds: [...value.manifest.l2TemplateIds, TEMPLATE_ID], +}; + +value.capabilities.push({ + ...structuredClone(unitsCapability), + id: USERS_CAPABILITY_ID, + request: { + ...structuredClone(unitsCapability.request), + path: "/api/v1/users", + query: { + incladmpan: false, + inclavfun: false, + inclbillinf: false, + inclextra: false, + inclexs: false, + inclmon: false, + inclsmtp: false, + o1: "userIdAsc", + pl: 500, + po: 0, + }, + response: { + collectionPaths: ["items"], + pagination: { + mode: "offset", + limitParameter: "pl", + offsetParameter: "po", + pageSize: 500, + itemsPath: "items", + totalPath: "paginationMetadata.totalCount", + maxPages: 10, + maxItems: 5000, + maxResponseBytes: 16 * 1024 * 1024, + }, + }, + }, +}); + +value.fieldPolicies.push({ + id: FIELD_POLICY_ID, + version: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_VERSION, + dataProductId: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID, + targetFields: [...contactFields], + unknownSourceFields: "drop", + dynamicSourceFields: "drop_until_classified", + restrictedSourcePaths: [ + "commands", + "currentUserAccess", + "customFields", + "driver", + "extraInfo", + "hwDecryptKey", + "lastMsg", + "lastSensorsVal", + "maintenancePlan", + "preSetCommandGroup", + "sensors", + "stationaryLat", + "stationaryLon", + "storageInfo", + ], +}); + +value.collectionProfiles.push({ + id: COLLECTION_PROFILE_ID, + version: GELIOS_PROVIDER_PACKAGE_VERSION, + mode: "realtime", + schedule: { intervalMs: 5 * 60 * 1000 }, + capabilityIds: [UNITS_CAPABILITY_ID, USERS_CAPABILITY_ID], + dataProductId: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID, + mappingContractId: MAPPING_ID, + fieldPolicyId: FIELD_POLICY_ID, + l2TemplateId: TEMPLATE_ID, + entityScope: { + mode: "all_visible_to_credential", + refresh: "each_collection_run", + businessEntityFilter: "forbidden", + }, + batching: { maxFacts: 5000 }, + cardinality: { + maxCurrentEntities: 5000, + onExceed: "require_partitioned_data_product", + }, + retry: { maxAttempts: 4, backoff: "fixed_delay", delayMs: 2000 }, +}); + +value.dataProducts.push({ + id: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID, + version: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_VERSION, + ontologyRevision: GELIOS_UNIT_CONTACTS_ONTOLOGY_REVISION, + deliveryMode: "snapshot+patch", + semanticTypes: ["map.moving_object"], + fields: [...contactFields], + fieldContracts: { + available_user_emails: optional("string_array"), + available_user_ids: optional("string_array"), + available_user_legal_names: optional("string_array"), + available_user_logins: optional("string_array"), + available_user_phones: optional("string_array"), + device_imei: optional("string"), + device_phone_primary: optional("string"), + device_phone_secondary: optional("string"), + display_name: { type: "string", required: true }, + provider_creator_email: optional("string"), + provider_creator_id: optional("string"), + provider_creator_legal_name: optional("string"), + provider_creator_login: optional("string"), + provider_creator_phone: optional("string"), + }, + history: { mode: "none", retentionDays: 1 }, +}); + +value.mappingContracts.push({ + schemaVersion: "nodedc.semantic-mapping/v1", + id: MAPPING_ID, + version: GELIOS_PROVIDER_PACKAGE_VERSION, + sourceCapabilityId: UNITS_CAPABILITY_ID, + fieldPolicyId: FIELD_POLICY_ID, + target: { + dataProductId: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID, + version: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_VERSION, + ontologyRevision: GELIOS_UNIT_CONTACTS_ONTOLOGY_REVISION, + semanticType: "map.moving_object", + }, + derivations: { + available_user_emails: responseLookupList("availableToUsers", "email"), + available_user_ids: boundedStringList("availableToUsers", "id"), + available_user_legal_names: responseLookupList("availableToUsers", "legalName"), + available_user_logins: boundedStringList("availableToUsers", "login"), + available_user_phones: responseLookupList("availableToUsers", "phone"), + provider_creator_email: responseLookupFirst("creator.id", "email"), + provider_creator_legal_name: responseLookupFirst("creator.id", "legalName"), + provider_creator_phone: responseLookupFirst("creator.id", "phone"), + }, + fact: { + sourceId: { + strategy: "first_non_empty", + paths: ["id", "unit_id", "unitId"], + coerce: "string", + prefix: "gelios-unit-", + }, + semanticType: { constant: "map.moving_object" }, + observedAt: { + strategy: "first_non_empty", + paths: ["updatedAt"], + coerce: "unix_or_iso_timestamp", + fallback: "collection_received_at", + }, + attributes: { + available_user_emails: { derive: "available_user_emails" }, + available_user_ids: { derive: "available_user_ids" }, + available_user_legal_names: { derive: "available_user_legal_names" }, + available_user_logins: { derive: "available_user_logins" }, + available_user_phones: { derive: "available_user_phones" }, + device_imei: stringField(["imei"]), + device_phone_primary: stringField(["phone"]), + device_phone_secondary: stringField(["phone2"]), + display_name: stringField(["name", "unit_name", "title", "label"], false), + provider_creator_email: { derive: "provider_creator_email", omitIfMissing: true }, + provider_creator_id: stringField(["creator.id"]), + provider_creator_legal_name: { + derive: "provider_creator_legal_name", + omitIfMissing: true, + }, + provider_creator_login: stringField(["creator.login"]), + provider_creator_phone: { derive: "provider_creator_phone", omitIfMissing: true }, + }, + }, +}); + +value.l2Templates.push({ + ...structuredClone(templateBase), + id: TEMPLATE_ID, + version: GELIOS_PROVIDER_PACKAGE_VERSION, + steps: [ + { id: "collection.trigger", kind: "collection_trigger", collectionProfileDriven: true }, + { id: "provider.fetch-units", kind: "provider_request", capabilityId: UNITS_CAPABILITY_ID }, + { id: "provider.fetch-users", kind: "provider_request", capabilityId: USERS_CAPABILITY_ID }, + { id: "provider.extract-units", kind: "extract_items", capabilityId: UNITS_CAPABILITY_ID }, + { id: "ontology.map", kind: "semantic_mapping", mappingContractId: MAPPING_ID }, + { + id: "data-product.publish", + kind: "data_product_publish", + dataProductId: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID, + nodeType: "n8n-nodes-ndc.ndcDataProductPublish", + }, + ], +}); + +export const geliosProviderPackageV13 = deepFreeze(value); + +function optional(type) { + return { type, required: false }; +} + +function stringField(paths, omitIfMissing = true) { + return { + strategy: "first_non_empty", + paths, + coerce: "string", + ...(omitIfMissing ? { omitIfMissing: true } : {}), + }; +} + +function boundedStringList(arrayPath, valuePath) { + return { + kind: "bounded_string_list", + rules: ["array.string_values"], + default: [], + parameters: { + arrayPath, + valuePath, + maxItems: 256, + maxItemLength: 512, + }, + }; +} + +function responseLookupList(sourceArrayPath, valuePath) { + return { + kind: "bounded_response_lookup", + rules: ["response.lookup.list"], + default: [], + parameters: { + responseCapabilityId: USERS_CAPABILITY_ID, + responseCollectionPath: "items", + responseKeyPath: "id", + sourceArrayPath, + sourceKeyPath: "id", + valuePath, + resultMode: "list", + maxResponseItems: 5000, + maxSourceKeys: 256, + maxItems: 256, + maxItemLength: 512, + }, + }; +} + +function responseLookupFirst(sourceKeyPath, valuePath) { + return { + kind: "bounded_response_lookup", + rules: ["response.lookup.first"], + default: "", + parameters: { + responseCapabilityId: USERS_CAPABILITY_ID, + responseCollectionPath: "items", + responseKeyPath: "id", + sourceKeyPath, + valuePath, + resultMode: "first", + maxResponseItems: 5000, + maxSourceKeys: 1, + maxItems: 1, + maxItemLength: 512, + }, + }; +} + +function deepFreeze(input) { + if (!input || typeof input !== "object" || Object.isFrozen(input)) return input; + Object.freeze(input); + for (const child of Object.values(input)) deepFreeze(child); + return input; +} diff --git a/packages/external-provider-contract/src/l2-execution-plan.mjs b/packages/external-provider-contract/src/l2-execution-plan.mjs index 1ee45a8..4d01eed 100644 --- a/packages/external-provider-contract/src/l2-execution-plan.mjs +++ b/packages/external-provider-contract/src/l2-execution-plan.mjs @@ -9,11 +9,12 @@ export const L2_EXECUTION_PLAN_SCHEMA_VERSION = "nodedc.l2-execution-plan/v1"; export const L2_GRAPH_BLUEPRINT_SCHEMA_VERSION = "nodedc.l2-graph-blueprint/v1"; export const L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION = "nodedc.l2-materialization-receipt/v1"; export const L2_MAPPING_RUNTIME_SCHEMA_VERSION = "nodedc.semantic-mapping-runtime/v1"; -export const L2_EXECUTION_PLAN_COMPILER_VERSION = "1.4.0"; +export const L2_EXECUTION_PLAN_COMPILER_VERSION = "1.5.0"; export const L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS = Object.freeze([ "1.1.0", "1.2.0", "1.3.0", + "1.4.0", L2_EXECUTION_PLAN_COMPILER_VERSION, ]); diff --git a/packages/external-provider-contract/src/provider-package.mjs b/packages/external-provider-contract/src/provider-package.mjs index 235e779..24f9c6b 100644 --- a/packages/external-provider-contract/src/provider-package.mjs +++ b/packages/external-provider-contract/src/provider-package.mjs @@ -259,6 +259,14 @@ export function validateProviderPackage(value) { if (!selectedCapabilityIds.includes(mapping.sourceCapabilityId)) { errors.push(`collectionProfile.${profile.id}.mapping_source_capability_must_be_selected`); } + for (const derivation of Object.values(mapping.derivations || {})) { + if ( + derivation?.kind === "bounded_response_lookup" + && !selectedCapabilityIds.includes(derivation.parameters?.responseCapabilityId) + ) { + errors.push(`collectionProfile.${profile.id}.lookup_response_capability_must_be_selected`); + } + } if (mapping.fieldPolicyId !== profile.fieldPolicyId) errors.push(`collectionProfile.${profile.id}.mapping_field_policy_mismatch`); if (mapping.target?.dataProductId !== profile.dataProductId) errors.push(`collectionProfile.${profile.id}.mapping_data_product_mismatch`); } @@ -283,6 +291,16 @@ export function validateProviderPackage(value) { } for (const mapping of mappingContracts) { assertReference(mapping.sourceCapabilityId, capabilityIds, `mappingContract.${mapping.id}.sourceCapabilityId`, errors); + for (const [derivationId, derivation] of Object.entries(mapping.derivations || {})) { + if (derivation?.kind === "bounded_response_lookup") { + assertReference( + derivation.parameters?.responseCapabilityId, + capabilityIds, + `mappingContract.${mapping.id}.derivations.${derivationId}.parameters.responseCapabilityId`, + errors, + ); + } + } assertReference(mapping.fieldPolicyId, fieldPolicyIds, `mappingContract.${mapping.id}.fieldPolicyId`, errors); assertReference(mapping.target?.dataProductId, dataProductIds, `mappingContract.${mapping.id}.target.dataProductId`, errors); const product = dataProducts.find((item) => item.id === mapping.target?.dataProductId); @@ -1049,6 +1067,7 @@ function validateDerivation(value, path, errors) { "ordered_rules", "flag_set", "bounded_readings", + "bounded_response_lookup", "bounded_string_list", "bounded_named_values", ]).has(value.kind)) errors.push(`${path}.kind_invalid`); @@ -1075,6 +1094,89 @@ function validateDerivation(value, path, errors) { if (value.kind === "bounded_named_values") { validateBoundedNamedValuesDerivation(value, path, errors); } + if (value.kind === "bounded_response_lookup") { + validateBoundedResponseLookupDerivation(value, path, errors); + } +} + +function validateBoundedResponseLookupDerivation(value, path, errors) { + const parameters = isPlainObject(value.parameters) ? value.parameters : {}; + const allowed = new Set([ + "responseCapabilityId", + "responseCollectionPath", + "responseKeyPath", + "sourceArrayPath", + "sourceKeyPath", + "valuePath", + "resultMode", + "maxResponseItems", + "maxSourceKeys", + "maxItems", + "maxItemLength", + ]); + if (Object.keys(parameters).some((key) => !allowed.has(key))) { + errors.push(`${path}.parameters_shape_invalid`); + } + if (!IDENTIFIER.test(String(parameters.responseCapabilityId || ""))) { + errors.push(`${path}.parameters.responseCapabilityId_invalid`); + } + for (const key of [ + "responseCollectionPath", + "responseKeyPath", + "sourceKeyPath", + "valuePath", + ]) { + if (!isCanonicalSourcePath(parameters[key])) { + errors.push(`${path}.parameters.${key}_must_be_canonical_dot_path`); + } + } + if (SECRET_FIELD_NAME.test(String(parameters.valuePath || ""))) { + errors.push(`${path}.parameters.valuePath_secret_field_not_allowed`); + } + if ( + parameters.sourceArrayPath !== undefined + && !isCanonicalSourcePath(parameters.sourceArrayPath) + ) { + errors.push(`${path}.parameters.sourceArrayPath_must_be_canonical_dot_path`); + } + if (!new Set(["first", "list"]).has(parameters.resultMode)) { + errors.push(`${path}.parameters.resultMode_invalid`); + } + if ( + !Number.isInteger(parameters.maxResponseItems) + || parameters.maxResponseItems < 1 + || parameters.maxResponseItems > 5000 + ) { + errors.push(`${path}.parameters.maxResponseItems_invalid`); + } + if ( + !Number.isInteger(parameters.maxSourceKeys) + || parameters.maxSourceKeys < 1 + || parameters.maxSourceKeys > 256 + ) { + errors.push(`${path}.parameters.maxSourceKeys_invalid`); + } + if ( + !Number.isInteger(parameters.maxItems) + || parameters.maxItems < 1 + || parameters.maxItems > 256 + ) { + errors.push(`${path}.parameters.maxItems_invalid`); + } + if ( + !Number.isInteger(parameters.maxItemLength) + || parameters.maxItemLength < 1 + || parameters.maxItemLength > 512 + ) { + errors.push(`${path}.parameters.maxItemLength_invalid`); + } + if (parameters.resultMode === "list") { + if (!Array.isArray(value.default) || value.default.length !== 0) { + errors.push(`${path}.default_must_be_empty_array_for_list`); + } + } else if (value.default !== "") { + errors.push(`${path}.default_must_be_empty_string_for_first`); + } } function validateBoundedStringListDerivation(value, path, errors) { diff --git a/packages/external-provider-contract/test/l2-execution-plan.test.mjs b/packages/external-provider-contract/test/l2-execution-plan.test.mjs index 403b67d..16565ad 100644 --- a/packages/external-provider-contract/test/l2-execution-plan.test.mjs +++ b/packages/external-provider-contract/test/l2-execution-plan.test.mjs @@ -31,9 +31,9 @@ const positionsPlan = compileL2ExecutionPlan( ); assert.equal(positionsPlan.schemaVersion, L2_EXECUTION_PLAN_SCHEMA_VERSION); -assert.equal(L2_EXECUTION_PLAN_COMPILER_VERSION, "1.4.0"); -assert.deepEqual(L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS, ["1.1.0", "1.2.0", "1.3.0", "1.4.0"]); -assert.equal(positionsPlan.compilerVersion, "1.4.0"); +assert.equal(L2_EXECUTION_PLAN_COMPILER_VERSION, "1.5.0"); +assert.deepEqual(L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS, ["1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0"]); +assert.equal(positionsPlan.compilerVersion, "1.5.0"); assert.deepEqual(validateL2ExecutionPlan(positionsPlan), { ok: true, errors: [] }); assert.match(positionsPlan.executionPlanDigest, /^sha256:[a-f0-9]{64}$/); assert.match(positionsPlan.artifacts.packageDigest, /^sha256:[a-f0-9]{64}$/); diff --git a/packages/external-provider-contract/test/provider-package.test.mjs b/packages/external-provider-contract/test/provider-package.test.mjs index ba357ce..058e32c 100644 --- a/packages/external-provider-contract/test/provider-package.test.mjs +++ b/packages/external-provider-contract/test/provider-package.test.mjs @@ -25,6 +25,7 @@ import { geliosProviderPackageV9 } from "../providers/gelios/v9/index.mjs"; import { geliosProviderPackageV10 } from "../providers/gelios/v10/index.mjs"; import { geliosProviderPackageV11 } from "../providers/gelios/v11/index.mjs"; import { geliosProviderPackageV12 } from "../providers/gelios/v12/index.mjs"; +import { geliosProviderPackageV13 } from "../providers/gelios/v13/index.mjs"; import { normalizeDataProductDefinition } from "../../../services/external-data-plane/src/data-product-policy.mjs"; const expectedFields = [ @@ -55,6 +56,7 @@ assert.deepEqual(validateProviderPackage(geliosProviderPackageV9), { ok: true, e assert.deepEqual(validateProviderPackage(geliosProviderPackageV10), { ok: true, errors: [] }); assert.deepEqual(validateProviderPackage(geliosProviderPackageV11), { ok: true, errors: [] }); assert.deepEqual(validateProviderPackage(geliosProviderPackageV12), { ok: true, errors: [] }); +assert.deepEqual(validateProviderPackage(geliosProviderPackageV13), { ok: true, errors: [] }); assert.equal(geliosProviderPackageV9.id, "gelios.provider.v9"); assert.equal(geliosProviderPackageV9.version, "9.0.0"); assert.equal(geliosProviderPackageV10.id, "gelios.provider.v10"); @@ -62,7 +64,9 @@ assert.equal(geliosProviderPackageV10.version, "10.0.0"); assert.equal(geliosProviderPackageV11.id, "gelios.provider.v11"); assert.equal(geliosProviderPackageV11.version, "11.0.0"); assert.equal(geliosProviderPackageV12.id, "gelios.provider.v12"); -assert.equal(geliosProviderPackageV12.version, "12.0.0"); +assert.equal(geliosProviderPackageV12.version, "12.0.1"); +assert.equal(geliosProviderPackageV13.id, "gelios.provider.v13"); +assert.equal(geliosProviderPackageV13.version, "13.0.0"); assert.equal(geliosProviderPackageV8.id, "gelios.provider.v8"); const profileProduct = geliosProviderPackageV8.dataProducts.find((product) => product.id === "fleet.units.profile.current.v1"); @@ -127,13 +131,56 @@ assert.deepEqual(identityCapability.request.query, { const identityCapabilityV12 = geliosProviderPackageV12.capabilities.find( (capability) => capability.id === "gelios.units.identity.read", ); +const unitsCapabilityV12 = geliosProviderPackageV12.capabilities.find( + (capability) => capability.id === "gelios.units.current.read", +); assert.equal(identityCapability.request.response.collectionPaths.includes("items"), false); assert.deepEqual(identityCapabilityV12.request.response.collectionPaths, [ "items", ...identityCapability.request.response.collectionPaths, ]); +assert.deepEqual(unitsCapabilityV12.request.response.collectionPaths, [ + "items", + ...geliosProviderPackageV11.capabilities.find( + (capability) => capability.id === "gelios.units.current.read", + ).request.response.collectionPaths, +]); assert.equal(Object.isFrozen(geliosProviderPackageV12), true); assert.equal(Object.isFrozen(identityCapabilityV12.request.response.collectionPaths), true); +assert.equal(Object.isFrozen(unitsCapabilityV12.request.response.collectionPaths), true); +const directoryCapabilityV13 = geliosProviderPackageV13.capabilities.find( + (capability) => capability.id === "gelios.users.directory.read", +); +assert.equal(directoryCapabilityV13.request.path, "/api/v1/users"); +assert.deepEqual(directoryCapabilityV13.request.response.collectionPaths, ["items"]); +assert.equal(directoryCapabilityV13.request.response.pagination.maxItems, 5000); +const contactsProductV2 = geliosProviderPackageV13.dataProducts.find( + (product) => product.id === "fleet.units.contacts.current.v2", +); +const registeredContactsProductV2 = JSON.parse(await readFile(new URL( + "../../../services/external-data-plane/definitions/fleet.units.contacts.current.v2.json", + import.meta.url, +), "utf8")); +assert.deepEqual(contactsProductV2, registeredContactsProductV2); +const contactsProfileV2 = geliosProviderPackageV13.collectionProfiles.find( + (profile) => profile.id === "gelios.units.contacts.warm.v2", +); +assert.deepEqual(contactsProfileV2.capabilityIds, [ + "gelios.units.identity.read", + "gelios.users.directory.read", +]); +const contactsMappingV2 = geliosProviderPackageV13.mappingContracts.find( + (mapping) => mapping.id === "gelios.units.to.fleet.units.contacts.current.v2", +); +assert.equal( + contactsMappingV2.derivations.available_user_emails.kind, + "bounded_response_lookup", +); +assert.equal( + contactsMappingV2.derivations.provider_creator_email.parameters.resultMode, + "first", +); +assert.equal(Object.isFrozen(geliosProviderPackageV13), true); const identityMapping = geliosProviderPackageV11.mappingContracts.find( (mapping) => mapping.id === "gelios.units.to.fleet.units.identity.current.v1", ); diff --git a/services/external-data-plane/definitions/fleet.units.contacts.current.v2.json b/services/external-data-plane/definitions/fleet.units.contacts.current.v2.json new file mode 100644 index 0000000..5b1595b --- /dev/null +++ b/services/external-data-plane/definitions/fleet.units.contacts.current.v2.json @@ -0,0 +1,45 @@ +{ + "id": "fleet.units.contacts.current.v2", + "version": "2.0.0", + "ontologyRevision": "ontology.map.moving_object.v3", + "deliveryMode": "snapshot+patch", + "semanticTypes": [ + "map.moving_object" + ], + "fields": [ + "available_user_emails", + "available_user_ids", + "available_user_legal_names", + "available_user_logins", + "available_user_phones", + "device_imei", + "device_phone_primary", + "device_phone_secondary", + "display_name", + "provider_creator_email", + "provider_creator_id", + "provider_creator_legal_name", + "provider_creator_login", + "provider_creator_phone" + ], + "fieldContracts": { + "available_user_emails": { "type": "string_array", "required": false }, + "available_user_ids": { "type": "string_array", "required": false }, + "available_user_legal_names": { "type": "string_array", "required": false }, + "available_user_logins": { "type": "string_array", "required": false }, + "available_user_phones": { "type": "string_array", "required": false }, + "device_imei": { "type": "string", "required": false }, + "device_phone_primary": { "type": "string", "required": false }, + "device_phone_secondary": { "type": "string", "required": false }, + "display_name": { "type": "string", "required": true }, + "provider_creator_email": { "type": "string", "required": false }, + "provider_creator_id": { "type": "string", "required": false }, + "provider_creator_legal_name": { "type": "string", "required": false }, + "provider_creator_login": { "type": "string", "required": false }, + "provider_creator_phone": { "type": "string", "required": false } + }, + "history": { + "mode": "none", + "retentionDays": 1 + } +} diff --git a/services/external-data-plane/test/definitions.test.mjs b/services/external-data-plane/test/definitions.test.mjs index cfabe70..8773390 100644 --- a/services/external-data-plane/test/definitions.test.mjs +++ b/services/external-data-plane/test/definitions.test.mjs @@ -13,6 +13,7 @@ assert.deepEqual(bundled.map((definition) => definition.id), [ "fleet.positions.current.v4", "fleet.positions.current.v5", "fleet.units.contacts.current.v1", + "fleet.units.contacts.current.v2", "fleet.units.identity.current.v1", "fleet.units.profile.current.v1", "map.zones.current.v1", @@ -59,27 +60,33 @@ assert.equal(bundled[5].ontologyRevision, "ontology.map.moving_object.v3"); assert.deepEqual(bundled[5].semanticTypes, ["map.moving_object"]); assert.equal(bundled[5].history.mode, "none"); assert.equal(bundled[5].fieldContracts.device_phone_primary.type, "string"); -assert.equal(bundled[6].version, "1.0.0"); +assert.equal(bundled[6].version, "2.0.0"); assert.equal(bundled[6].ontologyRevision, "ontology.map.moving_object.v3"); assert.deepEqual(bundled[6].semanticTypes, ["map.moving_object"]); assert.equal(bundled[6].history.mode, "none"); -assert.equal(bundled[6].fieldContracts.available_user_logins.type, "string_array"); -assert.equal(bundled[6].fieldContracts.device_imei.type, "string"); +assert.equal(bundled[6].fieldContracts.available_user_emails.type, "string_array"); +assert.equal(bundled[6].fieldContracts.provider_creator_email.type, "string"); assert.equal(bundled[7].version, "1.0.0"); assert.equal(bundled[7].ontologyRevision, "ontology.map.moving_object.v3"); assert.deepEqual(bundled[7].semanticTypes, ["map.moving_object"]); assert.equal(bundled[7].history.mode, "none"); -assert.equal(bundled[7].fieldContracts.hardware_port.type, "number"); +assert.equal(bundled[7].fieldContracts.available_user_logins.type, "string_array"); +assert.equal(bundled[7].fieldContracts.device_imei.type, "string"); assert.equal(bundled[8].version, "1.0.0"); -assert.equal(bundled[8].ontologyRevision, "ontology.map.zone.v1"); -assert.deepEqual(bundled[8].semanticTypes, ["map.zone"]); -assert.deepEqual(bundled[8].fieldContracts.geometry, { type: "geometry", required: true }); -assert.equal(bundled[8].fields.includes("dataset_authority"), false); -assert.equal(bundled[9].version, "2.0.0"); +assert.equal(bundled[8].ontologyRevision, "ontology.map.moving_object.v3"); +assert.deepEqual(bundled[8].semanticTypes, ["map.moving_object"]); +assert.equal(bundled[8].history.mode, "none"); +assert.equal(bundled[8].fieldContracts.hardware_port.type, "number"); +assert.equal(bundled[9].version, "1.0.0"); assert.equal(bundled[9].ontologyRevision, "ontology.map.zone.v1"); assert.deepEqual(bundled[9].semanticTypes, ["map.zone"]); assert.deepEqual(bundled[9].fieldContracts.geometry, { type: "geometry", required: true }); -assert.deepEqual(bundled[9].fieldContracts.dataset_authority.enum, ["moscow-department-of-transport"]); +assert.equal(bundled[9].fields.includes("dataset_authority"), false); +assert.equal(bundled[10].version, "2.0.0"); +assert.equal(bundled[10].ontologyRevision, "ontology.map.zone.v1"); +assert.deepEqual(bundled[10].semanticTypes, ["map.zone"]); +assert.deepEqual(bundled[10].fieldContracts.geometry, { type: "geometry", required: true }); +assert.deepEqual(bundled[10].fieldContracts.dataset_authority.enum, ["moscow-department-of-transport"]); assert.equal(Object.keys(bundled[3].fieldContracts).length, bundled[3].fields.length); assert.equal(Object.keys(bundled[4].fieldContracts).length, bundled[4].fields.length); assert.equal(Object.keys(bundled[5].fieldContracts).length, bundled[5].fields.length); @@ -87,6 +94,7 @@ assert.equal(Object.keys(bundled[6].fieldContracts).length, bundled[6].fields.le assert.equal(Object.keys(bundled[7].fieldContracts).length, bundled[7].fields.length); assert.equal(Object.keys(bundled[8].fieldContracts).length, bundled[8].fields.length); assert.equal(Object.keys(bundled[9].fieldContracts).length, bundled[9].fields.length); +assert.equal(Object.keys(bundled[10].fieldContracts).length, bundled[10].fields.length); const directory = await mkdtemp(join(tmpdir(), "nodedc-edp-definitions-")); try {