feat(deploy): add Engine ontology SDK transition

This commit is contained in:
Codex 2026-07-19 12:33:56 +03:00
parent f67c49bc4d
commit d85912b9d7
4 changed files with 598 additions and 3 deletions

View File

@ -16,6 +16,12 @@ const storeRelativePath = 'nodedc-source/server/engineAgents/store.js'
const predecessorSha256 = '52daa43499d6d9a97fe7ffa891edb9212b7791e733f91dd3ca686d42739b7e9a' const predecessorSha256 = '52daa43499d6d9a97fe7ffa891edb9212b7791e733f91dd3ca686d42739b7e9a'
const targetSha256 = '2e62654c2dc12905efcc83a9dff45a818dd7b47924a10600160835c4416540e9' const targetSha256 = '2e62654c2dc12905efcc83a9dff45a818dd7b47924a10600160835c4416540e9'
const readerExtendedSourceSha256 = 'debd351fe0b8c72b33b8c79909b8f339cbaf061b970576f3ae72df52ebaa211f' const readerExtendedSourceSha256 = 'debd351fe0b8c72b33b8c79909b8f339cbaf061b970576f3ae72df52ebaa211f'
const ontologyExtendedSourceSha256 = '4cd4bdd5958cfafee184e98a04fe12aa0c1cbe884326beaec63c99f9fff61285'
const frozenTargetArtifact = join(
workspaceRoot,
'deploy-artifacts/nodedc-engine-agent-full-grant-migration-20260717-002.tgz',
)
const frozenTargetArtifactSha256 = 'd104ee6e63d1fccb9069e2b3db5e0446907ee9bf9cb04f2387636c272f803d68'
const previouslyIssuedPatchIds = new Set([ const previouslyIssuedPatchIds = new Set([
'engine-agent-full-grant-migration-20260717-001', 'engine-agent-full-grant-migration-20260717-001',
]) ])
@ -30,7 +36,7 @@ if (previouslyIssuedPatchIds.has(patchId)) {
const source = join(engineRoot, storeRelativePath) const source = join(engineRoot, storeRelativePath)
const sourceInfo = await lstat(source) const sourceInfo = await lstat(source)
if (sourceInfo.isSymbolicLink() || !sourceInfo.isFile()) throw new Error('engine_agent_store_source_unsafe') if (sourceInfo.isSymbolicLink() || !sourceInfo.isFile()) throw new Error('engine_agent_store_source_unsafe')
const sourceBytes = materializeFrozenTarget(await readFile(source)) const sourceBytes = await materializeFrozenTarget(await readFile(source))
if (digest(sourceBytes) !== targetSha256) throw new Error('engine_agent_store_target_sha256_mismatch') if (digest(sourceBytes) !== targetSha256) throw new Error('engine_agent_store_target_sha256_mismatch')
const sourceText = sourceBytes.toString('utf8') const sourceText = sourceBytes.toString('utf8')
for (const required of [ for (const required of [
@ -121,9 +127,28 @@ function digest(bytes) {
return createHash('sha256').update(bytes).digest('hex') return createHash('sha256').update(bytes).digest('hex')
} }
function materializeFrozenTarget(bytes) { async function materializeFrozenTarget(bytes) {
const sourceSha256 = digest(bytes) const sourceSha256 = digest(bytes)
if (sourceSha256 === targetSha256) return bytes if (sourceSha256 === targetSha256) return bytes
if (sourceSha256 === ontologyExtendedSourceSha256) {
const artifactBytes = await readFile(frozenTargetArtifact)
if (digest(artifactBytes) !== frozenTargetArtifactSha256) {
throw new Error('engine_agent_store_frozen_target_artifact_sha256_mismatch')
}
const script = [
'import pathlib,sys,tarfile',
'p=pathlib.Path(sys.argv[1])',
"with tarfile.open(p,'r:gz') as t:",
" f=t.extractfile('payload/nodedc-source/server/engineAgents/store.js')",
" if f is None: raise SystemExit('member-unreadable')",
' sys.stdout.buffer.write(f.read())',
].join('\n')
const frozen = Buffer.from(run('python3', ['-c', script, frozenTargetArtifact]).stdout, 'utf8')
if (digest(frozen) !== targetSha256) {
throw new Error('engine_agent_store_frozen_target_sha256_mismatch')
}
return frozen
}
if (sourceSha256 !== readerExtendedSourceSha256) { if (sourceSha256 !== readerExtendedSourceSha256) {
throw new Error('engine_agent_store_target_sha256_mismatch') throw new Error('engine_agent_store_target_sha256_mismatch')
} }

View File

@ -0,0 +1,161 @@
#!/usr/bin/env node
import { createHash } from 'node:crypto'
import { spawnSync } from 'node:child_process'
import { cp, 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 here = dirname(fileURLToPath(import.meta.url))
const platformRoot = resolve(here, '../..')
const engineRoot = resolve(
process.env.NODEDC_ENGINE_SOURCE_ROOT || resolve(platformRoot, '../NODEDC_ENGINE_INFRA'),
)
const canonicalArtifactRoot = resolve(here, '../deploy-artifacts')
const artifactRoot = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || canonicalArtifactRoot)
const [transitionId = '20260718-005', ...extra] = process.argv.slice(2)
if (extra.length || !/^\d{8}-[0-9]{3}$/.test(transitionId)) {
throw new Error('usage: build-engine-mcp-ontology-sdk-artifact.mjs [YYYYMMDD-NNN]')
}
const id = `engine-mcp-control-plane-${transitionId}`
const target = join(artifactRoot, `nodedc-${id}.tgz`)
const predecessorArtifact = join(
canonicalArtifactRoot,
'nodedc-engine-mcp-control-plane-20260718-003.tgz',
)
const predecessorArtifactSha256 = '249aef9527666c562e9648b15737e66cc5c1dc7c0788b58ea714da270b5eb4ba'
const descriptorRel = 'nodedc-source/services/node-intelligence/activation.json'
const gatewayRel = 'nodedc-source/server/routes/engineAgentGateway.js'
const files = [
'nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs',
'nodedc-source/server/assets/engine-agent-npm/package.json',
'nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.5.tgz',
'nodedc-source/server/assets/provider-packages/v1/catalog.json',
'nodedc-source/server/dataProductPublishGrant/providerCatalog.js',
'nodedc-source/server/engineAgents/store.js',
gatewayRel,
descriptorRel,
]
const expectedSha256 = new Map([
[files[0], 'adff3e474c914680f7d36b204d1dc03e69dc8065fff1f80b6713526cfc970137'],
[files[1], '0741647e4f7f58f69f3021d367484609a8e1eb7b8727d6ad9639bd9906b7f455'],
[files[2], '72a1b2d41a12a298eaae63ba3334c7c66b6ca53de6a3f03a48607d4ff6da42fb'],
[files[3], '4800e1a1c2af5e4893b1a403c04e91f55689383457caff833edc9e35e8e7e37a'],
[files[4], 'd5511a8bd3b4238af88a89537661c9ca4c0126bca7b5ad225985e27ccdb2c64c'],
[files[5], '4cd4bdd5958cfafee184e98a04fe12aa0c1cbe884326beaec63c99f9fff61285'],
[files[6], '25fa013ddbfd7d0c7ece8c792daec5f345062757c85eb56cfbff45bf1e812152'],
])
await assertFresh(target)
assertSha(await readFile(predecessorArtifact), predecessorArtifactSha256, 'predecessor MCP artifact')
const stage = await mkdtemp(join(tmpdir(), 'nodedc-engine-mcp-ontology-sdk-'))
const payload = join(stage, 'payload')
try {
await mkdir(payload, { recursive: true })
for (const rel of files.slice(0, -1)) {
const source = join(engineRoot, rel)
const stat = await lstat(source)
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`source_boundary_invalid:${rel}`)
assertSha(await readFile(source), expectedSha256.get(rel), rel)
await mkdir(dirname(join(payload, rel)), { recursive: true })
await cp(source, join(payload, rel), { force: false })
}
const descriptor = JSON.parse(extractMember(
predecessorArtifact,
`payload/${descriptorRel}`,
))
if (
descriptor?.action !== 'activate'
|| descriptor?.releaseId !== '2.33.2-974a9fb3492f'
|| descriptor?.source?.gatewaySha256 !== '96c726dab5cf1341f74e6e1095d518058ca320e0dd5738e25bdbe75db1f4fc15'
|| descriptor?.source?.upstreamProjectionSha256 !== '761a874b102a938bc6018159ddacdaac71ad6ae08e9f0f8d7f3b58a0165a5131'
) throw new Error('mcp_control_plane_predecessor_descriptor_mismatch')
descriptor.source.gatewaySha256 = expectedSha256.get(gatewayRel)
await mkdir(dirname(join(payload, descriptorRel)), { recursive: true })
await writeFile(join(payload, descriptorRel), `${JSON.stringify(descriptor, null, 2)}\n`, 'utf8')
await writeFile(join(stage, 'manifest.env'), `id=${id}\ncomponent=engine\ntype=app-overlay\n`, 'utf8')
await writeFile(join(stage, 'files.txt'), `${files.join('\n')}\n`, 'utf8')
await mkdir(artifactRoot, { recursive: true })
run('python3', ['-c', canonicalTarScript(), target, stage])
const artifactSha256 = sha(await readFile(target))
console.log(JSON.stringify({
ok: true,
id,
artifact: target,
artifactSha256,
services: ['nodedc-backend'],
predecessorGatewaySha256: '96c726dab5cf1341f74e6e1095d518058ca320e0dd5738e25bdbe75db1f4fc15',
targetGatewaySha256: expectedSha256.get(gatewayRel),
mcpVersion: '0.6.0',
installerVersion: '0.1.5',
ontologyMcp: 'separate-read-only-proxy',
providerPackage: 'gelios.provider.v2',
providerCredential: 'httpQueryAuth',
preserved: ['n8n', 'L1', 'node-intelligence image', 'databases', 'provider credential values'],
files,
}, null, 2))
} finally {
await rm(stage, { recursive: true, force: true })
}
async function assertFresh(path) {
try {
await lstat(path)
} catch (error) {
if (error?.code === 'ENOENT') return
throw error
}
throw new Error('artifact_already_exists')
}
function extractMember(archive, member) {
const script = [
'import pathlib,sys,tarfile',
'p=pathlib.Path(sys.argv[1]); name=sys.argv[2]',
"with tarfile.open(p,'r:gz') as t:",
' m=t.getmember(name)',
" if not m.isfile(): raise SystemExit('member-not-file')",
' f=t.extractfile(m)',
" if f is None: raise SystemExit('member-unreadable')",
' sys.stdout.buffer.write(f.read())',
].join('\n')
return run('python3', ['-c', script, archive, member]).stdout
}
function canonicalTarScript() {
return [
'import gzip,io,pathlib,sys,tarfile',
'root=pathlib.Path(sys.argv[2])',
"with open(sys.argv[1],'wb') 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 assertSha(bytes, expected, label) {
const actual = sha(bytes)
if (!expected || actual !== expected) throw new Error(`${label.replaceAll(' ', '_')}_sha256_mismatch:${actual}`)
}
function sha(bytes) {
return createHash('sha256').update(bytes).digest('hex')
}
function run(command, args) {
const result = spawnSync(command, args, {
encoding: 'utf8',
maxBuffer: 128 * 1024 * 1024,
stdio: ['ignore', 'pipe', 'pipe'],
})
if (result.status !== 0) throw new Error(`${command}_failed:${result.stderr || result.stdout}`)
return result
}

View File

@ -174,6 +174,36 @@ ENGINE_MCP_CONTROL_PLANE_NEW_PATHS = (
"nodedc-source/server/dataProductReadGrant/store.js", "nodedc-source/server/dataProductReadGrant/store.js",
ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL, ENGINE_DATA_PRODUCT_READ_GRANT_OVERRIDE_REL,
) )
ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES = (
"nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs",
"nodedc-source/server/assets/engine-agent-npm/package.json",
"nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.5.tgz",
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js",
"nodedc-source/server/engineAgents/store.js",
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL,
ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL,
)
ENGINE_MCP_ONTOLOGY_SDK_PREDECESSOR_SHA256 = {
"nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs": "521098de69fe288a56bd4158c849c1055ba24be08e19f7a4111618d0e8138445",
"nodedc-source/server/assets/engine-agent-npm/package.json": "cbe113e9b10bb84b9ccbffa3e261908e58a8430c11abe2cf4fd5304741b04597",
"nodedc-source/server/assets/provider-packages/v1/catalog.json": "30f89052b6557a2444550bd6e7eed30747b2d9b7ff3a22d079d9905fe0a04702",
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "901b8fad80018ce177b34ced804b39cb140a47e831414057f484296b373c651d",
"nodedc-source/server/engineAgents/store.js": "debd351fe0b8c72b33b8c79909b8f339cbaf061b970576f3ae72df52ebaa211f",
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "96c726dab5cf1341f74e6e1095d518058ca320e0dd5738e25bdbe75db1f4fc15",
}
ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256 = {
"nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs": "adff3e474c914680f7d36b204d1dc03e69dc8065fff1f80b6713526cfc970137",
"nodedc-source/server/assets/engine-agent-npm/package.json": "0741647e4f7f58f69f3021d367484609a8e1eb7b8727d6ad9639bd9906b7f455",
"nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.5.tgz": "72a1b2d41a12a298eaae63ba3334c7c66b6ca53de6a3f03a48607d4ff6da42fb",
"nodedc-source/server/assets/provider-packages/v1/catalog.json": "4800e1a1c2af5e4893b1a403c04e91f55689383457caff833edc9e35e8e7e37a",
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js": "d5511a8bd3b4238af88a89537661c9ca4c0126bca7b5ad225985e27ccdb2c64c",
"nodedc-source/server/engineAgents/store.js": "4cd4bdd5958cfafee184e98a04fe12aa0c1cbe884326beaec63c99f9fff61285",
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "25fa013ddbfd7d0c7ece8c792daec5f345062757c85eb56cfbff45bf1e812152",
}
ENGINE_MCP_ONTOLOGY_SDK_NEW_PATHS = (
"nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.5.tgz",
)
ENGINE_CONTROL_PLANE_STATE_REL = "nodedc-control-plane" ENGINE_CONTROL_PLANE_STATE_REL = "nodedc-control-plane"
ENGINE_PUBLISH_GRANT_STATE_REL = f"{ENGINE_CONTROL_PLANE_STATE_REL}/publish-grants" ENGINE_PUBLISH_GRANT_STATE_REL = f"{ENGINE_CONTROL_PLANE_STATE_REL}/publish-grants"
ENGINE_READ_GRANT_STATE_REL = f"{ENGINE_CONTROL_PLANE_STATE_REL}/read-grants" ENGINE_READ_GRANT_STATE_REL = f"{ENGINE_CONTROL_PLANE_STATE_REL}/read-grants"
@ -2813,6 +2843,7 @@ def validate_engine_node_intelligence_transition(payload_dir, entries):
"const ENGINE_AGENT_MCP_VERSION = '0.3.0'", "const ENGINE_AGENT_MCP_VERSION = '0.3.0'",
"const ENGINE_AGENT_MCP_VERSION = '0.4.0'", "const ENGINE_AGENT_MCP_VERSION = '0.4.0'",
"const ENGINE_AGENT_MCP_VERSION = '0.5.0'", "const ENGINE_AGENT_MCP_VERSION = '0.5.0'",
"const ENGINE_AGENT_MCP_VERSION = '0.6.0'",
) )
): ):
die("Engine node-intelligence gateway MCP version is not registered") die("Engine node-intelligence gateway MCP version is not registered")
@ -3073,6 +3104,211 @@ process.stdout.write('engine-mcp-reader-grant:0.5.0:'+required.length+':'+scopes
} }
def is_engine_mcp_ontology_sdk_slice(component, entries):
return (
component == "engine"
and entries is not None
and tuple(entries) == ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES
)
def validate_engine_mcp_ontology_sdk_payload(payload_dir, entries):
if tuple(entries) != ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES:
die("Engine MCP Ontology/SDK files.txt exact set/order mismatch")
for rel, expected_sha256 in ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256.items():
path = payload_dir / rel
if path.is_symlink() or not path.is_file() or sha256_file(path) != expected_sha256:
die(f"Engine MCP Ontology/SDK target sha256 mismatch: {rel}")
gateway = (payload_dir / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL).read_text(
encoding="utf-8"
)
installer_path = (
payload_dir
/ "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs"
)
installer = installer_path.read_text(encoding="utf-8")
package_path = payload_dir / "nodedc-source/server/assets/engine-agent-npm/package.json"
package = read_strict_json(package_path, "Engine MCP Codex installer package")
if package.get("name") != "@nodedc/engine-codex-agent" or package.get("version") != "0.1.5":
die("Engine MCP Ontology/SDK installer package identity mismatch")
if any(
marker not in gateway
for marker in (
"const ENGINE_AGENT_MCP_VERSION = '0.6.0'",
"authenticateEngineAgentOntologyToken",
"apiRouter.post('/ontology-mcp'",
"process.env.NODEDC_INTERNAL_ACCESS_TOKEN",
"serverName: 'nodedc-engine-agent'",
"serverName: 'nodedc_ontology'",
)
):
die("Engine MCP separate Ontology gateway contract is incomplete")
if any(
marker not in installer
for marker in (
"const ENGINE_SERVER_NAME = 'nodedc-engine-agent'",
"const ONTOLOGY_SERVER_NAME = 'nodedc_ontology'",
"Ontology tools/catalog are never embedded or multiplexed into Engine MCP",
"version: '0.1.5'",
)
):
die("Engine MCP separate Ontology installer contract is incomplete")
archive_path = payload_dir / "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.5.tgz"
expected_members = {
"package/bin/nodedc-engine-codex-agent.mjs": installer_path.read_bytes(),
"package/package.json": package_path.read_bytes(),
}
observed_members = {}
try:
with tarfile.open(archive_path, "r:gz") as archive:
for member in archive:
if not (member.isfile() or member.isdir()) or member.name.startswith("/"):
die("Engine MCP Ontology/SDK installer archive member is unsafe")
if member.isfile():
source = archive.extractfile(member)
if source is None:
die("Engine MCP Ontology/SDK installer archive member is unreadable")
observed_members[member.name] = source.read(MAX_FILE_BYTES + 1)
except (OSError, tarfile.TarError):
die("Engine MCP Ontology/SDK installer archive is invalid")
if observed_members != expected_members:
die("Engine MCP Ontology/SDK installer archive/source equality mismatch")
catalog = read_strict_json(
payload_dir / "nodedc-source/server/assets/provider-packages/v1/catalog.json",
"Engine provider security catalog",
)
packages = catalog.get("packages") if isinstance(catalog, dict) else None
if not isinstance(packages, list) or len(packages) != 1:
die("Engine provider security catalog must contain one active package")
provider = packages[0]
capabilities = provider.get("capabilities") if isinstance(provider, dict) else None
capability = capabilities[0] if isinstance(capabilities, list) and len(capabilities) == 1 else None
request = capability.get("request") if isinstance(capability, dict) else None
if (
provider.get("id") != "gelios.provider.v2"
or provider.get("version") != "2.0.0"
or (provider.get("providerCredential") or {}).get("credentialType") != "httpQueryAuth"
or not isinstance(request, dict)
or request.get("method") != "GET"
or request.get("url") != "https://admin.geliospro.com/sdk/?svc=get_units&params=%7B%7D"
):
die("Engine Gelios SDK/query-auth security projection mismatch")
resolver = (
payload_dir / "nodedc-source/server/dataProductPublishGrant/providerCatalog.js"
).read_text(encoding="utf-8")
if "new Set(['httpBearerAuth', 'httpQueryAuth'])" not in resolver:
die("Engine provider credential type allowlist mismatch")
store = (payload_dir / "nodedc-source/server/engineAgents/store.js").read_text(
encoding="utf-8"
)
if any(
marker not in store
for marker in (
"const STORE_VERSION = 3",
"ontologyTokenHash",
"`ndc_eao_${",
"authenticateEngineAgentOntologyToken",
)
):
die("Engine separate Ontology token store migration is incomplete")
descriptor = read_engine_node_intelligence_descriptor(
payload_dir / ENGINE_MCP_CONTROL_PLANE_DESCRIPTOR_REL,
"Engine MCP Ontology/SDK node-intelligence descriptor",
)
if (
descriptor.get("action") != "activate"
or descriptor["source"].get("gatewaySha256")
!= ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL]
or descriptor["source"].get("upstreamProjectionSha256")
!= ENGINE_MCP_CONTROL_PLANE_TARGET_SHA256[
"nodedc-source/server/nodeIntelligence/upstreamProjection.js"
]
):
die("Engine MCP Ontology/SDK descriptor target mismatch")
return descriptor
def preflight_engine_mcp_ontology_sdk_predecessor(payload_dir):
candidate = validate_engine_mcp_ontology_sdk_payload(
payload_dir,
ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES,
)
installed = current_engine_node_intelligence_descriptor()
validate_installed_engine_node_intelligence_source(installed)
if installed is None or installed.get("action") != "activate":
die("Engine MCP Ontology/SDK requires active node intelligence")
expected_candidate = json.loads(json.dumps(installed))
expected_candidate["source"]["gatewaySha256"] = (
ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256[ENGINE_NODE_INTELLIGENCE_GATEWAY_REL]
)
if candidate != expected_candidate:
die("Engine MCP Ontology/SDK descriptor crosses node-intelligence source boundary")
root = component_root("engine")
for rel, expected_sha256 in ENGINE_MCP_ONTOLOGY_SDK_PREDECESSOR_SHA256.items():
path = root / rel
if path.is_symlink() or not path.is_file() or sha256_file(path) != expected_sha256:
die(f"Engine MCP Ontology/SDK predecessor drift detected: {rel}")
for rel in ENGINE_MCP_ONTOLOGY_SDK_NEW_PATHS:
path = root / rel
if path.exists() or path.is_symlink():
die(f"Engine MCP Ontology/SDK new path already exists: {rel}")
backend = preflight_engine_credential_backend_runtime()
if backend["mode"] != "verified-derived-retry":
die("Engine MCP Ontology/SDK requires the active immutable backend runtime")
return {
"descriptor": candidate,
"predecessor_gateway_sha256": installed["source"]["gatewaySha256"],
"target_gateway_sha256": candidate["source"]["gatewaySha256"],
"backend_mode": backend["mode"],
}
def accept_engine_mcp_ontology_sdk_runtime():
root = component_root("engine")
descriptor = validate_engine_mcp_ontology_sdk_payload(
root,
ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES,
)
installed = current_engine_node_intelligence_descriptor()
if installed != descriptor:
die("Engine MCP Ontology/SDK installed descriptor equality failed")
validate_installed_engine_node_intelligence_source(installed)
backend = preflight_engine_credential_backend_runtime()
if backend["mode"] != "verified-derived-retry":
die("Engine MCP Ontology/SDK backend runtime acceptance failed")
live = run_engine_backend_probe(
(
"node",
"--input-type=module",
"-e",
"""
const gateway=await import('file:///app/server/routes/engineAgentGateway.js');
const store=await import('file:///app/server/engineAgents/store.js');
const catalog=JSON.parse(await (await import('node:fs/promises')).readFile('/app/server/assets/provider-packages/v1/catalog.json','utf8'));
const provider=catalog.packages?.[0];
if(gateway.ENGINE_AGENT_MCP_VERSION!=='0.6.0')process.exit(2);
if(typeof store.authenticateEngineAgentOntologyToken!=='function')process.exit(3);
if(provider?.id!=='gelios.provider.v2'||provider?.providerCredential?.credentialType!=='httpQueryAuth')process.exit(4);
process.stdout.write('engine-mcp-ontology-sdk:0.6.0:0.1.5:gelios.provider.v2:httpQueryAuth');
""".strip(),
),
"Engine MCP Ontology/SDK capability",
container_id=engine_backend_container_id(),
)
expected = "engine-mcp-ontology-sdk:0.6.0:0.1.5:gelios.provider.v2:httpQueryAuth"
if live != expected:
die("Engine MCP Ontology/SDK live capability acceptance mismatch")
return {
"gateway_sha256": descriptor["source"]["gatewaySha256"],
"backend_mode": backend["mode"],
"capability": live,
}
def validate_no_lifecycle_scripts(payload_dir, label): def validate_no_lifecycle_scripts(payload_dir, label):
forbidden = ("preinstall", "install", "postinstall", "prepare", "prepack", "postpack") forbidden = ("preinstall", "install", "postinstall", "prepare", "prepack", "postpack")
for package_path in payload_dir.rglob("package.json"): for package_path in payload_dir.rglob("package.json"):
@ -3491,16 +3727,20 @@ def load_artifact(artifact, work_dir):
touches_node_intelligence touches_node_intelligence
and not is_engine_node_intelligence_transition(manifest["component"], entries) and not is_engine_node_intelligence_transition(manifest["component"], entries)
and not is_engine_mcp_control_plane_slice(manifest["component"], entries) and not is_engine_mcp_control_plane_slice(manifest["component"], entries)
and not is_engine_mcp_ontology_sdk_slice(manifest["component"], entries)
): ):
die("Engine node-intelligence payload requires the canonical transition") die("Engine node-intelligence payload requires the canonical transition")
if is_engine_node_intelligence_transition(manifest["component"], entries): if is_engine_node_intelligence_transition(manifest["component"], entries):
validate_engine_node_intelligence_transition(payload_dir, entries) validate_engine_node_intelligence_transition(payload_dir, entries)
if is_engine_mcp_control_plane_slice(manifest["component"], entries): if is_engine_mcp_control_plane_slice(manifest["component"], entries):
validate_engine_mcp_control_plane_payload(payload_dir, entries) validate_engine_mcp_control_plane_payload(payload_dir, entries)
if is_engine_mcp_ontology_sdk_slice(manifest["component"], entries):
validate_engine_mcp_ontology_sdk_payload(payload_dir, entries)
if touches_engine_credential_sink(manifest["component"], entries): if touches_engine_credential_sink(manifest["component"], entries):
validate_engine_credential_sink_slice(payload_dir, entries) validate_engine_credential_sink_slice(payload_dir, entries)
if ( if (
not is_engine_mcp_control_plane_slice(manifest["component"], entries) not is_engine_mcp_control_plane_slice(manifest["component"], entries)
and not is_engine_mcp_ontology_sdk_slice(manifest["component"], entries)
and ( and (
touches_engine_data_product_publish_grant(entries) touches_engine_data_product_publish_grant(entries)
or ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL in entries or ENGINE_DATA_PRODUCT_PUBLISH_GRANT_OVERRIDE_REL in entries
@ -3509,6 +3749,7 @@ def load_artifact(artifact, work_dir):
validate_engine_data_product_publish_grant_slice(payload_dir, entries) validate_engine_data_product_publish_grant_slice(payload_dir, entries)
elif ( elif (
not is_engine_mcp_control_plane_slice(manifest["component"], entries) not is_engine_mcp_control_plane_slice(manifest["component"], entries)
and not is_engine_mcp_ontology_sdk_slice(manifest["component"], entries)
and ENGINE_AGENT_FULL_GRANT_MIGRATION_STORE_REL in entries and ENGINE_AGENT_FULL_GRANT_MIGRATION_STORE_REL in entries
): ):
validate_engine_agent_full_grant_migration_slice(payload_dir, entries) validate_engine_agent_full_grant_migration_slice(payload_dir, entries)
@ -3857,7 +4098,10 @@ def is_platform_provider_catalog_only(entries):
def component_services(component, entries=None): def component_services(component, entries=None):
if is_engine_mcp_control_plane_slice(component, entries): if (
is_engine_mcp_control_plane_slice(component, entries)
or is_engine_mcp_ontology_sdk_slice(component, entries)
):
# This slice updates only the existing Engine backend control plane. # This slice updates only the existing Engine backend control plane.
# Node intelligence keeps the same immutable sidecar image and n8n/L1 # Node intelligence keeps the same immutable sidecar image and n8n/L1
# retain their current generations. # retain their current generations.
@ -5992,6 +6236,7 @@ def plan_artifact(artifact):
publish_grant_preflight = None publish_grant_preflight = None
node_intelligence_preflight = None node_intelligence_preflight = None
mcp_control_plane_preflight = None mcp_control_plane_preflight = None
mcp_ontology_sdk_preflight = None
with tempfile.TemporaryDirectory(prefix="plan-", dir=TMP_DIR) as tmp: with tempfile.TemporaryDirectory(prefix="plan-", dir=TMP_DIR) as tmp:
manifest, entries, payload_dir = load_artifact(artifact, Path(tmp)) manifest, entries, payload_dir = load_artifact(artifact, Path(tmp))
transition_descriptor = None transition_descriptor = None
@ -6016,6 +6261,10 @@ def plan_artifact(artifact):
mcp_control_plane_preflight = preflight_engine_mcp_control_plane_predecessor( mcp_control_plane_preflight = preflight_engine_mcp_control_plane_predecessor(
payload_dir payload_dir
) )
if is_engine_mcp_ontology_sdk_slice(manifest["component"], entries):
mcp_ontology_sdk_preflight = preflight_engine_mcp_ontology_sdk_predecessor(
payload_dir
)
component = manifest["component"] component = manifest["component"]
root = component_root(component) root = component_root(component)
@ -6098,6 +6347,24 @@ def plan_artifact(artifact):
print(f"backend_current_barrier={mcp_control_plane_preflight['backend_mode']}") print(f"backend_current_barrier={mcp_control_plane_preflight['backend_mode']}")
print("node_intelligence_image=preserved") print("node_intelligence_image=preserved")
print("n8n_l1=untouched") print("n8n_l1=untouched")
if mcp_ontology_sdk_preflight:
print("engine_mcp_transition=separate-ontology+gelios-sdk-query-auth")
print("engine_mcp_version=0.6.0")
print("engine_mcp_installer=0.1.5")
print("engine_mcp_ontology=separate-read-only-proxy")
print("engine_provider_package=gelios.provider.v2")
print("engine_provider_credential=httpQueryAuth")
print(
"predecessor_gateway_sha256="
f"{mcp_ontology_sdk_preflight['predecessor_gateway_sha256']}"
)
print(
"target_gateway_sha256="
f"{mcp_ontology_sdk_preflight['target_gateway_sha256']}"
)
print(f"backend_current_barrier={mcp_ontology_sdk_preflight['backend_mode']}")
print("node_intelligence_image=preserved")
print("n8n_l1=untouched")
if transition_descriptor: if transition_descriptor:
print(f"n8n_transition={transition_descriptor['action']}") print(f"n8n_transition={transition_descriptor['action']}")
print(f"n8n_version={transition_descriptor['n8nVersion']}") print(f"n8n_version={transition_descriptor['n8nVersion']}")
@ -7049,6 +7316,7 @@ def component_healthchecks(component, entries=None, services=None):
is_engine_data_product_publish_grant_slice(component, entries) is_engine_data_product_publish_grant_slice(component, entries)
or is_engine_agent_full_grant_migration_slice(component, entries) or is_engine_agent_full_grant_migration_slice(component, entries)
or is_engine_mcp_control_plane_slice(component, entries) or is_engine_mcp_control_plane_slice(component, entries)
or is_engine_mcp_ontology_sdk_slice(component, entries)
) )
and services is not None and services is not None
): ):
@ -7362,11 +7630,13 @@ def run_healthchecks(component, entries=None, services=None):
entries, entries,
) )
touches_mcp_control_plane = is_engine_mcp_control_plane_slice(component, entries) touches_mcp_control_plane = is_engine_mcp_control_plane_slice(component, entries)
touches_mcp_ontology_sdk = is_engine_mcp_ontology_sdk_slice(component, entries)
if ( if (
touches_engine_credential_sink(component, entries) touches_engine_credential_sink(component, entries)
or touches_publish_grant or touches_publish_grant
or touches_agent_grant_migration or touches_agent_grant_migration
or touches_mcp_control_plane or touches_mcp_control_plane
or touches_mcp_ontology_sdk
): ):
# The HTTP endpoint can become ready before Docker publishes the first # The HTTP endpoint can become ready before Docker publishes the first
# successful health probe. Wait for the Compose health barrier before # successful health probe. Wait for the Compose health barrier before
@ -7379,6 +7649,7 @@ def run_healthchecks(component, entries=None, services=None):
or touches_publish_grant or touches_publish_grant
or touches_agent_grant_migration or touches_agent_grant_migration
or touches_mcp_control_plane or touches_mcp_control_plane
or touches_mcp_ontology_sdk
): ):
runtime = preflight_engine_credential_backend_runtime() runtime = preflight_engine_credential_backend_runtime()
if runtime["mode"] != "verified-derived-retry": if runtime["mode"] != "verified-derived-retry":
@ -7387,6 +7658,8 @@ def run_healthchecks(component, entries=None, services=None):
accept_engine_agent_full_grant_migration_runtime() accept_engine_agent_full_grant_migration_runtime()
if touches_mcp_control_plane: if touches_mcp_control_plane:
accept_engine_mcp_control_plane_runtime() accept_engine_mcp_control_plane_runtime()
if touches_mcp_ontology_sdk:
accept_engine_mcp_ontology_sdk_runtime()
container_name = COMPONENTS[component].get("health_container") container_name = COMPONENTS[component].get("health_container")
if container_name: if container_name:
healthcheck_container(container_name) healthcheck_container(container_name)
@ -7463,6 +7736,8 @@ def apply_artifact(artifact):
node_intelligence_descriptor = node_intelligence_preflight["descriptor"] node_intelligence_descriptor = node_intelligence_preflight["descriptor"]
if is_engine_mcp_control_plane_slice(component, entries): if is_engine_mcp_control_plane_slice(component, entries):
preflight_engine_mcp_control_plane_predecessor(payload_dir) preflight_engine_mcp_control_plane_predecessor(payload_dir)
if is_engine_mcp_ontology_sdk_slice(component, entries):
preflight_engine_mcp_ontology_sdk_predecessor(payload_dir)
if not artifact_only and not compose_root.is_dir(): if not artifact_only and not compose_root.is_dir():
if bootstrap_root and is_relative_to(compose_root.resolve(strict=False), root.resolve(strict=False)): if bootstrap_root and is_relative_to(compose_root.resolve(strict=False), root.resolve(strict=False)):
compose_root.mkdir(parents=True, exist_ok=True) compose_root.mkdir(parents=True, exist_ok=True)
@ -7640,6 +7915,7 @@ def apply_artifact(artifact):
is_engine_data_product_publish_grant_slice(component, entries) is_engine_data_product_publish_grant_slice(component, entries)
or is_engine_agent_full_grant_migration_slice(component, entries) or is_engine_agent_full_grant_migration_slice(component, entries)
or is_engine_mcp_control_plane_slice(component, entries) or is_engine_mcp_control_plane_slice(component, entries)
or is_engine_mcp_ontology_sdk_slice(component, entries)
) )
and services is not None and services is not None
): ):

View File

@ -0,0 +1,133 @@
#!/usr/bin/env python3
import hashlib
import importlib.machinery
import importlib.util
import json
import os
import subprocess
import tarfile
import tempfile
import unittest
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
PLATFORM_ROOT = SCRIPT_DIR.parent.parent
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
ENGINE_BUILDER = SCRIPT_DIR / "build-engine-mcp-ontology-sdk-artifact.mjs"
PLATFORM_BUILDER = SCRIPT_DIR / "build-platform-gelios-provider-v2-artifact.mjs"
ENGINE_ARTIFACT = (
PLATFORM_ROOT
/ "infra/deploy-artifacts/nodedc-engine-mcp-control-plane-20260718-005.tgz"
)
PLATFORM_ARTIFACT = (
PLATFORM_ROOT
/ "infra/deploy-artifacts/nodedc-platform-gelios-provider-v2-20260718-005.tgz"
)
ENGINE_SHA256 = "8d6e1581ebbba420984e2393b8c6f0f21e0b644f630c4ca6483a7cc4a0ae5c71"
PLATFORM_SHA256 = "e1b838ec265369af735766fc7d1bf23c5e05bd2d15ef65e2446b4f7efa7a1baa"
def load_runner():
loader = importlib.machinery.SourceFileLoader("nodedc_engine_mcp_ontology_sdk", 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 EngineMcpOntologySdkTest(unittest.TestCase):
def test_canonical_artifacts_have_pinned_digests_and_safe_members(self):
for artifact, expected in (
(ENGINE_ARTIFACT, ENGINE_SHA256),
(PLATFORM_ARTIFACT, PLATFORM_SHA256),
):
self.assertEqual(hashlib.sha256(artifact.read_bytes()).hexdigest(), expected)
self.assertEqual(artifact.read_bytes()[4:8], b"\0\0\0\0")
with tarfile.open(artifact, "r:gz") as archive:
for member in archive:
self.assertTrue(member.isfile() or member.isdir())
self.assertFalse(Path(member.name).name.startswith("._"))
def test_engine_builder_is_byte_reproducible(self):
outputs = []
with tempfile.TemporaryDirectory(prefix="nodedc-engine-mcp-ontology-sdk-") as directory:
for index in range(2):
target = Path(directory) / str(index)
target.mkdir()
env = os.environ.copy()
env["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(target)
result = subprocess.run(
["node", str(ENGINE_BUILDER), "20260718-005"],
cwd=PLATFORM_ROOT,
env=env,
check=True,
capture_output=True,
text=True,
)
outputs.append(Path(json.loads(result.stdout)["artifact"]).read_bytes())
self.assertEqual(outputs[0], outputs[1])
self.assertEqual(outputs[0], ENGINE_ARTIFACT.read_bytes())
def test_platform_builder_is_byte_reproducible(self):
outputs = []
with tempfile.TemporaryDirectory(prefix="nodedc-platform-gelios-v2-") as directory:
for index in range(2):
target = Path(directory) / str(index)
target.mkdir()
env = os.environ.copy()
env["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(target)
result = subprocess.run(
["node", str(PLATFORM_BUILDER), "20260718-005"],
cwd=PLATFORM_ROOT,
env=env,
check=True,
capture_output=True,
text=True,
)
outputs.append(Path(json.loads(result.stdout)["artifact"]).read_bytes())
self.assertEqual(outputs[0], outputs[1])
self.assertEqual(outputs[0], PLATFORM_ARTIFACT.read_bytes())
def test_engine_slice_is_exact_and_restarts_only_backend(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
manifest, entries, payload = RUNNER.load_artifact(ENGINE_ARTIFACT, root)
descriptor = RUNNER.validate_engine_mcp_ontology_sdk_payload(payload, entries)
self.assertEqual(manifest["component"], "engine")
self.assertEqual(tuple(entries), RUNNER.ENGINE_MCP_ONTOLOGY_SDK_ARTIFACT_ENTRIES)
self.assertEqual(RUNNER.component_services("engine", entries), ("nodedc-backend",))
self.assertEqual(RUNNER.component_builds("engine", entries), ())
self.assertEqual(
descriptor["source"]["gatewaySha256"],
RUNNER.ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256[
RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
],
)
def test_platform_provider_package_is_metadata_only(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
manifest, entries, _payload = RUNNER.load_artifact(PLATFORM_ARTIFACT, root)
self.assertEqual(manifest["component"], "platform")
self.assertTrue(RUNNER.is_platform_provider_catalog_only(entries))
self.assertEqual(RUNNER.component_services("platform", entries), ())
self.assertEqual(RUNNER.component_builds("platform", entries), ())
self.assertEqual(RUNNER.component_healthchecks("platform", entries, ()), ())
def test_engine_payload_tampering_is_rejected(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
RUNNER.safe_extract(ENGINE_ARTIFACT, root)
entries = RUNNER.parse_files_list(root / "files.txt")
catalog = root / "payload/nodedc-source/server/assets/provider-packages/v1/catalog.json"
catalog.write_text("{}\n", encoding="utf-8")
with self.assertRaisesRegex(RUNNER.DeployError, "target sha256 mismatch"):
RUNNER.validate_engine_mcp_ontology_sdk_payload(root / "payload", entries)
if __name__ == "__main__":
unittest.main(verbosity=2)