feat(engine): register MCP profiles and provider transitions
This commit is contained in:
@@ -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"),
|
||||
};
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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])]),
|
||||
);
|
||||
}
|
||||
@@ -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])]),
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user