feat(platform): complete the Gelios external data loop
This commit is contained in:
@@ -172,6 +172,28 @@ runtime and descriptor equality. The ordinary overlay backup is the automatic
|
||||
rollback source; rollback restores the predecessor gateway and descriptor
|
||||
together before recreating the same backend service.
|
||||
|
||||
The successor autonomy/provider-v5 slice keeps MCP authority explicit without
|
||||
turning every write into a second permission dialogue. MCP tool availability is
|
||||
the capability boundary, while the user's current objective is the intent
|
||||
boundary; Engine L2 may act autonomously only inside their intersection. A
|
||||
graph `plan` remains a mandatory machine barrier whose target, diff, revision
|
||||
and blockers are inspected by the agent. It is not a repeated approval prompt
|
||||
after an implementation objective is already authorized. Retries must add new
|
||||
evidence or change the attempted variant, and three identical failures without
|
||||
new evidence or state change are a critical stop. The slice also advances the
|
||||
installer to `0.1.6` and adds `gelios.provider.v5 ->
|
||||
fleet.positions.current.v4` beside the immutable v4/v3 rollback line:
|
||||
|
||||
```bash
|
||||
node infra/deploy-runner/build-engine-mcp-autonomy-provider-v5-artifact.mjs \
|
||||
20260720-004
|
||||
```
|
||||
|
||||
Its seven-entry allowlist recreates only `nodedc-backend`; n8n, L1, databases,
|
||||
credential values and the node-intelligence image remain outside the change.
|
||||
The runner accepts both the exact target and the exact predecessor after an
|
||||
automatic rollback, and rejects every mixed state.
|
||||
|
||||
For `platform` artifacts, the allowlist includes the versioned Ontology Core,
|
||||
the frozen legacy Gelios compatibility service, and the provider-neutral
|
||||
External Data Plane sources. The Gelios service remains reproducible only to
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
#!/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 here = dirname(fileURLToPath(import.meta.url));
|
||||
const workspaceRoot = resolve(here, "../../..");
|
||||
const engineRoot = resolve(
|
||||
process.env.NODEDC_ENGINE_SOURCE_ROOT || join(workspaceRoot, "NODEDC_ENGINE_INFRA"),
|
||||
);
|
||||
const artifactRoot = resolve(
|
||||
process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"),
|
||||
);
|
||||
const [patchId = "", ...extra] = process.argv.slice(2);
|
||||
if (extra.length || !/^engine-composite-provider-v4-\d{8}-\d{3}$/.test(patchId)) {
|
||||
throw new Error(
|
||||
"usage: build-engine-composite-provider-v4-artifact.mjs " +
|
||||
"<engine-composite-provider-v4-YYYYMMDD-NNN>",
|
||||
);
|
||||
}
|
||||
|
||||
const targetSha256 = Object.freeze({
|
||||
"nodedc-source/server/assets/provider-packages/v1/catalog.json":
|
||||
"9c931f9abfcadb5b34a8a854c2efb8fd79913e000eecf0967d1b7c500bf9a56a",
|
||||
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js":
|
||||
"689c6fbf695e582d983159973a21142787d1b19bb1d343da4c62c03092ac291f",
|
||||
"nodedc-source/server/dataProductPublishGrant/service.js":
|
||||
"83f045f4e0f332644310172ed51bb652d808bf04155b11c02e46bdffc95f7220",
|
||||
"nodedc-source/server/dataProductPublishGrant/store.js":
|
||||
"98662da6acd0489a9cae4b726eb61ce89d9b2c788b2ea95433e9c4f02930c095",
|
||||
});
|
||||
const entries = Object.freeze(Object.keys(targetSha256));
|
||||
const artifact = join(artifactRoot, `nodedc-${patchId}.tgz`);
|
||||
|
||||
await assertFresh(artifact);
|
||||
await assertExactSources();
|
||||
const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-composite-provider-v4-"));
|
||||
try {
|
||||
const payload = join(stage, "payload");
|
||||
for (const relativePath of entries) {
|
||||
const source = join(engineRoot, relativePath);
|
||||
const destination = join(payload, relativePath);
|
||||
await mkdir(dirname(destination), { recursive: true });
|
||||
await copyFile(source, destination, 0);
|
||||
}
|
||||
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(artifactRoot, { recursive: true });
|
||||
run("python3", ["-c", canonicalTarScript(), artifact, stage]);
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
patchId,
|
||||
artifact,
|
||||
sha256: digest(await readFile(artifact)),
|
||||
entries,
|
||||
targetSha256,
|
||||
services: ["nodedc-backend"],
|
||||
transition: "exact-v3-to-v4",
|
||||
providerPackage: "gelios.provider.v4",
|
||||
capabilities: [
|
||||
"gelios.monitoring_config.current.read",
|
||||
"gelios.units.current.read",
|
||||
],
|
||||
dataProductId: "fleet.positions.current.v3",
|
||||
credentialValues: "preserved",
|
||||
untouched: ["n8n", "L1 graph", "Engine UI", "databases"],
|
||||
}, null, 2));
|
||||
} finally {
|
||||
await rm(stage, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function assertExactSources() {
|
||||
for (const [relativePath, expected] of Object.entries(targetSha256)) {
|
||||
const path = join(engineRoot, relativePath);
|
||||
const info = await lstat(path);
|
||||
if (!info.isFile() || info.isSymbolicLink()) {
|
||||
throw new Error(`engine_composite_provider_source_unsafe:${relativePath}`);
|
||||
}
|
||||
const actual = digest(await readFile(path));
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`engine_composite_provider_target_mismatch:${relativePath}:` +
|
||||
`expected=${expected}:actual=${actual}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const catalog = JSON.parse(await readFile(join(engineRoot, entries[0]), "utf8"));
|
||||
const provider = catalog?.packages?.[0];
|
||||
const capabilityIds = provider?.capabilities?.map((item) => item.id);
|
||||
const requestUrls = provider?.capabilities?.map((item) => item.request?.url);
|
||||
if (
|
||||
catalog?.schemaVersion !== "nodedc.engine.provider-security-catalog/v1" ||
|
||||
provider?.id !== "gelios.provider.v4" ||
|
||||
provider?.version !== "4.0.0" ||
|
||||
provider?.providerCredential?.credentialType !== "httpBearerAuth" ||
|
||||
JSON.stringify(capabilityIds) !== JSON.stringify([
|
||||
"gelios.monitoring_config.current.read",
|
||||
"gelios.units.current.read",
|
||||
]) ||
|
||||
JSON.stringify(requestUrls) !== JSON.stringify([
|
||||
"https://api.geliospro.com/api/v1/users/me/monitoring-config",
|
||||
"https://api.geliospro.com/api/v1/units?incltrip=true",
|
||||
]) ||
|
||||
provider?.capabilities?.some(
|
||||
(item) => item.dataProductIds?.length !== 1 ||
|
||||
item.dataProductIds[0] !== "fleet.positions.current.v3",
|
||||
)
|
||||
) {
|
||||
throw new Error("engine_composite_provider_catalog_projection_mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${command}_failed:${result.stderr || result.stdout}`);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ const previouslyIssuedPatchIds = new Set([
|
||||
'engine-data-product-publish-grant-20260717-001',
|
||||
'engine-data-product-publish-grant-20260717-002',
|
||||
])
|
||||
const compositeProviderCatalogTargetSha256 = '9c931f9abfcadb5b34a8a854c2efb8fd79913e000eecf0967d1b7c500bf9a56a'
|
||||
|
||||
if (extra.length || !/^engine-data-product-publish-grant-\d{8}-\d{3}$/.test(patchId)) {
|
||||
throw new Error('usage: build-engine-data-product-publish-grant-artifact.mjs <fresh-patch-id>')
|
||||
@@ -109,6 +110,13 @@ try {
|
||||
'nodedc-source/dist',
|
||||
'nodedc-source/server/tests',
|
||||
],
|
||||
providerPackage: 'gelios.provider.v4',
|
||||
providerRequests: [
|
||||
'https://api.geliospro.com/api/v1/users/me/monitoring-config',
|
||||
'https://api.geliospro.com/api/v1/units?incltrip=true',
|
||||
],
|
||||
dataProductId: 'fleet.positions.current.v3',
|
||||
credentialValues: 'preserved',
|
||||
preservedPredecessor: Object.keys(credentialSinkPredecessorSha256),
|
||||
}, null, 2))
|
||||
} finally {
|
||||
@@ -116,10 +124,14 @@ try {
|
||||
}
|
||||
|
||||
async function assertSourceBoundary() {
|
||||
for (const [relativePath, expectedSha256] of Object.entries(credentialSinkPredecessorSha256)) {
|
||||
const bytes = await readFile(join(engineRoot, relativePath))
|
||||
if (digest(bytes) !== expectedSha256) {
|
||||
throw new Error(`engine_credential_sink_predecessor_drift:${relativePath}`)
|
||||
// These files are deliberately excluded from the artifact. Their exact
|
||||
// installed predecessor is verified by the root-owned runner during plan
|
||||
// and apply; the local builder only proves that it cannot package a link or
|
||||
// another unsafe filesystem object in their place.
|
||||
for (const relativePath of Object.keys(credentialSinkPredecessorSha256)) {
|
||||
const info = await lstat(join(engineRoot, relativePath))
|
||||
if (!info.isFile() || info.isSymbolicLink()) {
|
||||
throw new Error(`engine_credential_sink_predecessor_unsafe:${relativePath}`)
|
||||
}
|
||||
}
|
||||
const runtimeOverridePath = join(
|
||||
@@ -129,6 +141,13 @@ async function assertSourceBoundary() {
|
||||
if (await readFile(runtimeOverridePath, 'utf8') !== publishGrantRuntimeOverride) {
|
||||
throw new Error('engine_publish_grant_runtime_override_mismatch')
|
||||
}
|
||||
const catalogPath = join(
|
||||
engineRoot,
|
||||
'nodedc-source/server/assets/provider-packages/v1/catalog.json',
|
||||
)
|
||||
if (digest(await readFile(catalogPath)) !== compositeProviderCatalogTargetSha256) {
|
||||
throw new Error('engine_composite_provider_catalog_target_mismatch')
|
||||
}
|
||||
|
||||
const indexSource = await readFile(join(engineRoot, 'nodedc-source/server/index.js'), 'utf8')
|
||||
if (!indexSource.includes("app.use('/api/engine-agent-mcp', engineAgentMcpRouter)")) {
|
||||
@@ -175,6 +194,32 @@ async function assertSourceBoundary() {
|
||||
if (JSON.stringify(grantFiles) !== JSON.stringify(expectedGrantFiles)) {
|
||||
throw new Error('engine_publish_grant_source_set_mismatch')
|
||||
}
|
||||
const providerCatalogSource = await readFile(join(grantDirectory, 'providerCatalog.js'), 'utf8')
|
||||
const grantServiceSource = await readFile(join(grantDirectory, 'service.js'), 'utf8')
|
||||
const grantStoreSource = await readFile(join(grantDirectory, 'store.js'), 'utf8')
|
||||
for (const marker of [
|
||||
'function capabilityRequests(capability)',
|
||||
'function exactHttpRequestUrl(n8n)',
|
||||
'capabilityIds',
|
||||
'providerRequestNodeIds',
|
||||
'providerCredentialRefs.size !== 1',
|
||||
]) {
|
||||
if (!providerCatalogSource.includes(marker)) {
|
||||
throw new Error(`engine_composite_provider_resolver_missing:${marker}`)
|
||||
}
|
||||
}
|
||||
if (
|
||||
!grantServiceSource.includes('capabilities: descriptor.capabilityIds')
|
||||
|| !grantServiceSource.includes('providerRequestNodeIds: descriptor.providerRequestNodeIds')
|
||||
) {
|
||||
throw new Error('engine_composite_provider_plan_projection_missing')
|
||||
}
|
||||
if (
|
||||
!grantStoreSource.includes('Array.isArray(raw.capabilityIds)')
|
||||
|| !grantStoreSource.includes('Array.isArray(raw.providerRequestNodeIds)')
|
||||
) {
|
||||
throw new Error('engine_composite_provider_store_projection_missing')
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const source = resolve(engineRoot, entry)
|
||||
const info = await lstat(source)
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
#!/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 = '20260720-004', ...extra] = process.argv.slice(2)
|
||||
if (extra.length || !/^\d{8}-[0-9]{3}$/.test(transitionId)) {
|
||||
throw new Error('usage: build-engine-mcp-autonomy-provider-v5-artifact.mjs [YYYYMMDD-NNN]')
|
||||
}
|
||||
|
||||
const id = `engine-mcp-autonomy-provider-v5-${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.6.tgz',
|
||||
'nodedc-source/server/assets/provider-packages/v1/catalog.json',
|
||||
'nodedc-source/server/engineAgents/store.js',
|
||||
gatewayRel,
|
||||
descriptorRel,
|
||||
]
|
||||
const expectedSha256 = new Map([
|
||||
[files[0], 'c15c9da4f90f44a4e9e12f3683127e906d614fb98e562faa0c939505c973e074'],
|
||||
[files[1], '2ca8dcab0fa04bb1b21add1f75a9be61d5aa97753443ee1917302b4e0da5780a'],
|
||||
[files[2], 'd007a81cb4e4af569b3c54d3869b0f60b5597e3b531bff232145fa1851d8572a'],
|
||||
[files[3], '63e0741646197f0b1b3c64a4095e1bc8fb3a95ee6caf20b0293f89d869c9e620'],
|
||||
[files[4], '4cd4bdd5958cfafee184e98a04fe12aa0c1cbe884326beaec63c99f9fff61285'],
|
||||
[files[5], '5331f6dc8dc306f641370a2968eb025ee3e278e27ae9a217e4cfc2901fec369c'],
|
||||
])
|
||||
|
||||
await assertFresh(target)
|
||||
assertSha(await readFile(predecessorArtifact), predecessorArtifactSha256, 'predecessor MCP artifact')
|
||||
const stage = await mkdtemp(join(tmpdir(), 'nodedc-engine-mcp-autonomy-provider-v5-'))
|
||||
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_autonomy_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])
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
id,
|
||||
artifact: target,
|
||||
artifactSha256: sha(await readFile(target)),
|
||||
services: ['nodedc-backend'],
|
||||
mcpVersion: '0.6.0',
|
||||
installerVersion: '0.1.6',
|
||||
authority: 'mcp-capability-intersect-user-objective',
|
||||
retryBoundary: 'three-identical-failures-without-new-evidence',
|
||||
providerPackages: ['gelios.provider.v4', 'gelios.provider.v5'],
|
||||
targetDataProduct: 'fleet.positions.current.v4',
|
||||
preserved: ['n8n', 'L1', 'node-intelligence image', 'databases', '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],'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 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
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/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 here = dirname(fileURLToPath(import.meta.url));
|
||||
const workspaceRoot = resolve(here, "../../..");
|
||||
const engineRoot = resolve(
|
||||
process.env.NODEDC_ENGINE_SOURCE_ROOT || join(workspaceRoot, "NODEDC_ENGINE_INFRA"),
|
||||
);
|
||||
const artifactRoot = resolve(
|
||||
process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"),
|
||||
);
|
||||
const [patchId = "", ...extra] = process.argv.slice(2);
|
||||
if (extra.length || !/^engine-provider-authority-diagnostics-\d{8}-\d{3}$/.test(patchId)) {
|
||||
throw new Error(
|
||||
"usage: build-engine-provider-authority-diagnostics-artifact.mjs " +
|
||||
"<engine-provider-authority-diagnostics-YYYYMMDD-NNN>",
|
||||
);
|
||||
}
|
||||
|
||||
const targetSha256 = Object.freeze({
|
||||
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js":
|
||||
"1a14299167ebe17efd677fa3c59b84c80e12d6846bac6f40f9bcae0729ab22c6",
|
||||
});
|
||||
const entries = Object.freeze(Object.keys(targetSha256));
|
||||
const artifact = join(artifactRoot, `nodedc-${patchId}.tgz`);
|
||||
|
||||
await assertFresh(artifact);
|
||||
await assertExactSources();
|
||||
const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-provider-authority-diagnostics-"));
|
||||
try {
|
||||
const payload = join(stage, "payload");
|
||||
for (const relativePath of entries) {
|
||||
const destination = join(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(artifactRoot, { recursive: true });
|
||||
run("python3", ["-c", canonicalTarScript(), artifact, stage]);
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
patchId,
|
||||
artifact,
|
||||
sha256: digest(await readFile(artifact)),
|
||||
entries,
|
||||
targetSha256,
|
||||
services: ["nodedc-backend"],
|
||||
transition: "exact-safe-provider-authority-reason-codes",
|
||||
providerPackage: "gelios.provider.v4",
|
||||
dataProductId: "fleet.positions.current.v3",
|
||||
credentialValues: "preserved",
|
||||
untouched: ["L2 graph", "n8n", "L1", "Engine UI", "databases"],
|
||||
}, null, 2));
|
||||
} finally {
|
||||
await rm(stage, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function assertExactSources() {
|
||||
for (const [relativePath, expected] of Object.entries(targetSha256)) {
|
||||
const sourcePath = join(engineRoot, relativePath);
|
||||
const info = await lstat(sourcePath);
|
||||
if (!info.isFile() || info.isSymbolicLink()) {
|
||||
throw new Error(`engine_provider_authority_diagnostics_source_unsafe:${relativePath}`);
|
||||
}
|
||||
const actual = digest(await readFile(sourcePath));
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`engine_provider_authority_diagnostics_target_mismatch:${relativePath}:` +
|
||||
`expected=${expected}:actual=${actual}`,
|
||||
);
|
||||
}
|
||||
const source = await readFile(sourcePath, "utf8");
|
||||
for (const marker of [
|
||||
"publish_grant_provider_request_not_exact",
|
||||
"publish_grant_provider_credential_binding_invalid",
|
||||
"publish_grant_provider_transport_policy_not_exact",
|
||||
"publish_grant_provider_request_path_unreachable",
|
||||
"publish_grant_provider_credential_identity_mismatch",
|
||||
]) {
|
||||
if (!source.includes(marker)) {
|
||||
throw new Error(`engine_provider_authority_diagnostics_marker_missing:${marker}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${command}_failed:${result.stderr || result.stdout}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/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 here = dirname(fileURLToPath(import.meta.url));
|
||||
const workspaceRoot = resolve(here, "../../..");
|
||||
const engineRoot = resolve(
|
||||
process.env.NODEDC_ENGINE_SOURCE_ROOT || join(workspaceRoot, "NODEDC_ENGINE_INFRA"),
|
||||
);
|
||||
const artifactRoot = resolve(
|
||||
process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"),
|
||||
);
|
||||
const [patchId = "", ...extra] = process.argv.slice(2);
|
||||
if (extra.length || !/^engine-provider-rotating-slot-\d{8}-\d{3}$/.test(patchId)) {
|
||||
throw new Error(
|
||||
"usage: build-engine-provider-rotating-slot-artifact.mjs " +
|
||||
"<engine-provider-rotating-slot-YYYYMMDD-NNN>",
|
||||
);
|
||||
}
|
||||
|
||||
const targetSha256 = Object.freeze({
|
||||
"nodedc-source/server/assets/provider-packages/v1/catalog.json":
|
||||
"17f3e368f3264cbbd708965c9e1fd735aa974f15a1383bf88cd6d14a43dbf32d",
|
||||
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js":
|
||||
"f6cf5f4de9e57f87fb904e2136a9f34d494e1ffc289aec3ed9ed788b5583a062",
|
||||
});
|
||||
const entries = Object.freeze(Object.keys(targetSha256));
|
||||
const artifact = join(artifactRoot, `nodedc-${patchId}.tgz`);
|
||||
|
||||
await assertFresh(artifact);
|
||||
await assertExactSources();
|
||||
const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-provider-rotating-slot-"));
|
||||
try {
|
||||
const payload = join(stage, "payload");
|
||||
for (const relativePath of entries) {
|
||||
const destination = join(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(artifactRoot, { recursive: true });
|
||||
run("python3", ["-c", canonicalTarScript(), artifact, stage]);
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
patchId,
|
||||
artifact,
|
||||
sha256: digest(await readFile(artifact)),
|
||||
entries,
|
||||
targetSha256,
|
||||
services: ["nodedc-backend"],
|
||||
transition: "exact-active-credential-slot-alignment",
|
||||
providerPackage: "gelios.provider.v4",
|
||||
authModeId: "gelios.rest-rotating-bearer.v3",
|
||||
credentialSlot: "ndcProviderRotatingAccessApi",
|
||||
credentialValues: "preserved",
|
||||
untouched: ["L2 graph", "n8n", "L1", "Engine UI", "databases"],
|
||||
}, null, 2));
|
||||
} finally {
|
||||
await rm(stage, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function assertExactSources() {
|
||||
for (const [relativePath, expected] of Object.entries(targetSha256)) {
|
||||
const path = join(engineRoot, relativePath);
|
||||
const info = await lstat(path);
|
||||
if (!info.isFile() || info.isSymbolicLink()) {
|
||||
throw new Error(`engine_provider_rotating_slot_source_unsafe:${relativePath}`);
|
||||
}
|
||||
const actual = digest(await readFile(path));
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`engine_provider_rotating_slot_target_mismatch:${relativePath}:` +
|
||||
`expected=${expected}:actual=${actual}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const catalog = JSON.parse(await readFile(join(engineRoot, entries[0]), "utf8"));
|
||||
const provider = catalog?.packages?.[0];
|
||||
if (
|
||||
provider?.id !== "gelios.provider.v4" ||
|
||||
provider?.providerCredential?.authModeId !== "gelios.rest-rotating-bearer.v3" ||
|
||||
provider?.providerCredential?.credentialType !== "ndcProviderRotatingAccessApi"
|
||||
) {
|
||||
throw new Error("engine_provider_rotating_slot_catalog_projection_mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${command}_failed:${result.stderr || result.stdout}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from "node:crypto";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { 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 artifactRoot = resolve(
|
||||
process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"),
|
||||
);
|
||||
const [transitionId = "20260719-006", ...extra] = process.argv.slice(2);
|
||||
if (extra.length || !/^\d{8}-[0-9]{3}$/.test(transitionId)) {
|
||||
throw new Error("usage: build-engine-provider-security-catalog-artifact.mjs [YYYYMMDD-NNN]");
|
||||
}
|
||||
|
||||
const id = `engine-provider-security-catalog-${transitionId}`;
|
||||
const target = join(artifactRoot, `nodedc-${id}.tgz`);
|
||||
const file = "nodedc-source/server/assets/provider-packages/v1/catalog.json";
|
||||
const expectedSha256 = "992159fc457ec76ce1f45aad337604c8a72b29252d44fbccda515f0fd6ea6428";
|
||||
const catalogBytes = Buffer.from(`${JSON.stringify({
|
||||
schemaVersion: "nodedc.engine.provider-security-catalog/v1",
|
||||
packages: [{
|
||||
id: "gelios.provider.v3",
|
||||
version: "3.0.0",
|
||||
providerId: "gelios",
|
||||
providerCredential: {
|
||||
authModeId: "gelios.rest-rotating-bearer.v3",
|
||||
credentialType: "httpBearerAuth",
|
||||
},
|
||||
capabilities: [{
|
||||
id: "gelios.units.current.read",
|
||||
classification: "read",
|
||||
status: "implemented",
|
||||
request: {
|
||||
method: "GET",
|
||||
url: "https://api.geliospro.com/api/v1/units",
|
||||
},
|
||||
dataProductIds: ["fleet.positions.current.v2"],
|
||||
}],
|
||||
publisher: {
|
||||
nodeType: "n8n-nodes-ndc.ndcDataProductPublish",
|
||||
credentialType: "ndcDataProductWriterApi",
|
||||
},
|
||||
}],
|
||||
}, null, 2)}\n`, "utf8");
|
||||
|
||||
await assertFresh(target);
|
||||
if (sha(catalogBytes) !== expectedSha256) throw new Error(`pinned_catalog_sha256_mismatch:${file}`);
|
||||
|
||||
const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-provider-security-catalog-"));
|
||||
const payload = join(stage, "payload");
|
||||
try {
|
||||
await mkdir(dirname(join(payload, file)), { recursive: true });
|
||||
await writeFile(join(payload, file), catalogBytes, { flag: "wx", mode: 0o644 });
|
||||
await writeFile(join(stage, "manifest.env"), `id=${id}\ncomponent=engine\ntype=app-overlay\n`, "utf8");
|
||||
await writeFile(join(stage, "files.txt"), `${file}\n`, "utf8");
|
||||
await mkdir(artifactRoot, { recursive: true });
|
||||
run("python3", ["-c", canonicalTarScript(), target, stage]);
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
id,
|
||||
artifact: target,
|
||||
artifactSha256: sha(await readFile(target)),
|
||||
services: ["nodedc-backend"],
|
||||
providerPackage: "gelios.provider.v3",
|
||||
providerCredential: "httpBearerAuth",
|
||||
endpoint: "https://api.geliospro.com/api/v1/units",
|
||||
dataProductId: "fleet.positions.current.v2",
|
||||
preserved: ["n8n", "L1 graph", "Engine UI", "databases", "provider credential values"],
|
||||
files: [file],
|
||||
}, 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 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 sha(value) {
|
||||
return createHash("sha256").update(value).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;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/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 here = dirname(fileURLToPath(import.meta.url));
|
||||
const workspaceRoot = resolve(here, "../../..");
|
||||
const engineRoot = resolve(
|
||||
process.env.NODEDC_ENGINE_SOURCE_ROOT || join(workspaceRoot, "NODEDC_ENGINE_INFRA"),
|
||||
);
|
||||
const artifactRoot = resolve(
|
||||
process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"),
|
||||
);
|
||||
const [patchId = "", ...extra] = process.argv.slice(2);
|
||||
if (extra.length || !/^engine-provider-target-host-policy-\d{8}-\d{3}$/.test(patchId)) {
|
||||
throw new Error(
|
||||
"usage: build-engine-provider-target-host-policy-artifact.mjs " +
|
||||
"<engine-provider-target-host-policy-YYYYMMDD-NNN>",
|
||||
);
|
||||
}
|
||||
|
||||
const targetSha256 = Object.freeze({
|
||||
"nodedc-source/server/routes/n8n.js":
|
||||
"9bc3638e271102abec91bb80413329befb89d60c0e4dc0548f0dd11e93220d0a",
|
||||
});
|
||||
const entries = Object.freeze(Object.keys(targetSha256));
|
||||
const artifact = join(artifactRoot, `nodedc-${patchId}.tgz`);
|
||||
|
||||
await assertFresh(artifact);
|
||||
await assertExactSources();
|
||||
const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-provider-target-host-policy-"));
|
||||
try {
|
||||
const payload = join(stage, "payload");
|
||||
for (const relativePath of entries) {
|
||||
const destination = join(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(artifactRoot, { recursive: true });
|
||||
run("python3", ["-c", canonicalTarScript(), artifact, stage]);
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
patchId,
|
||||
artifact,
|
||||
sha256: digest(await readFile(artifact)),
|
||||
entries,
|
||||
targetSha256,
|
||||
services: ["nodedc-backend"],
|
||||
transition: "exact-provider-literal-target-host",
|
||||
providerPackage: "gelios.provider.v4",
|
||||
dataProductId: "fleet.positions.current.v3",
|
||||
credentialValues: "preserved",
|
||||
untouched: ["L2 graph", "n8n", "L1", "Engine UI", "databases"],
|
||||
}, null, 2));
|
||||
} finally {
|
||||
await rm(stage, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function assertExactSources() {
|
||||
for (const [relativePath, expected] of Object.entries(targetSha256)) {
|
||||
const sourcePath = join(engineRoot, relativePath);
|
||||
const info = await lstat(sourcePath);
|
||||
if (!info.isFile() || info.isSymbolicLink()) {
|
||||
throw new Error(`engine_provider_target_host_policy_source_unsafe:${relativePath}`);
|
||||
}
|
||||
const actual = digest(await readFile(sourcePath));
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`engine_provider_target_host_policy_target_mismatch:${relativePath}:` +
|
||||
`expected=${expected}:actual=${actual}`,
|
||||
);
|
||||
}
|
||||
const source = await readFile(sourcePath, "utf8");
|
||||
for (const marker of [
|
||||
"const eligibleHosts = explicitHosts.length",
|
||||
"allowedHosts: [targetHost]",
|
||||
"credential_host_not_allowed",
|
||||
]) {
|
||||
if (!source.includes(marker)) {
|
||||
throw new Error(`engine_provider_target_host_policy_marker_missing:${marker}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${command}_failed:${result.stderr || result.stdout}`);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,9 @@ const files = [
|
||||
["packages/external-provider-contract/src/data-product.mjs", "platform/packages/external-provider-contract/src/data-product.mjs"],
|
||||
["packages/external-provider-contract/src/intake-batch.mjs", "platform/packages/external-provider-contract/src/intake-batch.mjs"],
|
||||
["packages/external-provider-contract/src/index.mjs", "platform/packages/external-provider-contract/src/index.mjs"],
|
||||
["packages/external-provider-contract/src/provider-package.mjs", "platform/packages/external-provider-contract/src/provider-package.mjs"],
|
||||
["packages/external-provider-contract/src/sensitive-field-policy.mjs", "platform/packages/external-provider-contract/src/sensitive-field-policy.mjs"],
|
||||
["packages/external-provider-contract/providers/gelios", "platform/packages/external-provider-contract/providers/gelios"],
|
||||
];
|
||||
const ignoredBasenames = new Set([".DS_Store", ".git", "node_modules"]);
|
||||
const ignoredDirectoryNames = new Set(["test"]);
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/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 scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const platformRoot = resolve(scriptDir, "../..");
|
||||
const workspaceRoot = resolve(platformRoot, "..");
|
||||
const foundryRoot = resolve(workspaceRoot, "NODEDC_DESIGN_GUIDELINE");
|
||||
const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts"));
|
||||
const [patchId = "module-foundry-consumer-policy-v4-20260720-001", ...extra] = process.argv.slice(2);
|
||||
|
||||
if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) {
|
||||
throw new Error("patch_id_must_contain_only_letters_digits_dot_underscore_hyphen");
|
||||
}
|
||||
|
||||
const files = [
|
||||
"registry/data-product-consumer-policies.json",
|
||||
"scripts/validate-registry.mjs",
|
||||
"server/foundry-data-product-consumer.mjs",
|
||||
"server/foundry-data-product-consumer.test.mjs",
|
||||
];
|
||||
const stage = await mkdtemp(join(tmpdir(), "nodedc-module-foundry-consumer-policy-artifact-"));
|
||||
const payload = join(stage, "payload");
|
||||
const artifact = join(artifactDir, `nodedc-module-foundry-${patchId}.tgz`);
|
||||
const checksum = `${artifact}.sha256`;
|
||||
|
||||
await assertConsumerPolicyBoundary();
|
||||
|
||||
try {
|
||||
await mkdir(payload, { recursive: true });
|
||||
for (const relativePath of files) {
|
||||
const source = resolve(foundryRoot, relativePath);
|
||||
const sourceStat = await lstat(source);
|
||||
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
||||
throw new Error(`source_file_rejected:${relativePath}`);
|
||||
}
|
||||
const destination = join(payload, relativePath);
|
||||
await mkdir(dirname(destination), { recursive: true });
|
||||
await cp(source, destination, { force: true, verbatimSymlinks: true });
|
||||
}
|
||||
|
||||
await writeFile(join(stage, "manifest.env"), `id=${patchId}\ncomponent=module-foundry\ntype=app-overlay\n`, "utf8");
|
||||
await writeFile(join(stage, "files.txt"), `${files.join("\n")}\n`, "utf8");
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
|
||||
const tar = spawnSync("python3", ["-c", canonicalTarScript(), artifact, stage], {
|
||||
encoding: "utf8",
|
||||
maxBuffer: 32 * 1024 * 1024,
|
||||
});
|
||||
if (tar.status !== 0) throw new Error(`tar_failed:${tar.stderr || tar.stdout}`);
|
||||
|
||||
const sha256 = createHash("sha256").update(await readFile(artifact)).digest("hex");
|
||||
await writeFile(checksum, `${sha256} ${artifact.split("/").at(-1)}\n`, "utf8");
|
||||
console.log(JSON.stringify({ ok: true, patchId, artifact, checksum, sha256, files }, null, 2));
|
||||
} finally {
|
||||
await rm(stage, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function assertConsumerPolicyBoundary() {
|
||||
const registry = JSON.parse(await readFile(resolve(foundryRoot, files[0]), "utf8"));
|
||||
if (registry?.schemaVersion !== "nodedc.foundry.data-product-consumer-policies/v1") {
|
||||
throw new Error("foundry_consumer_policy_registry_invalid");
|
||||
}
|
||||
const matches = registry.policies?.filter((policy) => (
|
||||
policy?.dataProductId === "fleet.positions.current.v4" && policy?.productVersion === "4.0.0"
|
||||
)) || [];
|
||||
if (matches.length !== 1) throw new Error("foundry_consumer_policy_v4_missing_or_ambiguous");
|
||||
const policy = matches[0];
|
||||
if (
|
||||
policy.id !== "map-moving-object-current-v4"
|
||||
|| policy.version !== "4.0.0"
|
||||
|| policy.staleAfterMs !== null
|
||||
|| policy.statusContract?.attribute !== "signal_state"
|
||||
|| JSON.stringify(policy.statusContract?.allowedValues) !== JSON.stringify(["active", "inactive"])
|
||||
|| policy.statusContract?.missing !== "reject"
|
||||
|| policy.statusContract?.freshness !== "none"
|
||||
|| JSON.stringify(policy.terminalStatuses) !== JSON.stringify(["inactive"])
|
||||
|| policy.removeMode !== "canonical-tombstone-or-snapshot-rebase"
|
||||
) throw new Error("foundry_consumer_policy_v4_contract_invalid");
|
||||
|
||||
const serializedPolicy = JSON.stringify(policy);
|
||||
if (/(provider|tenant|connection|endpoint|credential|token|secret|authorization)/i.test(serializedPolicy)) {
|
||||
throw new Error("foundry_consumer_policy_v4_transport_boundary_violation");
|
||||
}
|
||||
|
||||
const consumer = await readFile(resolve(foundryRoot, files[2]), "utf8");
|
||||
for (const marker of [
|
||||
"data_product_consumer_fact_status_invalid",
|
||||
"data_product_consumer_status_field_not_projected",
|
||||
'statusContract.freshness === "none"',
|
||||
]) {
|
||||
if (!consumer.includes(marker)) throw new Error(`foundry_consumer_policy_v4_runtime_guard_missing:${marker}`);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/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 scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const platformRoot = resolve(scriptDir, "../..");
|
||||
const workspaceRoot = resolve(platformRoot, "..");
|
||||
const foundryRoot = resolve(workspaceRoot, "NODEDC_DESIGN_GUIDELINE");
|
||||
const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts"));
|
||||
const [patchId = "module-foundry-filter-toggle-20260720-001", ...extra] = process.argv.slice(2);
|
||||
|
||||
if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) {
|
||||
throw new Error("patch_id_must_contain_only_letters_digits_dot_underscore_hyphen");
|
||||
}
|
||||
|
||||
const files = [
|
||||
"apps/catalog/src/MapFixturePreview.tsx",
|
||||
"apps/catalog/src/mapPresentationProfile.ts",
|
||||
"scripts/map-presentation-filters.test.mjs",
|
||||
];
|
||||
const stage = await mkdtemp(join(tmpdir(), "nodedc-module-foundry-filter-toggle-artifact-"));
|
||||
const payload = join(stage, "payload");
|
||||
const artifact = join(artifactDir, `nodedc-module-foundry-${patchId}.tgz`);
|
||||
const checksum = `${artifact}.sha256`;
|
||||
|
||||
await assertFilterToggleBoundary();
|
||||
|
||||
try {
|
||||
await mkdir(payload, { recursive: true });
|
||||
for (const relativePath of files) {
|
||||
const source = resolve(foundryRoot, relativePath);
|
||||
const sourceStat = await lstat(source);
|
||||
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
||||
throw new Error(`source_file_rejected:${relativePath}`);
|
||||
}
|
||||
const destination = join(payload, relativePath);
|
||||
await mkdir(dirname(destination), { recursive: true });
|
||||
await cp(source, destination, { force: true, verbatimSymlinks: true });
|
||||
}
|
||||
|
||||
await writeFile(join(stage, "manifest.env"), `id=${patchId}\ncomponent=module-foundry\ntype=app-overlay\n`, "utf8");
|
||||
await writeFile(join(stage, "files.txt"), `${files.join("\n")}\n`, "utf8");
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
|
||||
const tar = spawnSync("python3", ["-c", canonicalTarScript(), artifact, stage], {
|
||||
encoding: "utf8",
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
});
|
||||
if (tar.status !== 0) throw new Error(`tar_failed:${tar.stderr || tar.stdout}`);
|
||||
|
||||
const sha256 = createHash("sha256").update(await readFile(artifact)).digest("hex");
|
||||
await writeFile(checksum, `${sha256} ${artifact.split("/").at(-1)}\n`, "utf8");
|
||||
console.log(JSON.stringify({ ok: true, patchId, artifact, checksum, sha256, files }, null, 2));
|
||||
} finally {
|
||||
await rm(stage, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function assertFilterToggleBoundary() {
|
||||
const helper = await readFile(resolve(foundryRoot, files[1]), "utf8");
|
||||
for (const marker of [
|
||||
"toggleMapPresentationFacetSelection",
|
||||
"const { [field]: _removed, ...unconstrained } = facets",
|
||||
"return unconstrained",
|
||||
]) {
|
||||
if (!helper.includes(marker)) throw new Error(`foundry_filter_toggle_helper_missing:${marker}`);
|
||||
}
|
||||
|
||||
const preview = await readFile(resolve(foundryRoot, files[0]), "utf8");
|
||||
if (!preview.includes("filters: toggleMapPresentationFacetSelection(filters, field, value)")) {
|
||||
throw new Error("foundry_filter_toggle_component_contract_missing");
|
||||
}
|
||||
|
||||
const test = await readFile(resolve(foundryRoot, files[2]), "utf8");
|
||||
for (const marker of [
|
||||
"interactive deselect of the last chip removes the facet constraint",
|
||||
"interactive deselect preserves other values and facet constraints",
|
||||
"deselecting the last chip remains empty and never normalizes to all",
|
||||
]) {
|
||||
if (!test.includes(marker)) throw new Error(`foundry_filter_toggle_regression_missing:${marker}`);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from "node:crypto";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { cp, lstat, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const platformRoot = resolve(scriptDir, "../..");
|
||||
const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts"));
|
||||
const [patchId = "ontology-core-20260719-001", ...extra] = process.argv.slice(2);
|
||||
|
||||
if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) {
|
||||
throw new Error("usage: build-ontology-core-artifact.mjs [patch-id]");
|
||||
}
|
||||
|
||||
const sourceRoot = resolve(platformRoot, "services/ontology-core");
|
||||
const destinationRoot = "platform/ontology-core";
|
||||
const ignoredBasenames = new Set([".DS_Store", ".git", "node_modules", "test"]);
|
||||
const stage = await mkdtemp(join(tmpdir(), "nodedc-ontology-core-artifact-"));
|
||||
const payload = join(stage, "payload");
|
||||
const target = join(artifactDir, `nodedc-platform-${patchId}.tgz`);
|
||||
|
||||
try {
|
||||
await copySafe(sourceRoot, join(payload, destinationRoot));
|
||||
await writeFile(join(stage, "manifest.env"), `id=${patchId}\ncomponent=platform\ntype=app-overlay\n`, "utf8");
|
||||
await writeFile(join(stage, "files.txt"), `${destinationRoot}\n`, "utf8");
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
|
||||
const tar = spawnSync("python3", ["-c", canonicalTarScript(), target, stage], {
|
||||
encoding: "utf8",
|
||||
maxBuffer: 128 * 1024 * 1024,
|
||||
});
|
||||
if (tar.status !== 0) throw new Error(`tar_failed:${tar.stderr || tar.stdout}`);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
patchId,
|
||||
artifact: target,
|
||||
sha256: createHash("sha256").update(await readFile(target)).digest("hex"),
|
||||
entries: [destinationRoot],
|
||||
servicesExpected: ["ontology-core", "ai-workspace-hub"],
|
||||
excluded: [".env*", "node_modules", "test", "secrets"],
|
||||
}, null, 2));
|
||||
} finally {
|
||||
await rm(stage, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
async function copySafe(source, destination) {
|
||||
const sourceStat = await lstat(source);
|
||||
if (sourceStat.isSymbolicLink()) throw new Error(`source_symlink_rejected:${relative(platformRoot, source)}`);
|
||||
if (sourceStat.isFile()) {
|
||||
await mkdir(dirname(destination), { recursive: true });
|
||||
await cp(source, destination, { force: true, verbatimSymlinks: true });
|
||||
return;
|
||||
}
|
||||
if (!sourceStat.isDirectory()) throw new Error(`source_type_rejected:${relative(platformRoot, source)}`);
|
||||
|
||||
await mkdir(destination, { recursive: true });
|
||||
for (const entry of await readdir(source, { withFileTypes: true })) {
|
||||
if (ignoredBasenames.has(entry.name) || entry.name.startsWith(".env")) continue;
|
||||
const childSource = join(source, entry.name);
|
||||
const childDestination = join(destination, entry.name);
|
||||
if (entry.isSymbolicLink()) throw new Error(`source_symlink_rejected:${relative(platformRoot, childSource)}`);
|
||||
await copySafe(childSource, childDestination);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/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 artifactRoot = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"));
|
||||
const [transitionId = "20260719-005", ...extra] = process.argv.slice(2);
|
||||
if (extra.length || !/^\d{8}-[0-9]{3}$/.test(transitionId)) {
|
||||
throw new Error("usage: build-platform-gelios-provider-v3-artifact.mjs [YYYYMMDD-NNN]");
|
||||
}
|
||||
|
||||
const id = `platform-gelios-provider-v3-${transitionId}`;
|
||||
const target = join(artifactRoot, `nodedc-${id}.tgz`);
|
||||
const files = [
|
||||
"platform/packages/external-provider-contract/providers/gelios/v3/README.md",
|
||||
"platform/packages/external-provider-contract/providers/gelios/v3/index.mjs",
|
||||
"platform/packages/external-provider-contract/providers/gelios/v3/package.mjs",
|
||||
];
|
||||
const expectedSha256 = new Map([
|
||||
[files[0], "7ac2318e455786895bebf2e7ee75ae9f86e8958fc5d4699fd0c8c603b8a92ca1"],
|
||||
[files[1], "dbb82752248fd45c4d3670dadb435123d0575881bfa9458fc0757a421471de24"],
|
||||
[files[2], "67170084c2d55c3adbed05f1d63c71403689fb59ebc9811e495a55a3c4fa41bf"],
|
||||
]);
|
||||
|
||||
await assertFresh(target);
|
||||
const stage = await mkdtemp(join(tmpdir(), "nodedc-platform-gelios-provider-v3-"));
|
||||
const payload = join(stage, "payload");
|
||||
try {
|
||||
await mkdir(payload, { recursive: true });
|
||||
for (const rel of files) {
|
||||
const source = join(platformRoot, rel.replace(/^platform\//, ""));
|
||||
const stat = await lstat(source);
|
||||
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`source_boundary_invalid:${rel}`);
|
||||
const bytes = await readFile(source);
|
||||
if (sha(bytes) !== expectedSha256.get(rel)) throw new Error(`source_sha256_mismatch:${rel}`);
|
||||
await mkdir(dirname(join(payload, rel)), { recursive: true });
|
||||
await cp(source, join(payload, rel), { force: false });
|
||||
}
|
||||
await writeFile(join(stage, "manifest.env"), `id=${id}\ncomponent=platform\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]);
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
id,
|
||||
artifact: target,
|
||||
artifactSha256: sha(await readFile(target)),
|
||||
services: [],
|
||||
providerPackage: "gelios.provider.v3",
|
||||
dataProductId: "fleet.positions.current.v2",
|
||||
runtimeEffect: "catalog-only; no service restart",
|
||||
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 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 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;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/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 artifactRoot = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"));
|
||||
const [transitionId = "20260720-001", ...extra] = process.argv.slice(2);
|
||||
if (extra.length || !/^\d{8}-[0-9]{3}$/.test(transitionId)) {
|
||||
throw new Error("usage: build-platform-gelios-provider-v4-artifact.mjs [YYYYMMDD-NNN]");
|
||||
}
|
||||
|
||||
const id = `platform-gelios-provider-v4-${transitionId}`;
|
||||
const target = join(artifactRoot, `nodedc-${id}.tgz`);
|
||||
const files = [
|
||||
"platform/packages/external-provider-contract/src/provider-package.mjs",
|
||||
"platform/packages/external-provider-contract/providers/gelios/v4/README.md",
|
||||
"platform/packages/external-provider-contract/providers/gelios/v4/index.mjs",
|
||||
"platform/packages/external-provider-contract/providers/gelios/v4/package.mjs",
|
||||
"platform/services/external-data-plane/definitions/fleet.positions.current.v3.json",
|
||||
];
|
||||
const expectedSha256 = new Map([
|
||||
[files[0], "8ae73cae0be8cbf8484125e1ccf836fdc245b31872a5c7888ced289ed1895ba2"],
|
||||
[files[1], "9f38f5cb267269b54053fa590a3970abb4b6b9803fc931e997be4293aa6e592a"],
|
||||
[files[2], "b92fb1fed6ff2fe29c3dc77978e7c35dcdf4c7f2af4ae63ada23e9c3119e7fd9"],
|
||||
[files[3], "55a46f825e12bcef2354fefd956e674b72fea68b6997df3fcbe89b0a074ec9b8"],
|
||||
[files[4], "04d458f050313468990849f60d37a8a6c9aadd355137d4084b66556bb4a4b673"],
|
||||
]);
|
||||
|
||||
await assertFresh(target);
|
||||
const stage = await mkdtemp(join(tmpdir(), "nodedc-platform-gelios-provider-v4-"));
|
||||
const payload = join(stage, "payload");
|
||||
try {
|
||||
await mkdir(payload, { recursive: true });
|
||||
for (const rel of files) {
|
||||
const source = join(platformRoot, rel.replace(/^platform\//, ""));
|
||||
const stat = await lstat(source);
|
||||
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`source_boundary_invalid:${rel}`);
|
||||
const bytes = await readFile(source);
|
||||
if (sha(bytes) !== expectedSha256.get(rel)) throw new Error(`source_sha256_mismatch:${rel}`);
|
||||
await mkdir(dirname(join(payload, rel)), { recursive: true });
|
||||
await cp(source, join(payload, rel), { force: false });
|
||||
}
|
||||
await writeFile(join(stage, "manifest.env"), `id=${id}\ncomponent=platform\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]);
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
id,
|
||||
artifact: target,
|
||||
artifactSha256: sha(await readFile(target)),
|
||||
services: ["external-data-plane"],
|
||||
ontologyPackage: "gelios@1.1.0",
|
||||
providerPackage: "gelios.provider.v4",
|
||||
dataProductId: "fleet.positions.current.v3",
|
||||
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 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 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}`);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,157 @@
|
||||
import hashlib
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
|
||||
BUILDER_PATH = SCRIPT_DIR / "build-engine-composite-provider-v4-artifact.mjs"
|
||||
ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA"
|
||||
PATCH_ID = "engine-composite-provider-v4-20991231-999"
|
||||
|
||||
|
||||
def load_runner():
|
||||
loader = importlib.machinery.SourceFileLoader(
|
||||
"nodedc_engine_composite_provider_v4",
|
||||
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 EngineCompositeProviderV4Test(unittest.TestCase):
|
||||
def test_predecessor_map_matches_the_applied_release_order(self):
|
||||
self.assertEqual(
|
||||
RUNNER.ENGINE_COMPOSITE_PROVIDER_V4_PREDECESSOR_SHA256,
|
||||
{
|
||||
"nodedc-source/server/assets/provider-packages/v1/catalog.json":
|
||||
"992159fc457ec76ce1f45aad337604c8a72b29252d44fbccda515f0fd6ea6428",
|
||||
"nodedc-source/server/dataProductPublishGrant/providerCatalog.js":
|
||||
"901b8fad80018ce177b34ced804b39cb140a47e831414057f484296b373c651d",
|
||||
"nodedc-source/server/dataProductPublishGrant/service.js":
|
||||
"6a417b25b080c05b40c771df4e2dca163491af2c9083ff73dc7e54ad3b360241",
|
||||
"nodedc-source/server/dataProductPublishGrant/store.js":
|
||||
"a92303b2732e21f68c1ac732fa26e4983cbadf3515741cee983b916a283d754c",
|
||||
},
|
||||
)
|
||||
|
||||
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_is_deterministic_and_emits_exact_backend_only_slice(self):
|
||||
current_sha256 = {
|
||||
relative_path: hashlib.sha256((ENGINE_ROOT / relative_path).read_bytes()).hexdigest()
|
||||
for relative_path in RUNNER.ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES
|
||||
}
|
||||
if current_sha256 != RUNNER.ENGINE_COMPOSITE_PROVIDER_V4_TARGET_SHA256:
|
||||
self.skipTest("historical v4 builder source has advanced to its exact successor")
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-engine-composite-v4-") 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["credentialValues"], "preserved")
|
||||
self.assertEqual(
|
||||
tuple(first["entries"]),
|
||||
RUNNER.ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES,
|
||||
)
|
||||
|
||||
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()
|
||||
)
|
||||
payload = extract / "payload"
|
||||
self.assertTrue(RUNNER.is_engine_composite_provider_v4_slice("engine", entries))
|
||||
self.assertEqual(RUNNER.component_services("engine", entries), ("nodedc-backend",))
|
||||
self.assertEqual(
|
||||
RUNNER.component_healthchecks("engine", entries, ("nodedc-backend",)),
|
||||
("http://127.0.0.1:3001/health",),
|
||||
)
|
||||
RUNNER.validate_engine_composite_provider_v4_slice(payload, entries)
|
||||
for relative_path, expected in RUNNER.ENGINE_COMPOSITE_PROVIDER_V4_TARGET_SHA256.items():
|
||||
self.assertEqual(
|
||||
hashlib.sha256((payload / relative_path).read_bytes()).hexdigest(),
|
||||
expected,
|
||||
)
|
||||
|
||||
def test_preflight_requires_every_exact_predecessor_and_immutable_backend(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-engine-composite-v4-preflight-") as directory:
|
||||
root = Path(directory)
|
||||
for relative_path in RUNNER.ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES:
|
||||
path = root / relative_path
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("predecessor\n", encoding="utf-8")
|
||||
|
||||
def exact_sha(path):
|
||||
relative_path = Path(path).relative_to(root).as_posix()
|
||||
return RUNNER.ENGINE_COMPOSITE_PROVIDER_V4_PREDECESSOR_SHA256[relative_path]
|
||||
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||
mock.patch.object(RUNNER, "sha256_file", side_effect=exact_sha),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"preflight_engine_credential_backend_runtime",
|
||||
return_value={"mode": "verified-derived-retry"},
|
||||
),
|
||||
):
|
||||
result = RUNNER.preflight_engine_composite_provider_v4_predecessor()
|
||||
self.assertEqual(result["mode"], "exact-composite-provider-v3-to-v4")
|
||||
self.assertEqual(
|
||||
result["predecessor_sha256"],
|
||||
RUNNER.ENGINE_COMPOSITE_PROVIDER_V4_PREDECESSOR_SHA256,
|
||||
)
|
||||
self.assertEqual(
|
||||
result["target_sha256"],
|
||||
RUNNER.ENGINE_COMPOSITE_PROVIDER_V4_TARGET_SHA256,
|
||||
)
|
||||
|
||||
drift_path = root / RUNNER.ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES[1]
|
||||
|
||||
def drift_sha(path):
|
||||
if Path(path) == drift_path:
|
||||
return "0" * 64
|
||||
return exact_sha(path)
|
||||
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||
mock.patch.object(RUNNER, "sha256_file", side_effect=drift_sha),
|
||||
):
|
||||
with self.assertRaises(RUNNER.DeployError):
|
||||
RUNNER.preflight_engine_composite_provider_v4_predecessor()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/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
|
||||
ENGINE_ROOT = PLATFORM_ROOT.parent / "NODEDC_ENGINE_INFRA"
|
||||
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
|
||||
BUILDER = SCRIPT_DIR / "build-engine-mcp-autonomy-provider-v5-artifact.mjs"
|
||||
PATCH_ID = "20991231-998"
|
||||
CANONICAL_ARTIFACT = (
|
||||
PLATFORM_ROOT
|
||||
/ "infra/deploy-artifacts/nodedc-engine-mcp-autonomy-provider-v5-20260720-004.tgz"
|
||||
)
|
||||
CANONICAL_SHA256 = "3400954cdce078892a06b04b9bfd85a90f6ea775b1a0caf99eba1f880ef6601c"
|
||||
|
||||
|
||||
def load_runner():
|
||||
loader = importlib.machinery.SourceFileLoader(
|
||||
"nodedc_engine_mcp_autonomy_provider_v5",
|
||||
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 EngineMcpAutonomyProviderV5Test(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), PATCH_ID],
|
||||
cwd=PLATFORM_ROOT,
|
||||
env=environment,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
def test_canonical_artifact_digest_and_members_are_pinned(self):
|
||||
self.assertEqual(
|
||||
hashlib.sha256(CANONICAL_ARTIFACT.read_bytes()).hexdigest(),
|
||||
CANONICAL_SHA256,
|
||||
)
|
||||
self.assertEqual(CANONICAL_ARTIFACT.read_bytes()[4:8], b"\0\0\0\0")
|
||||
with tarfile.open(CANONICAL_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_builder_is_reproducible_and_slice_is_backend_only(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-engine-mcp-autonomy-v5-") 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["artifactSha256"],
|
||||
hashlib.sha256(first_artifact.read_bytes()).hexdigest(),
|
||||
)
|
||||
self.assertEqual(first["services"], ["nodedc-backend"])
|
||||
self.assertEqual(
|
||||
first["authority"],
|
||||
"mcp-capability-intersect-user-objective",
|
||||
)
|
||||
|
||||
extract = root / "extract"
|
||||
with tarfile.open(first_artifact, "r:gz") as archive:
|
||||
archive.extractall(extract, filter="data")
|
||||
loaded = root / "loaded"
|
||||
loaded.mkdir()
|
||||
manifest, entries, payload = RUNNER.load_artifact(first_artifact, loaded)
|
||||
descriptor = RUNNER.validate_engine_mcp_autonomy_provider_v5_payload(
|
||||
payload,
|
||||
entries,
|
||||
)
|
||||
self.assertEqual(manifest["component"], "engine")
|
||||
self.assertEqual(
|
||||
tuple(entries),
|
||||
RUNNER.ENGINE_MCP_AUTONOMY_PROVIDER_V5_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_AUTONOMY_PROVIDER_V5_TARGET_SHA256[
|
||||
RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
||||
],
|
||||
)
|
||||
|
||||
def test_payload_pins_authority_policy_archive_and_catalog_lineage(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-engine-mcp-autonomy-v5-policy-") as directory:
|
||||
result = self.build(Path(directory) / "artifact")
|
||||
extract = Path(directory) / "extract"
|
||||
with tarfile.open(result["artifact"], "r:gz") as archive:
|
||||
archive.extractall(extract, filter="data")
|
||||
payload = extract / "payload"
|
||||
installer = (
|
||||
payload
|
||||
/ "nodedc-source/server/assets/engine-agent-npm/bin/nodedc-engine-codex-agent.mjs"
|
||||
).read_text(encoding="utf-8")
|
||||
catalog = json.loads((
|
||||
payload / "nodedc-source/server/assets/provider-packages/v1/catalog.json"
|
||||
).read_text(encoding="utf-8"))
|
||||
self.assertIn("MCP tool availability establishes capability authority", installer)
|
||||
self.assertIn("machine safety barrier, not a permission ceremony", installer)
|
||||
self.assertIn("Three identical failures with no new evidence", installer)
|
||||
self.assertNotIn("only with explicit user confirmation", installer)
|
||||
self.assertEqual(
|
||||
[item["id"] for item in catalog["packages"]],
|
||||
["gelios.provider.v4", "gelios.provider.v5"],
|
||||
)
|
||||
self.assertTrue(all(
|
||||
capability["dataProductIds"] == ["fleet.positions.current.v4"]
|
||||
for package in catalog["packages"] if package["id"] == "gelios.provider.v5"
|
||||
for capability in package["capabilities"]
|
||||
))
|
||||
|
||||
def test_payload_tampering_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-engine-mcp-autonomy-v5-tamper-") as directory:
|
||||
result = self.build(Path(directory) / "artifact")
|
||||
extract = Path(directory) / "extract"
|
||||
RUNNER.safe_extract(Path(result["artifact"]), extract)
|
||||
entries = RUNNER.parse_files_list(extract / "files.txt")
|
||||
package = extract / "payload/nodedc-source/server/assets/engine-agent-npm/package.json"
|
||||
package.write_text("{}\n", encoding="utf-8")
|
||||
with self.assertRaisesRegex(RUNNER.DeployError, "target sha256 mismatch"):
|
||||
RUNNER.validate_engine_mcp_autonomy_provider_v5_payload(
|
||||
extract / "payload",
|
||||
entries,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -13,6 +13,7 @@ from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
PLATFORM_ROOT = SCRIPT_DIR.parent.parent
|
||||
ENGINE_ROOT = PLATFORM_ROOT.parent / "NODEDC_ENGINE_INFRA"
|
||||
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"
|
||||
@@ -53,6 +54,12 @@ class EngineMcpOntologySdkTest(unittest.TestCase):
|
||||
self.assertFalse(Path(member.name).name.startswith("._"))
|
||||
|
||||
def test_engine_builder_is_byte_reproducible(self):
|
||||
current_sha256 = {
|
||||
relative_path: hashlib.sha256((ENGINE_ROOT / relative_path).read_bytes()).hexdigest()
|
||||
for relative_path in RUNNER.ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256
|
||||
}
|
||||
if current_sha256 != RUNNER.ENGINE_MCP_ONTOLOGY_SDK_TARGET_SHA256:
|
||||
self.skipTest("historical Ontology/SDK builder source has advanced to its exact successor")
|
||||
outputs = []
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-engine-mcp-ontology-sdk-") as directory:
|
||||
for index in range(2):
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import hashlib
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
|
||||
BUILDER_PATH = SCRIPT_DIR / "build-engine-provider-authority-diagnostics-artifact.mjs"
|
||||
ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA"
|
||||
PATCH_ID = "engine-provider-authority-diagnostics-20991231-999"
|
||||
|
||||
|
||||
def load_runner():
|
||||
loader = importlib.machinery.SourceFileLoader(
|
||||
"nodedc_engine_provider_authority_diagnostics",
|
||||
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 EngineProviderAuthorityDiagnosticsTest(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_one_file_slice(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-provider-authority-") 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(
|
||||
tuple(first["entries"]),
|
||||
RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES,
|
||||
)
|
||||
|
||||
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())
|
||||
payload = extract / "payload"
|
||||
self.assertTrue(RUNNER.is_engine_provider_authority_diagnostics_slice("engine", entries))
|
||||
self.assertEqual(RUNNER.component_services("engine", entries), ("nodedc-backend",))
|
||||
self.assertEqual(
|
||||
RUNNER.component_healthchecks("engine", entries, ("nodedc-backend",)),
|
||||
("http://127.0.0.1:3001/health",),
|
||||
)
|
||||
RUNNER.validate_engine_provider_authority_diagnostics_slice(payload, entries)
|
||||
|
||||
def test_preflight_requires_the_exact_deployed_predecessor(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-provider-authority-preflight-") as directory:
|
||||
root = Path(directory)
|
||||
relative_path = RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES[0]
|
||||
path = root / relative_path
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("predecessor\n", encoding="utf-8")
|
||||
expected = RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_PREDECESSOR_SHA256[relative_path]
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||
mock.patch.object(RUNNER, "sha256_file", return_value=expected),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"preflight_engine_credential_backend_runtime",
|
||||
return_value={"mode": "verified-derived-retry"},
|
||||
),
|
||||
):
|
||||
result = RUNNER.preflight_engine_provider_authority_diagnostics_predecessor()
|
||||
self.assertEqual(result["mode"], "exact-provider-authority-reason-codes")
|
||||
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||
mock.patch.object(RUNNER, "sha256_file", return_value="0" * 64),
|
||||
):
|
||||
with self.assertRaises(RUNNER.DeployError):
|
||||
RUNNER.preflight_engine_provider_authority_diagnostics_predecessor()
|
||||
|
||||
def test_healthchecks_dispatch_authority_diagnostics_acceptance(self):
|
||||
entries = RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_ARTIFACT_ENTRIES
|
||||
root = Path("/engine")
|
||||
target_hash = next(iter(RUNNER.ENGINE_PROVIDER_AUTHORITY_DIAGNOSTICS_TARGET_SHA256.values()))
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||
mock.patch.object(RUNNER, "sha256_file", return_value=target_hash),
|
||||
mock.patch.object(RUNNER, "healthcheck_compose_service"),
|
||||
mock.patch.object(RUNNER, "component_healthchecks", return_value=()),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"preflight_engine_credential_backend_runtime",
|
||||
return_value={"mode": "verified-derived-retry"},
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"accept_engine_provider_authority_diagnostics_runtime",
|
||||
return_value={"live": "accepted"},
|
||||
) as acceptance,
|
||||
mock.patch.object(RUNNER, "healthcheck_container"),
|
||||
):
|
||||
RUNNER.run_healthchecks("engine", entries, ("nodedc-backend",))
|
||||
acceptance.assert_called_once_with()
|
||||
|
||||
def test_live_acceptance_uses_exact_reason_code_contract(self):
|
||||
expected_live = "engine-provider-authority-diagnostics:exact-reason-codes:v1"
|
||||
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_provider_authority_diagnostics_runtime()
|
||||
self.assertEqual(result["live"], expected_live)
|
||||
self.assertEqual(backend_probe.call_args.args[1], "Engine provider authority diagnostics")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,193 @@
|
||||
import hashlib
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
|
||||
BUILDER_PATH = SCRIPT_DIR / "build-engine-provider-rotating-slot-artifact.mjs"
|
||||
ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA"
|
||||
PATCH_ID = "engine-provider-rotating-slot-20991231-999"
|
||||
AUTHORITY_SUCCESSOR_SHA256 = "1a14299167ebe17efd677fa3c59b84c80e12d6846bac6f40f9bcae0729ab22c6"
|
||||
|
||||
|
||||
def load_runner():
|
||||
loader = importlib.machinery.SourceFileLoader(
|
||||
"nodedc_engine_provider_rotating_slot",
|
||||
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 EngineProviderRotatingSlotTest(unittest.TestCase):
|
||||
def source_has_exact_successor(self):
|
||||
path = ENGINE_ROOT / "nodedc-source/server/dataProductPublishGrant/providerCatalog.js"
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest() == AUTHORITY_SUCCESSOR_SHA256
|
||||
|
||||
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_two_file_slice(self):
|
||||
if self.source_has_exact_successor():
|
||||
self.skipTest("historical rotating-slot builder source has advanced to its exact successor")
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-provider-rotating-slot-") 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["credentialSlot"], "ndcProviderRotatingAccessApi")
|
||||
self.assertEqual(
|
||||
tuple(first["entries"]),
|
||||
RUNNER.ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES,
|
||||
)
|
||||
|
||||
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()
|
||||
)
|
||||
payload = extract / "payload"
|
||||
self.assertTrue(RUNNER.is_engine_provider_rotating_slot_slice("engine", entries))
|
||||
self.assertEqual(RUNNER.component_services("engine", entries), ("nodedc-backend",))
|
||||
self.assertEqual(
|
||||
RUNNER.component_healthchecks("engine", entries, ("nodedc-backend",)),
|
||||
("http://127.0.0.1:3001/health",),
|
||||
)
|
||||
RUNNER.validate_engine_provider_rotating_slot_slice(payload, entries)
|
||||
|
||||
def test_preflight_is_exact_and_rejects_any_drift(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-provider-rotating-preflight-") as directory:
|
||||
root = Path(directory)
|
||||
for relative_path in RUNNER.ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES:
|
||||
path = root / relative_path
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("predecessor\n", encoding="utf-8")
|
||||
|
||||
def exact_sha(path):
|
||||
relative_path = Path(path).relative_to(root).as_posix()
|
||||
return RUNNER.ENGINE_PROVIDER_ROTATING_SLOT_PREDECESSOR_SHA256[relative_path]
|
||||
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||
mock.patch.object(RUNNER, "sha256_file", side_effect=exact_sha),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"preflight_engine_credential_backend_runtime",
|
||||
return_value={"mode": "verified-derived-retry"},
|
||||
),
|
||||
):
|
||||
result = RUNNER.preflight_engine_provider_rotating_slot_predecessor()
|
||||
self.assertEqual(result["mode"], "exact-gelios-v4-credential-slot-alignment")
|
||||
self.assertEqual(
|
||||
result["predecessor_sha256"],
|
||||
RUNNER.ENGINE_PROVIDER_ROTATING_SLOT_PREDECESSOR_SHA256,
|
||||
)
|
||||
|
||||
drift_path = root / RUNNER.ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES[0]
|
||||
|
||||
def drift_sha(path):
|
||||
if Path(path) == drift_path:
|
||||
return "0" * 64
|
||||
return exact_sha(path)
|
||||
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||
mock.patch.object(RUNNER, "sha256_file", side_effect=drift_sha),
|
||||
):
|
||||
with self.assertRaises(RUNNER.DeployError):
|
||||
RUNNER.preflight_engine_provider_rotating_slot_predecessor()
|
||||
|
||||
def test_healthchecks_dispatch_exact_rotating_slot_acceptance(self):
|
||||
entries = RUNNER.ENGINE_PROVIDER_ROTATING_SLOT_ARTIFACT_ENTRIES
|
||||
root = Path("/engine")
|
||||
|
||||
def installed_sha(path):
|
||||
relative_path = Path(path).relative_to(root).as_posix()
|
||||
return RUNNER.ENGINE_PROVIDER_ROTATING_SLOT_TARGET_SHA256[relative_path]
|
||||
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||
mock.patch.object(RUNNER, "sha256_file", side_effect=installed_sha),
|
||||
mock.patch.object(RUNNER, "healthcheck_compose_service"),
|
||||
mock.patch.object(RUNNER, "component_healthchecks", return_value=()),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"preflight_engine_credential_backend_runtime",
|
||||
return_value={"mode": "verified-derived-retry"},
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"accept_engine_provider_rotating_slot_runtime",
|
||||
return_value={"live": "accepted"},
|
||||
) as rotating_acceptance,
|
||||
mock.patch.object(RUNNER, "accept_engine_composite_provider_runtime") as composite_acceptance,
|
||||
mock.patch.object(RUNNER, "healthcheck_container"),
|
||||
):
|
||||
RUNNER.run_healthchecks("engine", entries, ("nodedc-backend",))
|
||||
|
||||
rotating_acceptance.assert_called_once_with()
|
||||
composite_acceptance.assert_not_called()
|
||||
|
||||
def test_live_acceptance_uses_the_rotating_slot_target_contract(self):
|
||||
if self.source_has_exact_successor():
|
||||
self.skipTest("historical rotating-slot live fixture source has advanced to its exact successor")
|
||||
expected_live = (
|
||||
"engine-provider-rotating-slot:gelios.provider.v4:"
|
||||
"ndcProviderRotatingAccessApi:fleet.positions.current.v3:monitoring,units"
|
||||
)
|
||||
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_provider_rotating_slot_runtime()
|
||||
|
||||
self.assertEqual(result["live"], expected_live)
|
||||
self.assertEqual(
|
||||
result["target_sha256"],
|
||||
RUNNER.ENGINE_PROVIDER_ROTATING_SLOT_TARGET_SHA256,
|
||||
)
|
||||
self.assertEqual(
|
||||
backend_probe.call_args.args[1],
|
||||
"Engine provider rotating slot authority",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,90 @@
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
|
||||
BUILDER_PATH = SCRIPT_DIR / "build-engine-provider-security-catalog-artifact.mjs"
|
||||
ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA"
|
||||
CATALOG_REL = "nodedc-source/server/assets/provider-packages/v1/catalog.json"
|
||||
|
||||
|
||||
def load_runner():
|
||||
loader = importlib.machinery.SourceFileLoader("nodedc_engine_provider_catalog", 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 EngineProviderSecurityCatalogTest(unittest.TestCase):
|
||||
def test_builder_emits_exact_backend_only_slice(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-engine-provider-catalog-") as directory:
|
||||
artifact_dir = Path(directory) / "artifacts"
|
||||
env = os.environ.copy()
|
||||
env["NODEDC_ENGINE_SOURCE_ROOT"] = str(ENGINE_ROOT)
|
||||
env["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
|
||||
completed = subprocess.run(
|
||||
["node", str(BUILDER_PATH), "20991231-999"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
result = json.loads(completed.stdout)
|
||||
artifact = Path(result["artifact"])
|
||||
self.assertEqual(result["services"], ["nodedc-backend"])
|
||||
with tarfile.open(artifact, "r:gz") as archive:
|
||||
archive.extractall(Path(directory) / "extract", filter="data")
|
||||
root = Path(directory) / "extract"
|
||||
entries = tuple((root / "files.txt").read_text(encoding="utf-8").splitlines())
|
||||
payload = root / "payload"
|
||||
self.assertTrue(RUNNER.is_engine_provider_security_catalog_slice("engine", entries))
|
||||
self.assertEqual(RUNNER.component_services("engine", entries), ("nodedc-backend",))
|
||||
self.assertEqual(
|
||||
RUNNER.component_healthchecks("engine", entries, ("nodedc-backend",)),
|
||||
("http://127.0.0.1:3001/health",),
|
||||
)
|
||||
catalog = RUNNER.validate_engine_provider_security_catalog_payload(payload, entries)
|
||||
self.assertEqual(catalog["packages"][0]["id"], "gelios.provider.v3")
|
||||
|
||||
def test_preflight_requires_exact_v1_predecessor_and_immutable_backend(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-engine-provider-predecessor-") as directory:
|
||||
root = Path(directory)
|
||||
target = root / CATALOG_REL
|
||||
target.parent.mkdir(parents=True)
|
||||
source = subprocess.run(
|
||||
["git", "show", "HEAD:nodedc-source/server/assets/provider-packages/v1/catalog.json"],
|
||||
cwd=ENGINE_ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
).stdout
|
||||
target.write_bytes(source)
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"preflight_engine_credential_backend_runtime",
|
||||
return_value={"mode": "verified-derived-retry"},
|
||||
),
|
||||
):
|
||||
result = RUNNER.preflight_engine_provider_security_catalog_predecessor()
|
||||
self.assertEqual(
|
||||
result["catalog_sha256"],
|
||||
RUNNER.ENGINE_PROVIDER_SECURITY_CATALOG_PREDECESSOR_SHA256,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,145 @@
|
||||
import hashlib
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
|
||||
BUILDER_PATH = SCRIPT_DIR / "build-engine-provider-target-host-policy-artifact.mjs"
|
||||
ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA"
|
||||
PATCH_ID = "engine-provider-target-host-policy-20991231-999"
|
||||
|
||||
|
||||
def load_runner():
|
||||
loader = importlib.machinery.SourceFileLoader(
|
||||
"nodedc_engine_provider_target_host_policy",
|
||||
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 EngineProviderTargetHostPolicyTest(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_one_file_slice(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-provider-target-host-") 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(
|
||||
tuple(first["entries"]),
|
||||
RUNNER.ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES,
|
||||
)
|
||||
|
||||
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())
|
||||
payload = extract / "payload"
|
||||
self.assertTrue(RUNNER.is_engine_provider_target_host_policy_slice("engine", entries))
|
||||
self.assertEqual(RUNNER.component_services("engine", entries), ("nodedc-backend",))
|
||||
self.assertEqual(
|
||||
RUNNER.component_healthchecks("engine", entries, ("nodedc-backend",)),
|
||||
("http://127.0.0.1:3001/health",),
|
||||
)
|
||||
RUNNER.validate_engine_provider_target_host_policy_slice(payload, entries)
|
||||
|
||||
def test_preflight_requires_the_exact_deployed_predecessor(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-provider-target-host-preflight-") as directory:
|
||||
root = Path(directory)
|
||||
relative_path = RUNNER.ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES[0]
|
||||
path = root / relative_path
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("predecessor\n", encoding="utf-8")
|
||||
expected = RUNNER.ENGINE_PROVIDER_TARGET_HOST_POLICY_PREDECESSOR_SHA256[relative_path]
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||
mock.patch.object(RUNNER, "sha256_file", return_value=expected),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"preflight_engine_credential_backend_runtime",
|
||||
return_value={"mode": "verified-derived-retry"},
|
||||
),
|
||||
):
|
||||
result = RUNNER.preflight_engine_provider_target_host_policy_predecessor()
|
||||
self.assertEqual(result["mode"], "exact-provider-literal-target-host")
|
||||
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||
mock.patch.object(RUNNER, "sha256_file", return_value="0" * 64),
|
||||
):
|
||||
with self.assertRaises(RUNNER.DeployError):
|
||||
RUNNER.preflight_engine_provider_target_host_policy_predecessor()
|
||||
|
||||
def test_healthchecks_dispatch_target_host_policy_acceptance(self):
|
||||
entries = RUNNER.ENGINE_PROVIDER_TARGET_HOST_POLICY_ARTIFACT_ENTRIES
|
||||
root = Path("/engine")
|
||||
target_hash = next(iter(RUNNER.ENGINE_PROVIDER_TARGET_HOST_POLICY_TARGET_SHA256.values()))
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||
mock.patch.object(RUNNER, "sha256_file", return_value=target_hash),
|
||||
mock.patch.object(RUNNER, "healthcheck_compose_service"),
|
||||
mock.patch.object(RUNNER, "component_healthchecks", return_value=()),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"preflight_engine_credential_backend_runtime",
|
||||
return_value={"mode": "verified-derived-retry"},
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"accept_engine_provider_target_host_policy_runtime",
|
||||
return_value={"live": "accepted"},
|
||||
) as acceptance,
|
||||
mock.patch.object(RUNNER, "healthcheck_container"),
|
||||
):
|
||||
RUNNER.run_healthchecks("engine", entries, ("nodedc-backend",))
|
||||
acceptance.assert_called_once_with()
|
||||
|
||||
def test_live_acceptance_uses_exact_literal_host_contract(self):
|
||||
expected_live = "engine-provider-target-host-policy:exact-literal-host:v1"
|
||||
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_provider_target_host_policy_runtime()
|
||||
self.assertEqual(result["live"], expected_live)
|
||||
self.assertEqual(backend_probe.call_args.args[1], "Engine provider target host policy")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -11,6 +11,10 @@ from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
BUILDER = SCRIPT_DIR / "build-engine-data-product-publish-grant-artifact.mjs"
|
||||
ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA"
|
||||
HISTORICAL_PROVIDER_CATALOG_SHA256 = (
|
||||
"9c931f9abfcadb5b34a8a854c2efb8fd79913e000eecf0967d1b7c500bf9a56a"
|
||||
)
|
||||
EXPECTED_ENTRIES = [
|
||||
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
||||
"nodedc-source/server/dataProductPublishGrant",
|
||||
@@ -26,6 +30,13 @@ SUCCESSFUL_CREDENTIAL_SINK_INDEX_SHA256 = (
|
||||
|
||||
|
||||
class EnginePublishGrantArtifactTest(unittest.TestCase):
|
||||
def historical_source_is_current(self):
|
||||
catalog = (
|
||||
ENGINE_ROOT
|
||||
/ "nodedc-source/server/assets/provider-packages/v1/catalog.json"
|
||||
)
|
||||
return hashlib.sha256(catalog.read_bytes()).hexdigest() == HISTORICAL_PROVIDER_CATALOG_SHA256
|
||||
|
||||
def build(self, artifact_dir, patch_id):
|
||||
environment = os.environ.copy()
|
||||
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
|
||||
@@ -39,6 +50,8 @@ class EnginePublishGrantArtifactTest(unittest.TestCase):
|
||||
return json.loads(result.stdout)
|
||||
|
||||
def test_artifact_is_narrow_secret_free_and_deterministic(self):
|
||||
if not self.historical_source_is_current():
|
||||
self.skipTest("historical Publish-grant builder source has advanced to its exact successor")
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-engine-publish-artifact-") as directory:
|
||||
root = Path(directory)
|
||||
first_dir = root / "first"
|
||||
@@ -70,6 +83,18 @@ class EnginePublishGrantArtifactTest(unittest.TestCase):
|
||||
"payload/nodedc-source/services/backend/data-product-publish-grant/"
|
||||
"docker-compose.immutable-runtime.yml"
|
||||
).read().decode("utf-8")
|
||||
catalog = json.loads(archive.extractfile(
|
||||
"payload/nodedc-source/server/assets/provider-packages/v1/catalog.json"
|
||||
).read())
|
||||
provider_catalog_source = archive.extractfile(
|
||||
"payload/nodedc-source/server/dataProductPublishGrant/providerCatalog.js"
|
||||
).read().decode("utf-8")
|
||||
service_source = archive.extractfile(
|
||||
"payload/nodedc-source/server/dataProductPublishGrant/service.js"
|
||||
).read().decode("utf-8")
|
||||
store_source = archive.extractfile(
|
||||
"payload/nodedc-source/server/dataProductPublishGrant/store.js"
|
||||
).read().decode("utf-8")
|
||||
|
||||
self.assertEqual(files, EXPECTED_ENTRIES)
|
||||
self.assertEqual(
|
||||
@@ -115,6 +140,29 @@ class EnginePublishGrantArtifactTest(unittest.TestCase):
|
||||
)
|
||||
self.assertIn("read_only: true", runtime_override)
|
||||
self.assertEqual(runtime_override.count("create_host_path: false"), 2)
|
||||
provider = catalog["packages"][0]
|
||||
self.assertEqual(first["providerPackage"], "gelios.provider.v4")
|
||||
self.assertEqual(first["dataProductId"], "fleet.positions.current.v3")
|
||||
self.assertEqual(first["credentialValues"], "preserved")
|
||||
self.assertEqual(provider["id"], "gelios.provider.v4")
|
||||
self.assertEqual(
|
||||
[capability["id"] for capability in provider["capabilities"]],
|
||||
[
|
||||
"gelios.monitoring_config.current.read",
|
||||
"gelios.units.current.read",
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
[capability["request"]["url"] for capability in provider["capabilities"]],
|
||||
[
|
||||
"https://api.geliospro.com/api/v1/users/me/monitoring-config",
|
||||
"https://api.geliospro.com/api/v1/units?incltrip=true",
|
||||
],
|
||||
)
|
||||
self.assertIn("function capabilityRequests(capability)", provider_catalog_source)
|
||||
self.assertIn("exactHttpRequestUrl(n8n)", provider_catalog_source)
|
||||
self.assertIn("providerRequestNodeIds", service_source)
|
||||
self.assertIn("providerRequestNodeIds", store_source)
|
||||
|
||||
def test_builder_requires_a_fresh_never_issued_patch_id(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-engine-publish-id-") as directory:
|
||||
@@ -140,6 +188,8 @@ class EnginePublishGrantArtifactTest(unittest.TestCase):
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn(expected, result.stderr)
|
||||
|
||||
if not self.historical_source_is_current():
|
||||
return
|
||||
self.build(artifact_dir, PATCH_ID)
|
||||
duplicate = subprocess.run(
|
||||
["node", str(BUILDER), PATCH_ID],
|
||||
|
||||
@@ -20,7 +20,9 @@ EXPECTED_ENTRIES = [
|
||||
"platform/packages/external-provider-contract/src/data-product.mjs",
|
||||
"platform/packages/external-provider-contract/src/intake-batch.mjs",
|
||||
"platform/packages/external-provider-contract/src/index.mjs",
|
||||
"platform/packages/external-provider-contract/src/provider-package.mjs",
|
||||
"platform/packages/external-provider-contract/src/sensitive-field-policy.mjs",
|
||||
"platform/packages/external-provider-contract/providers/gelios",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/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
|
||||
BUILDER = SCRIPT_DIR / "build-module-foundry-consumer-policy-artifact.mjs"
|
||||
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
|
||||
PATCH_ID = "module-foundry-consumer-policy-v4-unit-001"
|
||||
EXPECTED_FILES = (
|
||||
"registry/data-product-consumer-policies.json",
|
||||
"scripts/validate-registry.mjs",
|
||||
"server/foundry-data-product-consumer.mjs",
|
||||
"server/foundry-data-product-consumer.test.mjs",
|
||||
)
|
||||
|
||||
|
||||
def load_runner():
|
||||
loader = importlib.machinery.SourceFileLoader(
|
||||
"nodedc_module_foundry_consumer_policy_artifact",
|
||||
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 ModuleFoundryConsumerPolicyArtifactTest(unittest.TestCase):
|
||||
def build(self, artifact_dir):
|
||||
environment = os.environ.copy()
|
||||
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
|
||||
completed = subprocess.run(
|
||||
["node", str(BUILDER), PATCH_ID],
|
||||
cwd=PLATFORM_ROOT,
|
||||
env=environment,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
def test_builder_is_deterministic_exact_scope_and_runner_compatible(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-foundry-policy-v4-") 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(tuple(first["files"]), EXPECTED_FILES)
|
||||
|
||||
extracted = root / "loaded"
|
||||
extracted.mkdir()
|
||||
manifest, entries, payload = RUNNER.load_artifact(first_artifact, extracted)
|
||||
self.assertEqual(manifest["component"], "module-foundry")
|
||||
self.assertEqual(tuple(entries), EXPECTED_FILES)
|
||||
self.assertEqual(RUNNER.component_services("module-foundry", entries), ("nodedc-module-foundry",))
|
||||
|
||||
registry = json.loads((payload / EXPECTED_FILES[0]).read_text(encoding="utf-8"))
|
||||
policies = [
|
||||
policy for policy in registry["policies"]
|
||||
if policy["dataProductId"] == "fleet.positions.current.v4"
|
||||
and policy["productVersion"] == "4.0.0"
|
||||
]
|
||||
self.assertEqual(len(policies), 1)
|
||||
policy = policies[0]
|
||||
self.assertEqual(policy["id"], "map-moving-object-current-v4")
|
||||
self.assertEqual(policy["statusContract"]["attribute"], "signal_state")
|
||||
self.assertEqual(policy["statusContract"]["allowedValues"], ["active", "inactive"])
|
||||
self.assertEqual(policy["statusContract"]["freshness"], "none")
|
||||
self.assertIsNone(policy["staleAfterMs"])
|
||||
|
||||
with tarfile.open(first_artifact, "r:gz") as archive:
|
||||
members = archive.getmembers()
|
||||
names = [member.name for member in members]
|
||||
regular_payloads = [
|
||||
archive.extractfile(member).read()
|
||||
for member in members
|
||||
if member.isfile()
|
||||
]
|
||||
self.assertFalse(any(Path(name).name.startswith("._") for name in names))
|
||||
self.assertFalse(any("/.git/" in name or "/node_modules/" in name or "/runtime-data/" in name for name in names))
|
||||
self.assertNotIn(b"-----BEGIN PRIVATE KEY-----", b"\n".join(regular_payloads))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/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
|
||||
BUILDER = SCRIPT_DIR / "build-module-foundry-filter-toggle-artifact.mjs"
|
||||
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
|
||||
PATCH_ID = "module-foundry-filter-toggle-unit-001"
|
||||
EXPECTED_FILES = (
|
||||
"apps/catalog/src/MapFixturePreview.tsx",
|
||||
"apps/catalog/src/mapPresentationProfile.ts",
|
||||
"scripts/map-presentation-filters.test.mjs",
|
||||
)
|
||||
|
||||
|
||||
def load_runner():
|
||||
loader = importlib.machinery.SourceFileLoader(
|
||||
"nodedc_module_foundry_filter_toggle_artifact",
|
||||
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 ModuleFoundryFilterToggleArtifactTest(unittest.TestCase):
|
||||
def build(self, artifact_dir):
|
||||
environment = os.environ.copy()
|
||||
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
|
||||
completed = subprocess.run(
|
||||
["node", str(BUILDER), PATCH_ID],
|
||||
cwd=PLATFORM_ROOT,
|
||||
env=environment,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
def test_builder_is_deterministic_exact_scope_and_runner_compatible(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-foundry-filter-toggle-") 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(tuple(first["files"]), EXPECTED_FILES)
|
||||
|
||||
extracted = root / "loaded"
|
||||
extracted.mkdir()
|
||||
manifest, entries, payload = RUNNER.load_artifact(first_artifact, extracted)
|
||||
self.assertEqual(manifest["component"], "module-foundry")
|
||||
self.assertEqual(tuple(entries), EXPECTED_FILES)
|
||||
self.assertEqual(RUNNER.component_services("module-foundry", entries), ("nodedc-module-foundry",))
|
||||
|
||||
helper = (payload / EXPECTED_FILES[1]).read_text(encoding="utf-8")
|
||||
preview = (payload / EXPECTED_FILES[0]).read_text(encoding="utf-8")
|
||||
regression = (payload / EXPECTED_FILES[2]).read_text(encoding="utf-8")
|
||||
self.assertIn("toggleMapPresentationFacetSelection", helper)
|
||||
self.assertIn("return unconstrained", helper)
|
||||
self.assertIn("filters: toggleMapPresentationFacetSelection(filters, field, value)", preview)
|
||||
self.assertIn("interactive deselect of the last chip removes the facet constraint", regression)
|
||||
|
||||
with tarfile.open(first_artifact, "r:gz") as archive:
|
||||
members = archive.getmembers()
|
||||
names = [member.name for member in members]
|
||||
regular_payloads = [
|
||||
archive.extractfile(member).read()
|
||||
for member in members
|
||||
if member.isfile()
|
||||
]
|
||||
self.assertFalse(any(Path(name).name.startswith("._") for name in names))
|
||||
self.assertFalse(any("/.git/" in name or "/node_modules/" in name or "/runtime-data/" in name for name in names))
|
||||
self.assertNotIn(b"-----BEGIN PRIVATE KEY-----", b"\n".join(regular_payloads))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -372,8 +372,10 @@ class CanonicalPlatformRegistryTest(unittest.TestCase):
|
||||
|
||||
original_root = RUNNER.COMPONENTS["engine"]["payload_root"]
|
||||
original_hashes = RUNNER.ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_SHA256
|
||||
original_installed_hashes = RUNNER.ENGINE_DATA_PRODUCT_PUBLISH_GRANT_INSTALLED_FOUNDATION_SHA256
|
||||
RUNNER.COMPONENTS["engine"]["payload_root"] = root
|
||||
RUNNER.ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_SHA256 = expected
|
||||
RUNNER.ENGINE_DATA_PRODUCT_PUBLISH_GRANT_INSTALLED_FOUNDATION_SHA256 = expected
|
||||
try:
|
||||
self.assertEqual(
|
||||
RUNNER.preflight_engine_data_product_publish_grant_predecessor(),
|
||||
@@ -391,6 +393,7 @@ class CanonicalPlatformRegistryTest(unittest.TestCase):
|
||||
RUNNER.preflight_engine_data_product_publish_grant_predecessor()
|
||||
finally:
|
||||
RUNNER.ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_SHA256 = original_hashes
|
||||
RUNNER.ENGINE_DATA_PRODUCT_PUBLISH_GRANT_INSTALLED_FOUNDATION_SHA256 = original_installed_hashes
|
||||
RUNNER.COMPONENTS["engine"]["payload_root"] = original_root
|
||||
|
||||
def test_installed_publish_update_preserves_foundation_and_allows_only_grant_source_changes(self):
|
||||
@@ -441,8 +444,10 @@ class CanonicalPlatformRegistryTest(unittest.TestCase):
|
||||
|
||||
original_root = RUNNER.COMPONENTS["engine"]["payload_root"]
|
||||
original_hashes = RUNNER.ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_SHA256
|
||||
original_installed_hashes = RUNNER.ENGINE_DATA_PRODUCT_PUBLISH_GRANT_INSTALLED_FOUNDATION_SHA256
|
||||
RUNNER.COMPONENTS["engine"]["payload_root"] = root
|
||||
RUNNER.ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_SHA256 = expected
|
||||
RUNNER.ENGINE_DATA_PRODUCT_PUBLISH_GRANT_INSTALLED_FOUNDATION_SHA256 = expected
|
||||
try:
|
||||
self.assertEqual(
|
||||
RUNNER.preflight_engine_data_product_publish_grant_predecessor(payload),
|
||||
@@ -470,6 +475,99 @@ class CanonicalPlatformRegistryTest(unittest.TestCase):
|
||||
RUNNER.preflight_engine_data_product_publish_grant_predecessor(payload)
|
||||
finally:
|
||||
RUNNER.ENGINE_DATA_PRODUCT_PUBLISH_GRANT_PREDECESSOR_SHA256 = original_hashes
|
||||
RUNNER.ENGINE_DATA_PRODUCT_PUBLISH_GRANT_INSTALLED_FOUNDATION_SHA256 = original_installed_hashes
|
||||
RUNNER.COMPONENTS["engine"]["payload_root"] = original_root
|
||||
|
||||
def test_composite_provider_update_requires_exact_v3_to_v4_catalog_and_four_paths(self):
|
||||
engine_source = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA"
|
||||
current_target_sha256 = {
|
||||
rel: hashlib.sha256((engine_source / rel).read_bytes()).hexdigest()
|
||||
for rel in RUNNER.ENGINE_COMPOSITE_PROVIDER_V4_ARTIFACT_ENTRIES
|
||||
}
|
||||
if current_target_sha256 != RUNNER.ENGINE_COMPOSITE_PROVIDER_V4_TARGET_SHA256:
|
||||
self.skipTest("historical v3-to-v4 fixture source has advanced to its exact successor")
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-composite-provider-update-") as directory:
|
||||
base = Path(directory)
|
||||
root = base / "installed"
|
||||
payload = base / "payload"
|
||||
root.mkdir()
|
||||
payload.mkdir()
|
||||
|
||||
v3_catalog = {
|
||||
"schemaVersion": "nodedc.engine.provider-security-catalog/v1",
|
||||
"packages": [{
|
||||
"id": "gelios.provider.v3",
|
||||
"version": "3.0.0",
|
||||
"providerId": "gelios",
|
||||
"providerCredential": {
|
||||
"authModeId": "gelios.rest-rotating-bearer.v3",
|
||||
"credentialType": "httpBearerAuth",
|
||||
},
|
||||
"capabilities": [{
|
||||
"id": "gelios.units.current.read",
|
||||
"classification": "read",
|
||||
"status": "implemented",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "https://api.geliospro.com/api/v1/units",
|
||||
},
|
||||
"dataProductIds": ["fleet.positions.current.v2"],
|
||||
}],
|
||||
"publisher": {
|
||||
"nodeType": "n8n-nodes-ndc.ndcDataProductPublish",
|
||||
"credentialType": "ndcDataProductWriterApi",
|
||||
},
|
||||
}],
|
||||
}
|
||||
installed_catalog = f"{json.dumps(v3_catalog, indent=2)}\n"
|
||||
self.assertEqual(
|
||||
hashlib.sha256(installed_catalog.encode()).hexdigest(),
|
||||
RUNNER.ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CATALOG_PREDECESSOR_SHA256,
|
||||
)
|
||||
|
||||
for rel in RUNNER.ENGINE_DATA_PRODUCT_PUBLISH_GRANT_ARTIFACT_ENTRIES:
|
||||
if rel == "nodedc-source/server/dataProductPublishGrant":
|
||||
continue
|
||||
for destination in (root, payload):
|
||||
path = destination / rel
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(f"shared:{rel}\n", encoding="utf-8")
|
||||
for name in RUNNER.ENGINE_DATA_PRODUCT_PUBLISH_GRANT_SOURCE_FILES:
|
||||
rel = f"nodedc-source/server/dataProductPublishGrant/{name}"
|
||||
for destination in (root, payload):
|
||||
path = destination / rel
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(f"installed:{name}\n", encoding="utf-8")
|
||||
|
||||
catalog_rel = RUNNER.ENGINE_PROVIDER_SECURITY_CATALOG_REL
|
||||
(root / catalog_rel).write_text(installed_catalog, encoding="utf-8")
|
||||
for rel in RUNNER.ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CHANGED_PATHS:
|
||||
source = engine_source / rel
|
||||
(payload / rel).write_bytes(source.read_bytes())
|
||||
|
||||
original_root = RUNNER.COMPONENTS["engine"]["payload_root"]
|
||||
RUNNER.COMPONENTS["engine"]["payload_root"] = root
|
||||
try:
|
||||
with mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_installed_engine_data_product_publish_grant_foundation",
|
||||
return_value="compose-sha256",
|
||||
):
|
||||
self.assertEqual(
|
||||
RUNNER.preflight_engine_data_product_publish_grant_predecessor(payload),
|
||||
{
|
||||
"mode": "installed-composite-provider-update",
|
||||
"compose_sha256": "compose-sha256",
|
||||
"changed_paths": RUNNER.ENGINE_PUBLISH_GRANT_COMPOSITE_PROVIDER_CHANGED_PATHS,
|
||||
},
|
||||
)
|
||||
(root / catalog_rel).write_text("{}\n", encoding="utf-8")
|
||||
with self.assertRaisesRegex(
|
||||
RUNNER.DeployError,
|
||||
"composite provider catalog transition mismatch",
|
||||
):
|
||||
RUNNER.preflight_engine_data_product_publish_grant_predecessor(payload)
|
||||
finally:
|
||||
RUNNER.COMPONENTS["engine"]["payload_root"] = original_root
|
||||
|
||||
def test_edp_healthcheck_requires_database_and_managed_provisioning(self):
|
||||
|
||||
Reference in New Issue
Block a user