#!/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 workspaceRoot = resolve(platformRoot, '..') const engineRoot = resolve(workspaceRoot, 'NODEDC_ENGINE_INFRA') const artifactRoot = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || join(workspaceRoot, 'deploy-artifacts')) const [patchId = '', ...extra] = process.argv.slice(2) const storeRelativePath = 'nodedc-source/server/engineAgents/store.js' const predecessorSha256 = '52daa43499d6d9a97fe7ffa891edb9212b7791e733f91dd3ca686d42739b7e9a' const targetSha256 = '2e62654c2dc12905efcc83a9dff45a818dd7b47924a10600160835c4416540e9' const readerExtendedSourceSha256 = 'debd351fe0b8c72b33b8c79909b8f339cbaf061b970576f3ae72df52ebaa211f' const ontologyExtendedSourceSha256 = '4cd4bdd5958cfafee184e98a04fe12aa0c1cbe884326beaec63c99f9fff61285' const frozenTargetArtifact = join( workspaceRoot, 'deploy-artifacts/nodedc-engine-agent-full-grant-migration-20260717-002.tgz', ) const frozenTargetArtifactSha256 = 'd104ee6e63d1fccb9069e2b3db5e0446907ee9bf9cb04f2387636c272f803d68' const previouslyIssuedPatchIds = new Set([ 'engine-agent-full-grant-migration-20260717-001', ]) if (extra.length || !/^engine-agent-full-grant-migration-\d{8}-\d{3}$/.test(patchId)) { throw new Error('usage: build-engine-agent-full-grant-migration-artifact.mjs ') } if (previouslyIssuedPatchIds.has(patchId)) { throw new Error('engine_agent_full_grant_migration_patch_id_already_issued') } const source = join(engineRoot, storeRelativePath) const sourceInfo = await lstat(source) if (sourceInfo.isSymbolicLink() || !sourceInfo.isFile()) throw new Error('engine_agent_store_source_unsafe') const sourceBytes = await materializeFrozenTarget(await readFile(source)) if (digest(sourceBytes) !== targetSha256) throw new Error('engine_agent_store_target_sha256_mismatch') const sourceText = sourceBytes.toString('utf8') for (const required of [ 'const STORE_VERSION = 2', "export const ENGINE_AGENT_FULL_DEVELOPER_PROFILE = 'full-developer'", "export const ENGINE_AGENT_CUSTOM_PROFILE = 'custom'", 'const LEGACY_FULL_DEVELOPER_SCOPES = Object.freeze([', "'engine:l2:data-product-publish-grant:plan'", "'engine:l2:data-product-publish-grant:write'", 'const migrateLegacyFullDeveloper = sourceVersion === 1', 'LEGACY_FULL_DEVELOPER_SCOPES.every((scope) => scopes.includes(scope))', 'profile === ENGINE_AGENT_FULL_DEVELOPER_PROFILE ? [...ENGINE_AGENT_SCOPES] : scopes', "throw new Error('engine_agent_store_version_unsupported')", ]) { if (!sourceText.includes(required)) throw new Error(`engine_agent_store_contract_missing:${required}`) } if (/gelios|robot2b/i.test(sourceText)) throw new Error('engine_agent_store_provider_logic_forbidden') run('node', ['--check', source]) await mkdir(artifactRoot, { recursive: true }) const artifact = join(artifactRoot, `nodedc-${patchId}.tgz`) await assertFresh(artifact) const stage = await mkdtemp(join(tmpdir(), 'nodedc-engine-agent-grant-migration-')) try { const destination = join(stage, 'payload', storeRelativePath) await mkdir(dirname(destination), { recursive: true }) await writeFile(destination, sourceBytes) await writeFile( join(stage, 'manifest.env'), `id=${patchId}\ncomponent=engine\ntype=app-overlay\n`, 'utf8', ) await writeFile(join(stage, 'files.txt'), `${storeRelativePath}\n`, 'utf8') run('python3', ['-c', canonicalTarScript(), artifact, stage]) const artifactBytes = await readFile(artifact) console.log(JSON.stringify({ ok: true, patchId, artifact, sha256: digest(artifactBytes), entries: [storeRelativePath], predecessorSha256, targetSha256, services: ['nodedc-backend'], excluded: ['n8n', 'app', 'databases', 'credentials', 'runtime-data'], }, null, 2)) } finally { await rm(stage, { recursive: true, force: true }) } async function assertFresh(target) { try { await lstat(target) } catch (error) { if (error?.code === 'ENOENT') return throw error } throw new Error('engine_agent_full_grant_migration_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 run(command, args) { const result = spawnSync(command, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 16 * 1024 * 1024, }) if (result.status !== 0) throw new Error(`${command}_failed:${result.stderr || result.stdout}`) return result } function digest(bytes) { return createHash('sha256').update(bytes).digest('hex') } async function materializeFrozenTarget(bytes) { const sourceSha256 = digest(bytes) if (sourceSha256 === targetSha256) return bytes if (sourceSha256 === ontologyExtendedSourceSha256) { const artifactBytes = await readFile(frozenTargetArtifact) if (digest(artifactBytes) !== frozenTargetArtifactSha256) { throw new Error('engine_agent_store_frozen_target_artifact_sha256_mismatch') } const script = [ 'import pathlib,sys,tarfile', 'p=pathlib.Path(sys.argv[1])', "with tarfile.open(p,'r:gz') as t:", " f=t.extractfile('payload/nodedc-source/server/engineAgents/store.js')", " if f is None: raise SystemExit('member-unreadable')", ' sys.stdout.buffer.write(f.read())', ].join('\n') const frozen = Buffer.from(run('python3', ['-c', script, frozenTargetArtifact]).stdout, 'utf8') if (digest(frozen) !== targetSha256) { throw new Error('engine_agent_store_frozen_target_sha256_mismatch') } return frozen } if (sourceSha256 !== readerExtendedSourceSha256) { throw new Error('engine_agent_store_target_sha256_mismatch') } const text = bytes.toString('utf8') const readerScopes = [ " 'engine:l2:data-product-read-grant:plan',\n", " 'engine:l2:data-product-read-grant:write',\n", ] let frozen = text for (const scope of readerScopes) { if (!frozen.includes(scope)) throw new Error('engine_agent_store_reader_scope_boundary_missing') frozen = frozen.replace(scope, '') } return Buffer.from(frozen, 'utf8') }