feat(engine): register MCP profiles and provider transitions

This commit is contained in:
Codex 2026-07-25 15:39:23 +03:00
parent 951b884c4b
commit e9e03143cd
21 changed files with 3573 additions and 28 deletions

View File

@ -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"),
};
}

View File

@ -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}`);
}
}

View File

@ -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}`);
}
}

View File

@ -12,6 +12,33 @@ import {
geliosTelemetryFieldRegistryV5, geliosTelemetryFieldRegistryV5,
} from "../../packages/external-provider-contract/providers/gelios/v12/index.mjs"; } 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( const engineRoot = resolve(
process.env.NODEDC_ENGINE_SOURCE_ROOT || "../NODEDC_ENGINE_INFRA", process.env.NODEDC_ENGINE_SOURCE_ROOT || "../NODEDC_ENGINE_INFRA",
); );
@ -72,9 +99,14 @@ function executionCatalogEntry() {
: {}, : {},
); );
const { packageDigest: _packageDigest, ...artifacts } = plan.artifacts; 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 { return {
id: profile.id, id: profile.id,
dataProductId: profile.dataProductId, dataProductId: profile.dataProductId,
...publicMetadata,
capabilityIds: [...profile.capabilityIds], capabilityIds: [...profile.capabilityIds],
stepSignatures: plan.steps.map((step) => ({ stepSignatures: plan.steps.map((step) => ({
id: step.id, id: step.id,
@ -87,6 +119,7 @@ function executionCatalogEntry() {
...(step.config.nodeType ? { nodeType: step.config.nodeType } : {}), ...(step.config.nodeType ? { nodeType: step.config.nodeType } : {}),
})), })),
artifacts, artifacts,
executionPlanTemplate: plan,
}; };
}); });
return { return {

View File

@ -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])]),
);
}

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -147,15 +147,15 @@ class EngineNodeIntelligenceTest(unittest.TestCase):
) )
self.assertEqual(actual, descriptor) 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: with tempfile.TemporaryDirectory() as directory:
payload, descriptor = self.make_activation_payload(Path(directory)) payload, descriptor = self.make_activation_payload(Path(directory))
gateway = payload / RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL gateway = payload / RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
gateway_text = gateway.read_text(encoding="utf-8") 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.write_text(
gateway_text.replace( 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'", "const ENGINE_AGENT_MCP_VERSION = '9.9.9'",
), ),
encoding="utf-8", encoding="utf-8",

View File

@ -7,3 +7,4 @@ export * from "./v9/index.mjs";
export { geliosProviderPackageV10 } from "./v10/index.mjs"; export { geliosProviderPackageV10 } from "./v10/index.mjs";
export { geliosProviderPackageV11 } from "./v11/index.mjs"; export { geliosProviderPackageV11 } from "./v11/index.mjs";
export { geliosProviderPackageV12 } from "./v12/index.mjs"; export { geliosProviderPackageV12 } from "./v12/index.mjs";
export { geliosProviderPackageV13 } from "./v13/index.mjs";

View File

@ -1,12 +1,14 @@
import { geliosProviderPackageV11 } from "../v11/package.mjs"; import { geliosProviderPackageV11 } from "../v11/package.mjs";
export const GELIOS_PROVIDER_PACKAGE_ID = "gelios.provider.v12"; 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_ID = "fleet.units.identity.current.v1";
export const GELIOS_UNIT_IDENTITY_DATA_PRODUCT_VERSION = "1.0.0"; export const GELIOS_UNIT_IDENTITY_DATA_PRODUCT_VERSION = "1.0.0";
export const GELIOS_UNIT_IDENTITY_ONTOLOGY_REVISION = "ontology.map.moving_object.v3"; 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 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_PROFILE_ID = "gelios.units.identity.warm.v1";
const IDENTITY_MAPPING_ID = "gelios.units.to.fleet.units.identity.current.v1"; const IDENTITY_MAPPING_ID = "gelios.units.to.fleet.units.identity.current.v1";
const IDENTITY_TEMPLATE_ID = "gelios.units.identity.l2.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, (capability) => capability.id === IDENTITY_CAPABILITY_ID,
); );
if (!identityCapability) throw new Error("gelios_v12_identity_capability_missing"); if (!identityCapability) throw new Error("gelios_v12_identity_capability_missing");
identityCapability.request.response.collectionPaths = [ prependItemsCollectionPath(identityCapability);
"items",
...identityCapability.request.response.collectionPaths.filter( const unitsCapability = value.capabilities.find(
(path) => path !== "items", (capability) => capability.id === UNITS_CAPABILITY_ID,
), );
]; if (!unitsCapability) throw new Error("gelios_v12_units_capability_missing");
prependItemsCollectionPath(unitsCapability);
const identityProfile = value.collectionProfiles.find( const identityProfile = value.collectionProfiles.find(
(profile) => profile.id === IDENTITY_PROFILE_ID, (profile) => profile.id === IDENTITY_PROFILE_ID,
); );
if (!identityProfile) throw new Error("gelios_v12_identity_profile_missing"); 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( const identityMapping = value.mappingContracts.find(
(mapping) => mapping.id === IDENTITY_MAPPING_ID, (mapping) => mapping.id === IDENTITY_MAPPING_ID,
); );
if (!identityMapping) throw new Error("gelios_v12_identity_mapping_missing"); 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( const identityTemplate = value.l2Templates.find(
(template) => template.id === IDENTITY_TEMPLATE_ID, (template) => template.id === IDENTITY_TEMPLATE_ID,
); );
if (!identityTemplate) throw new Error("gelios_v12_identity_template_missing"); 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); export const geliosProviderPackageV12 = deepFreeze(value);
function prependItemsCollectionPath(capability) {
capability.request.response.collectionPaths = [
"items",
...capability.request.response.collectionPaths.filter(
(path) => path !== "items",
),
];
}
function deepFreeze(input) { function deepFreeze(input) {
if (!input || typeof input !== "object" || Object.isFrozen(input)) return input; if (!input || typeof input !== "object" || Object.isFrozen(input)) return input;
Object.freeze(input); Object.freeze(input);

View File

@ -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";

View File

@ -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;
}

View File

@ -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_GRAPH_BLUEPRINT_SCHEMA_VERSION = "nodedc.l2-graph-blueprint/v1";
export const L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION = "nodedc.l2-materialization-receipt/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_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([ export const L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS = Object.freeze([
"1.1.0", "1.1.0",
"1.2.0", "1.2.0",
"1.3.0", "1.3.0",
"1.4.0",
L2_EXECUTION_PLAN_COMPILER_VERSION, L2_EXECUTION_PLAN_COMPILER_VERSION,
]); ]);

View File

@ -259,6 +259,14 @@ export function validateProviderPackage(value) {
if (!selectedCapabilityIds.includes(mapping.sourceCapabilityId)) { if (!selectedCapabilityIds.includes(mapping.sourceCapabilityId)) {
errors.push(`collectionProfile.${profile.id}.mapping_source_capability_must_be_selected`); 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.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`); 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) { for (const mapping of mappingContracts) {
assertReference(mapping.sourceCapabilityId, capabilityIds, `mappingContract.${mapping.id}.sourceCapabilityId`, errors); 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.fieldPolicyId, fieldPolicyIds, `mappingContract.${mapping.id}.fieldPolicyId`, errors);
assertReference(mapping.target?.dataProductId, dataProductIds, `mappingContract.${mapping.id}.target.dataProductId`, errors); assertReference(mapping.target?.dataProductId, dataProductIds, `mappingContract.${mapping.id}.target.dataProductId`, errors);
const product = dataProducts.find((item) => item.id === mapping.target?.dataProductId); const product = dataProducts.find((item) => item.id === mapping.target?.dataProductId);
@ -1049,6 +1067,7 @@ function validateDerivation(value, path, errors) {
"ordered_rules", "ordered_rules",
"flag_set", "flag_set",
"bounded_readings", "bounded_readings",
"bounded_response_lookup",
"bounded_string_list", "bounded_string_list",
"bounded_named_values", "bounded_named_values",
]).has(value.kind)) errors.push(`${path}.kind_invalid`); ]).has(value.kind)) errors.push(`${path}.kind_invalid`);
@ -1075,6 +1094,89 @@ function validateDerivation(value, path, errors) {
if (value.kind === "bounded_named_values") { if (value.kind === "bounded_named_values") {
validateBoundedNamedValuesDerivation(value, path, errors); 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) { function validateBoundedStringListDerivation(value, path, errors) {

View File

@ -31,9 +31,9 @@ const positionsPlan = compileL2ExecutionPlan(
); );
assert.equal(positionsPlan.schemaVersion, L2_EXECUTION_PLAN_SCHEMA_VERSION); assert.equal(positionsPlan.schemaVersion, L2_EXECUTION_PLAN_SCHEMA_VERSION);
assert.equal(L2_EXECUTION_PLAN_COMPILER_VERSION, "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"]); 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.4.0"); assert.equal(positionsPlan.compilerVersion, "1.5.0");
assert.deepEqual(validateL2ExecutionPlan(positionsPlan), { ok: true, errors: [] }); assert.deepEqual(validateL2ExecutionPlan(positionsPlan), { ok: true, errors: [] });
assert.match(positionsPlan.executionPlanDigest, /^sha256:[a-f0-9]{64}$/); assert.match(positionsPlan.executionPlanDigest, /^sha256:[a-f0-9]{64}$/);
assert.match(positionsPlan.artifacts.packageDigest, /^sha256:[a-f0-9]{64}$/); assert.match(positionsPlan.artifacts.packageDigest, /^sha256:[a-f0-9]{64}$/);

View File

@ -25,6 +25,7 @@ import { geliosProviderPackageV9 } from "../providers/gelios/v9/index.mjs";
import { geliosProviderPackageV10 } from "../providers/gelios/v10/index.mjs"; import { geliosProviderPackageV10 } from "../providers/gelios/v10/index.mjs";
import { geliosProviderPackageV11 } from "../providers/gelios/v11/index.mjs"; import { geliosProviderPackageV11 } from "../providers/gelios/v11/index.mjs";
import { geliosProviderPackageV12 } from "../providers/gelios/v12/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"; import { normalizeDataProductDefinition } from "../../../services/external-data-plane/src/data-product-policy.mjs";
const expectedFields = [ const expectedFields = [
@ -55,6 +56,7 @@ assert.deepEqual(validateProviderPackage(geliosProviderPackageV9), { ok: true, e
assert.deepEqual(validateProviderPackage(geliosProviderPackageV10), { ok: true, errors: [] }); assert.deepEqual(validateProviderPackage(geliosProviderPackageV10), { ok: true, errors: [] });
assert.deepEqual(validateProviderPackage(geliosProviderPackageV11), { ok: true, errors: [] }); assert.deepEqual(validateProviderPackage(geliosProviderPackageV11), { ok: true, errors: [] });
assert.deepEqual(validateProviderPackage(geliosProviderPackageV12), { 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.id, "gelios.provider.v9");
assert.equal(geliosProviderPackageV9.version, "9.0.0"); assert.equal(geliosProviderPackageV9.version, "9.0.0");
assert.equal(geliosProviderPackageV10.id, "gelios.provider.v10"); 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.id, "gelios.provider.v11");
assert.equal(geliosProviderPackageV11.version, "11.0.0"); assert.equal(geliosProviderPackageV11.version, "11.0.0");
assert.equal(geliosProviderPackageV12.id, "gelios.provider.v12"); 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"); assert.equal(geliosProviderPackageV8.id, "gelios.provider.v8");
const profileProduct = geliosProviderPackageV8.dataProducts.find((product) => product.id === "fleet.units.profile.current.v1"); 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( const identityCapabilityV12 = geliosProviderPackageV12.capabilities.find(
(capability) => capability.id === "gelios.units.identity.read", (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.equal(identityCapability.request.response.collectionPaths.includes("items"), false);
assert.deepEqual(identityCapabilityV12.request.response.collectionPaths, [ assert.deepEqual(identityCapabilityV12.request.response.collectionPaths, [
"items", "items",
...identityCapability.request.response.collectionPaths, ...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(geliosProviderPackageV12), true);
assert.equal(Object.isFrozen(identityCapabilityV12.request.response.collectionPaths), 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( const identityMapping = geliosProviderPackageV11.mappingContracts.find(
(mapping) => mapping.id === "gelios.units.to.fleet.units.identity.current.v1", (mapping) => mapping.id === "gelios.units.to.fleet.units.identity.current.v1",
); );

View File

@ -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
}
}

View File

@ -13,6 +13,7 @@ assert.deepEqual(bundled.map((definition) => definition.id), [
"fleet.positions.current.v4", "fleet.positions.current.v4",
"fleet.positions.current.v5", "fleet.positions.current.v5",
"fleet.units.contacts.current.v1", "fleet.units.contacts.current.v1",
"fleet.units.contacts.current.v2",
"fleet.units.identity.current.v1", "fleet.units.identity.current.v1",
"fleet.units.profile.current.v1", "fleet.units.profile.current.v1",
"map.zones.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.deepEqual(bundled[5].semanticTypes, ["map.moving_object"]);
assert.equal(bundled[5].history.mode, "none"); assert.equal(bundled[5].history.mode, "none");
assert.equal(bundled[5].fieldContracts.device_phone_primary.type, "string"); 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.equal(bundled[6].ontologyRevision, "ontology.map.moving_object.v3");
assert.deepEqual(bundled[6].semanticTypes, ["map.moving_object"]); assert.deepEqual(bundled[6].semanticTypes, ["map.moving_object"]);
assert.equal(bundled[6].history.mode, "none"); assert.equal(bundled[6].history.mode, "none");
assert.equal(bundled[6].fieldContracts.available_user_logins.type, "string_array"); assert.equal(bundled[6].fieldContracts.available_user_emails.type, "string_array");
assert.equal(bundled[6].fieldContracts.device_imei.type, "string"); assert.equal(bundled[6].fieldContracts.provider_creator_email.type, "string");
assert.equal(bundled[7].version, "1.0.0"); assert.equal(bundled[7].version, "1.0.0");
assert.equal(bundled[7].ontologyRevision, "ontology.map.moving_object.v3"); assert.equal(bundled[7].ontologyRevision, "ontology.map.moving_object.v3");
assert.deepEqual(bundled[7].semanticTypes, ["map.moving_object"]); assert.deepEqual(bundled[7].semanticTypes, ["map.moving_object"]);
assert.equal(bundled[7].history.mode, "none"); 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].version, "1.0.0");
assert.equal(bundled[8].ontologyRevision, "ontology.map.zone.v1"); assert.equal(bundled[8].ontologyRevision, "ontology.map.moving_object.v3");
assert.deepEqual(bundled[8].semanticTypes, ["map.zone"]); assert.deepEqual(bundled[8].semanticTypes, ["map.moving_object"]);
assert.deepEqual(bundled[8].fieldContracts.geometry, { type: "geometry", required: true }); assert.equal(bundled[8].history.mode, "none");
assert.equal(bundled[8].fields.includes("dataset_authority"), false); assert.equal(bundled[8].fieldContracts.hardware_port.type, "number");
assert.equal(bundled[9].version, "2.0.0"); assert.equal(bundled[9].version, "1.0.0");
assert.equal(bundled[9].ontologyRevision, "ontology.map.zone.v1"); assert.equal(bundled[9].ontologyRevision, "ontology.map.zone.v1");
assert.deepEqual(bundled[9].semanticTypes, ["map.zone"]); assert.deepEqual(bundled[9].semanticTypes, ["map.zone"]);
assert.deepEqual(bundled[9].fieldContracts.geometry, { type: "geometry", required: true }); 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[3].fieldContracts).length, bundled[3].fields.length);
assert.equal(Object.keys(bundled[4].fieldContracts).length, bundled[4].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); 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[7].fieldContracts).length, bundled[7].fields.length);
assert.equal(Object.keys(bundled[8].fieldContracts).length, bundled[8].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[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-")); const directory = await mkdtemp(join(tmpdir(), "nodedc-edp-definitions-"));
try { try {