feat(deploy): add Engine ontology SDK transition
This commit is contained in:
parent
f67c49bc4d
commit
d85912b9d7
|
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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¶ms=%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
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
Loading…
Reference in New Issue