feat(engine): register MCP profiles and provider transitions
This commit is contained in:
@@ -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])]),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user