Add exact Engine L2 reconciliation transition

This commit is contained in:
Codex 2026-07-23 13:10:11 +03:00
parent ad3760e767
commit 50cba59bde
4 changed files with 1114 additions and 15 deletions

View File

@ -23,6 +23,22 @@ const engineRoot = resolve(
const artifactRoot = resolve( const artifactRoot = resolve(
process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"), process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(here, "../deploy-artifacts"),
); );
const baselineArtifact = resolve(
here,
"../deploy-artifacts/nodedc-engine-provider-authority-diagnostics-20260723-029.tgz",
);
const baselineArtifactSha256 =
"b4a0a1f707bf9814a4decc3f7f261b8afffe5727af2129c14bcdc76f81675e38";
const descriptorRel =
"nodedc-source/services/node-intelligence/activation.json";
const baselineDescriptorSha256 =
"84b0e15a10cedf334ad04d6f31c908da2dc155503c9974f0eeb75b01e20fb884";
const predecessorGatewaySha256 =
"5331f6dc8dc306f641370a2968eb025ee3e278e27ae9a217e4cfc2901fec369c";
const targetGatewaySha256 =
"4f600a2781be9118bec891fa2a6f20d7f55e0892ef2f06af24059193cebd28f4";
const targetDescriptorSha256 =
"63e7619c6971d02102583bb5d80d33ece293b952f113200fa0b76f0e88c2dd32";
const [patchId = "", ...extra] = process.argv.slice(2); const [patchId = "", ...extra] = process.argv.slice(2);
if (extra.length || !/^engine-l2-closed-loop-\d{8}-\d{3}$/.test(patchId)) { if (extra.length || !/^engine-l2-closed-loop-\d{8}-\d{3}$/.test(patchId)) {
throw new Error( throw new Error(
@ -45,6 +61,7 @@ const entries = Object.freeze([
"nodedc-source/src/utils/n8nApi.ts", "nodedc-source/src/utils/n8nApi.ts",
"nodedc-source/dist/index.html", "nodedc-source/dist/index.html",
"nodedc-source/dist/assets", "nodedc-source/dist/assets",
descriptorRel,
]); ]);
const targetSha256 = Object.freeze({ const targetSha256 = Object.freeze({
"nodedc-source/server/index.js": "nodedc-source/server/index.js":
@ -73,20 +90,30 @@ const targetSha256 = Object.freeze({
"18322addd45c126a7b8396f36b005f3085fddba3e9b346dd2c910f6fa6987ebf", "18322addd45c126a7b8396f36b005f3085fddba3e9b346dd2c910f6fa6987ebf",
"nodedc-source/dist/assets/index-CqvJfRRS.js": "nodedc-source/dist/assets/index-CqvJfRRS.js":
"06e6c23b03ea2ba8a4890e1f1714fd08ebaf18d735834200af6ab430af360ca8", "06e6c23b03ea2ba8a4890e1f1714fd08ebaf18d735834200af6ab430af360ca8",
[descriptorRel]: targetDescriptorSha256,
}); });
const artifact = join(artifactRoot, `nodedc-${patchId}.tgz`); const artifact = join(artifactRoot, `nodedc-${patchId}.tgz`);
await assertFresh(artifact); await assertFresh(artifact);
assertSourceCommit(); assertSourceCommit();
await assertExactSources(); await assertExactSources();
const targetDescriptor = await buildTargetDescriptor();
const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-l2-closed-loop-")); const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-l2-closed-loop-"));
try { try {
const payload = join(stage, "payload"); const payload = join(stage, "payload");
for (const relativePath of entries) { for (const relativePath of entries) {
const source = join(engineRoot, relativePath);
const destination = join(payload, relativePath); const destination = join(payload, relativePath);
await mkdir(dirname(destination), { recursive: true }); await mkdir(dirname(destination), { recursive: true });
if (relativePath === descriptorRel) {
await writeFile(destination, targetDescriptor, {
encoding: "utf8",
flag: "wx",
mode: 0o644,
});
continue;
}
const source = join(engineRoot, relativePath);
const info = await lstat(source); const info = await lstat(source);
if (info.isSymbolicLink()) { if (info.isSymbolicLink()) {
throw new Error(`engine_l2_closed_loop_source_symlink:${relativePath}`); throw new Error(`engine_l2_closed_loop_source_symlink:${relativePath}`);
@ -130,6 +157,12 @@ try {
], ],
mcpVersion: "0.7.0", mcpVersion: "0.7.0",
graphContract: "semantic-revision-cas-v1", graphContract: "semantic-revision-cas-v1",
transition: "failed-030-partial-source-reconciliation",
predecessorDescriptorSha256: baselineDescriptorSha256,
targetDescriptorSha256,
predecessorGatewaySha256,
targetGatewaySha256,
recoveryArtifact: "nodedc-engine-l2-closed-loop-20260723-030.tgz",
actorPlanes: ["external_codex_mcp", "ai_workspace", "engine_ui"], actorPlanes: ["external_codex_mcp", "ai_workspace", "engine_ui"],
untouched: [ untouched: [
"L2 graph data", "L2 graph data",
@ -153,9 +186,12 @@ function assertSourceCommit() {
} }
async function assertExactSources() { async function assertExactSources() {
const expectedPaths = Object.keys(targetSha256).sort(); const sourceEntries = entries.filter((entry) => entry !== descriptorRel);
const expectedPaths = Object.keys(targetSha256)
.filter((entry) => entry !== descriptorRel)
.sort();
const actualPaths = []; const actualPaths = [];
for (const entry of entries) { for (const entry of sourceEntries) {
const source = join(engineRoot, entry); const source = join(engineRoot, entry);
const info = await lstat(source); const info = await lstat(source);
if (info.isSymbolicLink() || (!info.isFile() && !info.isDirectory())) { if (info.isSymbolicLink() || (!info.isFile() && !info.isDirectory())) {
@ -188,6 +224,39 @@ async function assertExactSources() {
} }
} }
async function buildTargetDescriptor() {
const baselineBytes = await readFile(baselineArtifact);
if (digest(baselineBytes) !== baselineArtifactSha256) {
throw new Error("engine_l2_closed_loop_baseline_artifact_mismatch");
}
const baseline = run("tar", [
"-xOf",
baselineArtifact,
`payload/${descriptorRel}`,
]).stdout;
if (digest(Buffer.from(baseline, "utf8")) !== baselineDescriptorSha256) {
throw new Error("engine_l2_closed_loop_baseline_descriptor_mismatch");
}
const descriptor = JSON.parse(baseline);
if (
descriptor?.schemaVersion !==
"nodedc.engine-node-intelligence-transition/v1" ||
descriptor?.action !== "activate" ||
descriptor?.releaseId !== "2.33.2-974a9fb3492f" ||
descriptor?.source?.gatewaySha256 !== predecessorGatewaySha256 ||
descriptor?.source?.upstreamProjectionSha256 !==
"2dfa6b4f37d9bfa8b92a8109d4060d02dd2634ceb8f7924504b83ccf3fdd1523"
) {
throw new Error("engine_l2_closed_loop_baseline_descriptor_contract_mismatch");
}
descriptor.source.gatewaySha256 = targetGatewaySha256;
const rendered = `${JSON.stringify(descriptor, null, 2)}\n`;
if (digest(Buffer.from(rendered, "utf8")) !== targetDescriptorSha256) {
throw new Error("engine_l2_closed_loop_target_descriptor_mismatch");
}
return rendered;
}
async function assertFresh(path) { async function assertFresh(path) {
try { try {
await lstat(path); await lstat(path);

View File

@ -235,6 +235,76 @@ ENGINE_MCP_AUTONOMY_PROVIDER_V5_TARGET_SHA256 = {
ENGINE_MCP_AUTONOMY_PROVIDER_V5_NEW_PATHS = ( ENGINE_MCP_AUTONOMY_PROVIDER_V5_NEW_PATHS = (
"nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.6.tgz", "nodedc-source/server/assets/nodedc-engine-codex-agent-0.1.6.tgz",
) )
ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL = ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL
ENGINE_L2_CLOSED_LOOP_ARTIFACT_ENTRIES = (
"nodedc-source/server/index.js",
"nodedc-source/server/l2/graphRepository.js",
"nodedc-source/server/realtime/ws.js",
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL,
"nodedc-source/server/routes/n8n.js",
"nodedc-source/server/routes/ndcAgentMcp.js",
"nodedc-source/src/App.tsx",
"nodedc-source/src/n8n/N8nSubworkflowHost.tsx",
"nodedc-source/src/realtime/useMultiplayer.ts",
"nodedc-source/src/utils/n8nApi.ts",
"nodedc-source/dist/index.html",
"nodedc-source/dist/assets",
ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL,
)
ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256 = {
"nodedc-source/server/index.js": "1896e1cade61579863c50ff3f52f2e81ff27f2a511a9d362db81925ee21cefad",
"nodedc-source/server/l2/graphRepository.js": "94ad08e1bb7e7be04854f1f911e06ee1acd6e09631f33f4c01e42e230665ac33",
"nodedc-source/server/realtime/ws.js": "82cfa833e05c2fc6d6049dd4564af06164b5c784fdfb82c243ca67519f4509c2",
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "4f600a2781be9118bec891fa2a6f20d7f55e0892ef2f06af24059193cebd28f4",
"nodedc-source/server/routes/n8n.js": "752cb1524160adc1c95163c34e139b615c063d2ce8980f2a63879bddbd0f1e08",
"nodedc-source/server/routes/ndcAgentMcp.js": "353a10291ceb93c3641ece60ec6e809a394be86f5ceb97cb44755178940409dd",
"nodedc-source/src/App.tsx": "b3e61a89330b774f309660208d05143d299ab582f18765c1c14e2122a62c4d5e",
"nodedc-source/src/n8n/N8nSubworkflowHost.tsx": "683aa44ba92aeac4879889e2bdb5319282976d7680d620701578830ce9677087",
"nodedc-source/src/realtime/useMultiplayer.ts": "a4d001d9914098e50a78d8abe0a66344d23680b7a19b3573aa7b6f48c8bb9c35",
"nodedc-source/src/utils/n8nApi.ts": "6f16d4992c51ffcc1e71a7bd172fd9ceb440d6e1a74d69ef1db62e0ac4230b3c",
"nodedc-source/dist/index.html": "90d72f8790dc8cf951066b1210447c84d640373117bae23f3a4cb3227fb96d6d",
"nodedc-source/dist/assets/index-Bim2pv1P.css": "18322addd45c126a7b8396f36b005f3085fddba3e9b346dd2c910f6fa6987ebf",
"nodedc-source/dist/assets/index-CqvJfRRS.js": "06e6c23b03ea2ba8a4890e1f1714fd08ebaf18d735834200af6ab430af360ca8",
ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL: "84b0e15a10cedf334ad04d6f31c908da2dc155503c9974f0eeb75b01e20fb884",
}
ENGINE_L2_CLOSED_LOOP_TARGET_SHA256 = {
**ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256,
ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL: "63e7619c6971d02102583bb5d80d33ece293b952f113200fa0b76f0e88c2dd32",
}
ENGINE_L2_CLOSED_LOOP_STABLE_SHA256 = {
"nodedc-source/server/index.js": "0ac408e0e9a7bc5c8e13a00afc957b982b065e2bc414919839e0b0a11aa05ba4",
"nodedc-source/server/realtime/ws.js": "340437ee2c6cc06c41b1e47f8b1ada24b68e175bb6fc4c361398bef00dd89686",
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: "5331f6dc8dc306f641370a2968eb025ee3e278e27ae9a217e4cfc2901fec369c",
"nodedc-source/server/routes/n8n.js": "9bc3638e271102abec91bb80413329befb89d60c0e4dc0548f0dd11e93220d0a",
"nodedc-source/server/routes/ndcAgentMcp.js": "534e2c85e1faecc72a00da7ad32d0584ee09b6bd89eeec2221c3b23d80e1e962",
"nodedc-source/src/App.tsx": "d9bc0f23ceaaf4f5534ac9a9b1f97d84fd8eee4679c59b5999b5a75b8c77bb32",
"nodedc-source/src/n8n/N8nSubworkflowHost.tsx": "306694f329670a64322651ad249b057a74b34dace82d68d4071a365636d91d71",
"nodedc-source/src/realtime/useMultiplayer.ts": "3d8f150037d994dc8739fa9f6696acfe874c45128e567a6090cc4996b76f9e9d",
"nodedc-source/src/utils/n8nApi.ts": "2d10acda54d8c10637008612af9e3ad7625bd7fe17e9cfa38aa45c340eb171eb",
"nodedc-source/dist/index.html": "8894f62ad590168862e81266bc4602410279634b666af72cb14ad80aeb71ea74",
"nodedc-source/dist/assets/index-4TenBzkg.js": "a48fcacf3348c20a432cd6da2627b89c0c3e54bc989c727b620125dd3412538a",
"nodedc-source/dist/assets/index-BQ3RBHy9.js": "6e0f4c841afe3e0beb398943af915c8ee5d4a7df0b77b92e277e66faa3fa26a8",
"nodedc-source/dist/assets/index-Bim2pv1P.css": "18322addd45c126a7b8396f36b005f3085fddba3e9b346dd2c910f6fa6987ebf",
"nodedc-source/dist/assets/index-DJ1CMfu4.js": "a0aaf72d2ed390142ff2ea03dbcfdd534637e5faefd80d8aa7d705a804abe065",
ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL: "84b0e15a10cedf334ad04d6f31c908da2dc155503c9974f0eeb75b01e20fb884",
}
ENGINE_L2_CLOSED_LOOP_FAILED_PATCH_ID = "engine-l2-closed-loop-20260723-030"
ENGINE_L2_CLOSED_LOOP_FAILED_ARTIFACT = (
"nodedc-engine-l2-closed-loop-20260723-030.tgz.20260723-123228"
)
ENGINE_L2_CLOSED_LOOP_FAILED_ARTIFACT_SHA256 = (
"4c8401fcb1d318c52933cc3327015e9e4cbaaa5c38f1b45856acec2c72efdd2e"
)
ENGINE_L2_CLOSED_LOOP_RECOVERY_BACKUP_ID = (
"engine-engine-l2-closed-loop-20260723-030-20260723-123228"
)
ENGINE_L2_CLOSED_LOOP_RECOVERY_BACKUP_SHA256 = {
"manifest.env": "7cf2a04c64f62547d408613307f7880bf350b9d305306af0e4eb8b81cfaa73bc",
"files.txt": "c4d2e0395b8ff076b648bd348283e4113feecf50b056ba6d1f495bafe4d09f27",
"source-before.tgz": "056d9400d52eeda7a51d225a803ab4d3465e06541d6646b4b728296a21c08ec5",
"existing-files.txt": "4c5de3d062d96b398dcaf44a6dde20c168ca0efd263893d51ce1fc545c59b8f8",
"missing-files.txt": "16d708945792cb35fcbe11d2c23a146e489fb183c4f498496ef40a97090c6008",
}
ENGINE_PROVIDER_SECURITY_CATALOG_REL = ( ENGINE_PROVIDER_SECURITY_CATALOG_REL = (
"nodedc-source/server/assets/provider-packages/v1/catalog.json" "nodedc-source/server/assets/provider-packages/v1/catalog.json"
) )
@ -3156,6 +3226,7 @@ def validate_engine_node_intelligence_transition(payload_dir, entries):
"const ENGINE_AGENT_MCP_VERSION = '0.4.0'", "const ENGINE_AGENT_MCP_VERSION = '0.4.0'",
"const ENGINE_AGENT_MCP_VERSION = '0.5.0'", "const ENGINE_AGENT_MCP_VERSION = '0.5.0'",
"const ENGINE_AGENT_MCP_VERSION = '0.6.0'", "const ENGINE_AGENT_MCP_VERSION = '0.6.0'",
"const ENGINE_AGENT_MCP_VERSION = '0.7.0'",
) )
): ):
die("Engine node-intelligence gateway MCP version is not registered") die("Engine node-intelligence gateway MCP version is not registered")
@ -4658,6 +4729,87 @@ def validate_engine_provider_authority_diagnostics_slice(payload_dir, entries):
die("Engine private node validation reconciliation descriptor mismatch") die("Engine private node validation reconciliation descriptor mismatch")
def validate_engine_l2_closed_loop_payload(payload_dir, entries):
if tuple(entries) != ENGINE_L2_CLOSED_LOOP_ARTIFACT_ENTRIES:
die("Engine L2 closed-loop files.txt exact set/order mismatch")
actual = collect_exact_files(
payload_dir,
entries,
"Engine L2 closed-loop payload",
)
if actual != ENGINE_L2_CLOSED_LOOP_TARGET_SHA256:
changed = sorted(
set(actual) ^ set(ENGINE_L2_CLOSED_LOOP_TARGET_SHA256)
| {
rel
for rel in set(actual) & set(ENGINE_L2_CLOSED_LOOP_TARGET_SHA256)
if actual[rel] != ENGINE_L2_CLOSED_LOOP_TARGET_SHA256[rel]
}
)
detail = changed[0] if changed else "unknown"
die(f"Engine L2 closed-loop payload digest mismatch: {detail}")
marker_contract = {
"nodedc-source/server/l2/graphRepository.js": (
"expectedRevision",
"expectedContentRevision",
"writeAtomic",
"hydrateWorkflow",
),
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL: (
"ENGINE_AGENT_MCP_VERSION = '0.7.0'",
"engine_get_node_output_profile",
"expectedRevision",
),
"nodedc-source/server/routes/n8n.js": (
"toSafeNodeOutputProfile",
"expectedRevision",
"l2GraphRepository",
),
"nodedc-source/server/routes/ndcAgentMcp.js": (
"external_codex_mcp",
"l2GraphRepository",
"expectedRevision",
),
"nodedc-source/server/realtime/ws.js": (
"broadcastWorkflowEvent",
),
}
for rel, markers in marker_contract.items():
try:
source = (payload_dir / rel).read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
die(f"Engine L2 closed-loop source is unreadable: {rel}")
if any(marker not in source for marker in markers):
die(f"Engine L2 closed-loop source contract mismatch: {rel}")
for rel in (
"nodedc-source/server/l2/graphRepository.js",
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL,
"nodedc-source/server/routes/ndcAgentMcp.js",
):
source = (payload_dir / rel).read_text(encoding="utf-8")
if re.search(r"\b(?:gelios|robot2b)\b", source, re.IGNORECASE):
die(f"Engine L2 closed-loop provider hardcode is forbidden: {rel}")
descriptor = read_engine_node_intelligence_descriptor(
payload_dir / ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL,
"Engine L2 closed-loop node-intelligence descriptor",
)
if (
descriptor.get("action") != "activate"
or descriptor.get("releaseId") != ENGINE_NODE_INTELLIGENCE_RELEASE_ID
or descriptor.get("source", {}).get("gatewaySha256")
!= ENGINE_L2_CLOSED_LOOP_TARGET_SHA256[
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
]
or descriptor.get("source", {}).get("upstreamProjectionSha256")
!= "2dfa6b4f37d9bfa8b92a8109d4060d02dd2634ceb8f7924504b83ccf3fdd1523"
):
die("Engine L2 closed-loop descriptor target mismatch")
return descriptor
def validate_engine_depttrans_zone_authority_v1_slice(payload_dir, entries): def validate_engine_depttrans_zone_authority_v1_slice(payload_dir, entries):
if tuple(entries) != ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES: if tuple(entries) != ENGINE_DEPTTRANS_ZONE_AUTHORITY_V1_ARTIFACT_ENTRIES:
die("Engine Depttrans zone authority v1 files.txt exact set/order mismatch") die("Engine Depttrans zone authority v1 files.txt exact set/order mismatch")
@ -4822,7 +4974,10 @@ def current_engine_node_intelligence_descriptor():
) )
def validate_installed_engine_node_intelligence_source(descriptor): def validate_installed_engine_node_intelligence_source(
descriptor,
expected_gateway_sha256=None,
):
if descriptor is None or descriptor.get("action") != "activate": if descriptor is None or descriptor.get("action") != "activate":
die("active Engine node-intelligence descriptor is required") die("active Engine node-intelligence descriptor is required")
root = component_root("engine") root = component_root("engine")
@ -4849,8 +5004,15 @@ def validate_installed_engine_node_intelligence_source(descriptor):
path = source_root / rel path = source_root / rel
if path.is_symlink() or sha256_file(path) != expected_sha256: if path.is_symlink() or sha256_file(path) != expected_sha256:
die(f"installed Engine node-intelligence source drift detected: {rel}") die(f"installed Engine node-intelligence source drift detected: {rel}")
gateway_sha256 = (
expected_gateway_sha256
if expected_gateway_sha256 is not None
else descriptor["source"]["gatewaySha256"]
)
if not re.fullmatch(r"[a-f0-9]{64}", str(gateway_sha256)):
die("installed Engine node-intelligence gateway expectation is invalid")
gateway = root / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL gateway = root / ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
if gateway.is_symlink() or sha256_file(gateway) != descriptor["source"]["gatewaySha256"]: if gateway.is_symlink() or sha256_file(gateway) != gateway_sha256:
die("installed Engine node-intelligence gateway drift detected") die("installed Engine node-intelligence gateway drift detected")
override = root / ENGINE_NODE_INTELLIGENCE_OVERRIDE_REL override = root / ENGINE_NODE_INTELLIGENCE_OVERRIDE_REL
try: try:
@ -4868,7 +5030,7 @@ def validate_installed_engine_node_intelligence_source(descriptor):
readme = root / ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL / "README.md" readme = root / ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL / "README.md"
if readme.is_symlink() or sha256_file(readme) != descriptor["source"]["readmeSha256"]: if readme.is_symlink() or sha256_file(readme) != descriptor["source"]["readmeSha256"]:
die("installed Engine node-intelligence README drift detected") die("installed Engine node-intelligence README drift detected")
return descriptor["source"]["gatewaySha256"] return gateway_sha256
def preflight_engine_node_intelligence_predecessor(payload_dir): def preflight_engine_node_intelligence_predecessor(payload_dir):
@ -4931,6 +5093,178 @@ def preflight_engine_node_intelligence_predecessor(payload_dir):
} }
def validate_engine_l2_closed_loop_recovery_evidence():
backup_dir = BACKUPS_DIR / ENGINE_L2_CLOSED_LOOP_RECOVERY_BACKUP_ID
try:
backup_stat = backup_dir.lstat()
except FileNotFoundError:
die("Engine L2 closed-loop recovery backup is missing")
if stat.S_ISLNK(backup_stat.st_mode) or not stat.S_ISDIR(backup_stat.st_mode):
die("Engine L2 closed-loop recovery backup is unsafe")
backup_names = {
child.name
for child in backup_dir.iterdir()
}
if backup_names != set(ENGINE_L2_CLOSED_LOOP_RECOVERY_BACKUP_SHA256):
die("Engine L2 closed-loop recovery backup file set mismatch")
for name, expected in ENGINE_L2_CLOSED_LOOP_RECOVERY_BACKUP_SHA256.items():
path = backup_dir / name
path_stat = path.lstat()
if (
stat.S_ISLNK(path_stat.st_mode)
or not stat.S_ISREG(path_stat.st_mode)
or sha256_file(path) != expected
):
die(f"Engine L2 closed-loop recovery backup drift detected: {name}")
failed_artifact = FAILED_DIR / ENGINE_L2_CLOSED_LOOP_FAILED_ARTIFACT
try:
failed_stat = failed_artifact.lstat()
except FileNotFoundError:
die("Engine L2 closed-loop failed artifact is missing")
if (
stat.S_ISLNK(failed_stat.st_mode)
or not stat.S_ISREG(failed_stat.st_mode)
or sha256_file(failed_artifact)
!= ENGINE_L2_CLOSED_LOOP_FAILED_ARTIFACT_SHA256
):
die("Engine L2 closed-loop failed artifact evidence mismatch")
try:
state_stat = FAILED_STATE_FILE.lstat()
state_lines = FAILED_STATE_FILE.read_text(encoding="utf-8").splitlines()
except (FileNotFoundError, OSError, UnicodeDecodeError):
die("Engine L2 closed-loop failed journal is unreadable")
if stat.S_ISLNK(state_stat.st_mode) or not stat.S_ISREG(state_stat.st_mode):
die("Engine L2 closed-loop failed journal is unsafe")
records = []
for line in state_lines:
try:
value = json.loads(line)
except json.JSONDecodeError:
die("Engine L2 closed-loop failed journal contains invalid JSON")
if (
isinstance(value, dict)
and value.get("id") == ENGINE_L2_CLOSED_LOOP_FAILED_PATCH_ID
):
records.append(value)
if len(records) != 1:
die("Engine L2 closed-loop failed journal evidence count mismatch")
record = records[0]
if (
record.get("artifact") != ENGINE_L2_CLOSED_LOOP_FAILED_ARTIFACT
or record.get("backup_id") != ENGINE_L2_CLOSED_LOOP_RECOVERY_BACKUP_ID
or record.get("component") != "engine"
or record.get("sha256") != ENGINE_L2_CLOSED_LOOP_FAILED_ARTIFACT_SHA256
or record.get("started_apply") is not True
or record.get("rollback_status") != "not-required"
or record.get("status") != "failed"
or record.get("message")
!= "installed Engine node-intelligence gateway drift detected"
):
die("Engine L2 closed-loop failed journal evidence mismatch")
return backup_dir
def preflight_engine_l2_closed_loop_predecessor(payload_dir):
candidate = validate_engine_l2_closed_loop_payload(
payload_dir,
ENGINE_L2_CLOSED_LOOP_ARTIFACT_ENTRIES,
)
recovery_backup = validate_engine_l2_closed_loop_recovery_evidence()
root = component_root("engine")
actual = collect_exact_files(
root,
ENGINE_L2_CLOSED_LOOP_ARTIFACT_ENTRIES,
"installed Engine L2 closed-loop partial state",
)
if actual != ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256:
changed = sorted(
set(actual) ^ set(ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256)
| {
rel
for rel in set(actual) & set(ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256)
if actual[rel] != ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[rel]
}
)
detail = changed[0] if changed else "unknown"
die(f"Engine L2 closed-loop partial predecessor drift detected: {detail}")
nginx_entries = ("nginx-html/index.html", "nginx-html/assets")
nginx_actual = collect_exact_files(
root,
nginx_entries,
"installed Engine L2 closed-loop nginx publication",
)
nginx_expected = {
"nginx-html/index.html": ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[
"nodedc-source/dist/index.html"
],
"nginx-html/assets/index-Bim2pv1P.css":
ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[
"nodedc-source/dist/assets/index-Bim2pv1P.css"
],
"nginx-html/assets/index-CqvJfRRS.js":
ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[
"nodedc-source/dist/assets/index-CqvJfRRS.js"
],
}
if nginx_actual != nginx_expected:
die("Engine L2 closed-loop nginx partial predecessor drift detected")
installed = current_engine_node_intelligence_descriptor()
if (
installed is None
or installed.get("action") != "activate"
or sha256_file(root / ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL)
!= ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[
ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL
]
):
die("Engine L2 closed-loop partial descriptor mismatch")
expected_candidate = json.loads(json.dumps(installed))
expected_candidate["source"]["gatewaySha256"] = (
ENGINE_L2_CLOSED_LOOP_TARGET_SHA256[
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
]
)
if candidate != expected_candidate:
die("Engine L2 closed-loop descriptor crosses node-intelligence boundary")
validate_installed_engine_node_intelligence_source(
installed,
expected_gateway_sha256=ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
],
)
backend = preflight_engine_credential_backend_runtime(
expected_node_intelligence_gateway_sha256=
ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
],
)
if backend["mode"] != "verified-derived-retry":
die("Engine L2 closed-loop requires the active immutable backend")
app_container_id = engine_compose_service_container_id_for_gateway(
"app",
ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
],
)
return {
"mode": "failed-030-partial-source-reconciliation",
"descriptor": candidate,
"recovery_backup": recovery_backup,
"predecessor_gateway_sha256": installed["source"]["gatewaySha256"],
"partial_gateway_sha256": ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[
ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
],
"target_gateway_sha256": candidate["source"]["gatewaySha256"],
"backend_mode": backend["mode"],
"backend_container_id": backend["container_id"],
"app_container_id": app_container_id,
}
def validate_engine_n8n_staged_release(descriptor): def validate_engine_n8n_staged_release(descriptor):
release_rel = f"releases/n8n-nodes-ndc/{descriptor['releaseId']}" release_rel = f"releases/n8n-nodes-ndc/{descriptor['releaseId']}"
release_dir = N8N_PRIVATE_EXTENSION_RELEASES_ROOT / release_rel release_dir = N8N_PRIVATE_EXTENSION_RELEASES_ROOT / release_rel
@ -5009,6 +5343,7 @@ def load_artifact(artifact, work_dir):
and not is_engine_mcp_control_plane_slice(manifest["component"], entries) and not is_engine_mcp_control_plane_slice(manifest["component"], entries)
and not is_engine_mcp_ontology_sdk_slice(manifest["component"], entries) and not is_engine_mcp_ontology_sdk_slice(manifest["component"], entries)
and not is_engine_mcp_autonomy_provider_v5_slice(manifest["component"], entries) and not is_engine_mcp_autonomy_provider_v5_slice(manifest["component"], entries)
and not is_engine_l2_closed_loop_slice(manifest["component"], entries)
and not is_engine_provider_authority_diagnostics_slice( and not is_engine_provider_authority_diagnostics_slice(
manifest["component"], manifest["component"],
entries, entries,
@ -5023,6 +5358,8 @@ def load_artifact(artifact, work_dir):
validate_engine_mcp_ontology_sdk_payload(payload_dir, entries) validate_engine_mcp_ontology_sdk_payload(payload_dir, entries)
if is_engine_mcp_autonomy_provider_v5_slice(manifest["component"], entries): if is_engine_mcp_autonomy_provider_v5_slice(manifest["component"], entries):
validate_engine_mcp_autonomy_provider_v5_payload(payload_dir, entries) validate_engine_mcp_autonomy_provider_v5_payload(payload_dir, entries)
if is_engine_l2_closed_loop_slice(manifest["component"], entries):
validate_engine_l2_closed_loop_payload(payload_dir, entries)
if is_engine_composite_provider_v4_slice(manifest["component"], entries): if is_engine_composite_provider_v4_slice(manifest["component"], entries):
validate_engine_composite_provider_v4_slice(payload_dir, entries) validate_engine_composite_provider_v4_slice(payload_dir, entries)
if is_engine_provider_rotating_slot_slice(manifest["component"], entries): if is_engine_provider_rotating_slot_slice(manifest["component"], entries):
@ -5089,6 +5426,17 @@ def state_has_patch_id(patch_id):
return any(row.get("id") == patch_id for row in load_state(STATE_FILE)) return any(row.get("id") == patch_id for row in load_state(STATE_FILE))
def reject_terminal_engine_l2_failed_artifact(manifest, sha256):
if (
manifest.get("id") == ENGINE_L2_CLOSED_LOOP_FAILED_PATCH_ID
or sha256 == ENGINE_L2_CLOSED_LOOP_FAILED_ARTIFACT_SHA256
):
die(
"Engine L2 closed-loop 030 is terminal failed; "
"use the exact registered reconciliation successor"
)
class DeployLock: class DeployLock:
def __enter__(self): def __enter__(self):
try: try:
@ -5221,6 +5569,42 @@ def is_engine_agent_full_grant_migration_slice(component, entries):
) )
def is_engine_l2_closed_loop_slice(component, entries):
return (
component == "engine"
and entries is not None
and tuple(entries) == ENGINE_L2_CLOSED_LOOP_ARTIFACT_ENTRIES
)
def collect_exact_files(root, entries, label):
files = {}
for rel in entries:
path = root / rel
try:
path_stat = path.lstat()
except FileNotFoundError:
die(f"{label} path is missing: {rel}")
if stat.S_ISLNK(path_stat.st_mode):
die(f"{label} path is unsafe: {rel}")
if stat.S_ISREG(path_stat.st_mode):
files[rel] = sha256_file(path)
continue
if not stat.S_ISDIR(path_stat.st_mode):
die(f"{label} path is unsafe: {rel}")
for child in sorted(path.rglob("*")):
child_rel = child.relative_to(root).as_posix()
child_stat = child.lstat()
if stat.S_ISLNK(child_stat.st_mode):
die(f"{label} path is unsafe: {child_rel}")
if stat.S_ISDIR(child_stat.st_mode):
continue
if not stat.S_ISREG(child_stat.st_mode):
die(f"{label} path is unsafe: {child_rel}")
files[child_rel] = sha256_file(child)
return files
def collect_engine_data_product_publish_grant_files(root, entries, label): def collect_engine_data_product_publish_grant_files(root, entries, label):
files = {} files = {}
for rel in entries: for rel in entries:
@ -5658,6 +6042,12 @@ def is_platform_provider_catalog_only(entries):
def component_services(component, entries=None): def component_services(component, entries=None):
if is_engine_l2_closed_loop_slice(component, entries):
# The failed 030 apply published both the backend source and the built
# UI before Compose rejected the descriptor/source mismatch. The exact
# reconciliation transition must therefore activate both generations.
return ("nodedc-backend", "app")
if ( if (
is_engine_mcp_control_plane_slice(component, entries) is_engine_mcp_control_plane_slice(component, entries)
or is_engine_mcp_ontology_sdk_slice(component, entries) or is_engine_mcp_ontology_sdk_slice(component, entries)
@ -5830,7 +6220,11 @@ def engine_backend_immutable_runtime_is_current():
) )
def component_compose_files(component, allow_prepared_engine_backend=False): def component_compose_files(
component,
allow_prepared_engine_backend=False,
expected_node_intelligence_gateway_sha256=None,
):
if component == "engine": if component == "engine":
root = component_root(component) root = component_root(component)
files = [root / "docker-compose.yml"] files = [root / "docker-compose.yml"]
@ -5882,9 +6276,16 @@ def component_compose_files(component, allow_prepared_engine_backend=False):
node_intelligence_descriptor is not None node_intelligence_descriptor is not None
and node_intelligence_descriptor["action"] == "activate" and node_intelligence_descriptor["action"] == "activate"
): ):
if expected_node_intelligence_gateway_sha256 is None:
validate_installed_engine_node_intelligence_source( validate_installed_engine_node_intelligence_source(
node_intelligence_descriptor node_intelligence_descriptor
) )
else:
validate_installed_engine_node_intelligence_source(
node_intelligence_descriptor,
expected_gateway_sha256=
expected_node_intelligence_gateway_sha256,
)
files.append(root / ENGINE_NODE_INTELLIGENCE_OVERRIDE_REL) files.append(root / ENGINE_NODE_INTELLIGENCE_OVERRIDE_REL)
return tuple(files) return tuple(files)
files = COMPONENTS[component].get("compose_files", ()) files = COMPONENTS[component].get("compose_files", ())
@ -6275,6 +6676,43 @@ def engine_backend_container_id():
return container_ids[0] return container_ids[0]
def engine_backend_container_id_for_gateway(expected_gateway_sha256):
return engine_compose_service_container_id_for_gateway(
"nodedc-backend",
expected_gateway_sha256,
)
def engine_compose_service_container_id_for_gateway(
service,
expected_gateway_sha256,
):
if service not in ("nodedc-backend", "app"):
die("Engine L2 closed-loop service lookup is not registered")
result = subprocess.run(
[
*compose_base_cmd(
"engine",
expected_node_intelligence_gateway_sha256=
expected_gateway_sha256,
),
"ps",
"-q",
service,
],
cwd=str(component_compose_root("engine")),
check=False,
capture_output=True,
text=True,
)
container_ids = [line.strip() for line in result.stdout.splitlines() if line.strip()]
if (result.returncode != 0
or len(container_ids) != 1
or not re.fullmatch(r"[a-f0-9]{12,64}", container_ids[0])):
die(f"Engine L2 closed-loop service topology mismatch: {service}")
return container_ids[0]
def run_engine_backend_probe(arguments, label, container_id=None, image_ref=None): def run_engine_backend_probe(arguments, label, container_id=None, image_ref=None):
if (container_id is None) == (image_ref is None): if (container_id is None) == (image_ref is None):
die(f"{label} probe target mismatch") die(f"{label} probe target mismatch")
@ -6438,6 +6876,7 @@ def validate_engine_backend_mounts_for_installed_runtime(
def validate_engine_backend_node_intelligence_mounts_for_installed_runtime( def validate_engine_backend_node_intelligence_mounts_for_installed_runtime(
container, container,
node_modules_read_only, node_modules_read_only,
expected_gateway_sha256=None,
): ):
descriptor = current_engine_node_intelligence_descriptor() descriptor = current_engine_node_intelligence_descriptor()
active = descriptor is not None and descriptor["action"] == "activate" active = descriptor is not None and descriptor["action"] == "activate"
@ -6450,7 +6889,13 @@ def validate_engine_backend_node_intelligence_mounts_for_installed_runtime(
node_modules_read_only, node_modules_read_only,
) )
if expected_gateway_sha256 is None:
validate_installed_engine_node_intelligence_source(descriptor) validate_installed_engine_node_intelligence_source(descriptor)
else:
validate_installed_engine_node_intelligence_source(
descriptor,
expected_gateway_sha256=expected_gateway_sha256,
)
mounts = container.get("Mounts") mounts = container.get("Mounts")
if not isinstance(mounts, list): if not isinstance(mounts, list):
die("Engine backend mount inventory is missing") die("Engine backend mount inventory is missing")
@ -6882,7 +7327,9 @@ def ensure_engine_backend_release_image(
return image return image
def preflight_engine_credential_backend_runtime(): def preflight_engine_credential_backend_runtime(
expected_node_intelligence_gateway_sha256=None,
):
base_image_id = inspect_local_image( base_image_id = inspect_local_image(
ENGINE_CREDENTIAL_BACKEND_BASE_IMAGE, ENGINE_CREDENTIAL_BACKEND_BASE_IMAGE,
"Engine backend base image", "Engine backend base image",
@ -6907,7 +7354,13 @@ def preflight_engine_credential_backend_runtime():
or node_modules_stat.st_mode & (stat.S_IWGRP | stat.S_IWOTH)): or node_modules_stat.st_mode & (stat.S_IWGRP | stat.S_IWOTH)):
die("Engine backend node_modules host source is unsafe") die("Engine backend node_modules host source is unsafe")
container_id = engine_backend_container_id() container_id = (
engine_backend_container_id()
if expected_node_intelligence_gateway_sha256 is None
else engine_backend_container_id_for_gateway(
expected_node_intelligence_gateway_sha256
)
)
containers = docker_json(["container", "inspect", container_id], "Engine backend container inspect") containers = docker_json(["container", "inspect", container_id], "Engine backend container inspect")
if not isinstance(containers, list) or len(containers) != 1 or not isinstance(containers[0], dict): if not isinstance(containers, list) or len(containers) != 1 or not isinstance(containers[0], dict):
die("Engine backend container inspect shape mismatch") die("Engine backend container inspect shape mismatch")
@ -6925,6 +7378,7 @@ def preflight_engine_credential_backend_runtime():
validate_engine_backend_node_intelligence_mounts_for_installed_runtime( validate_engine_backend_node_intelligence_mounts_for_installed_runtime(
container, container,
node_modules_read_only=config_image == ENGINE_CREDENTIAL_BACKEND_IMAGE, node_modules_read_only=config_image == ENGINE_CREDENTIAL_BACKEND_IMAGE,
expected_gateway_sha256=expected_node_intelligence_gateway_sha256,
) )
if config_image == ENGINE_CREDENTIAL_BACKEND_BASE_IMAGE and current_image_id == base_image_id: if config_image == ENGINE_CREDENTIAL_BACKEND_BASE_IMAGE and current_image_id == base_image_id:
runtime_presence = { runtime_presence = {
@ -7805,6 +8259,7 @@ def plan_artifact(artifact):
mcp_control_plane_preflight = None mcp_control_plane_preflight = None
mcp_ontology_sdk_preflight = None mcp_ontology_sdk_preflight = None
mcp_autonomy_provider_v5_preflight = None mcp_autonomy_provider_v5_preflight = None
l2_closed_loop_preflight = None
composite_provider_v4_preflight = None composite_provider_v4_preflight = None
provider_rotating_slot_preflight = None provider_rotating_slot_preflight = None
provider_authority_diagnostics_preflight = None provider_authority_diagnostics_preflight = None
@ -7813,6 +8268,7 @@ def plan_artifact(artifact):
provider_catalog_preflight = None provider_catalog_preflight = None
with tempfile.TemporaryDirectory(prefix="plan-", dir=TMP_DIR) as tmp: with tempfile.TemporaryDirectory(prefix="plan-", dir=TMP_DIR) as tmp:
manifest, entries, payload_dir = load_artifact(artifact, Path(tmp)) manifest, entries, payload_dir = load_artifact(artifact, Path(tmp))
reject_terminal_engine_l2_failed_artifact(manifest, sha)
transition_descriptor = None transition_descriptor = None
transition_preflight = None transition_preflight = None
if is_engine_n8n_transition(manifest["component"], entries): if is_engine_n8n_transition(manifest["component"], entries):
@ -7843,6 +8299,10 @@ def plan_artifact(artifact):
mcp_autonomy_provider_v5_preflight = ( mcp_autonomy_provider_v5_preflight = (
preflight_engine_mcp_autonomy_provider_v5_predecessor(payload_dir) preflight_engine_mcp_autonomy_provider_v5_predecessor(payload_dir)
) )
if is_engine_l2_closed_loop_slice(manifest["component"], entries):
l2_closed_loop_preflight = preflight_engine_l2_closed_loop_predecessor(
payload_dir
)
if is_engine_composite_provider_v4_slice(manifest["component"], entries): if is_engine_composite_provider_v4_slice(manifest["component"], entries):
composite_provider_v4_preflight = preflight_engine_composite_provider_v4_predecessor() composite_provider_v4_preflight = preflight_engine_composite_provider_v4_predecessor()
if is_engine_provider_rotating_slot_slice(manifest["component"], entries): if is_engine_provider_rotating_slot_slice(manifest["component"], entries):
@ -7961,6 +8421,65 @@ def plan_artifact(artifact):
print(f"build_root={build_root}") print(f"build_root={build_root}")
print(f"build={' '.join((str(DOCKER),) + tuple(build_args))}") print(f"build={' '.join((str(DOCKER),) + tuple(build_args))}")
print(f"services={' '.join(services)}") print(f"services={' '.join(services)}")
if l2_closed_loop_preflight:
descriptor = l2_closed_loop_preflight["descriptor"]
print(
"engine_l2_transition="
f"{l2_closed_loop_preflight['mode']}"
)
print(
"failed_patch="
f"{ENGINE_L2_CLOSED_LOOP_FAILED_PATCH_ID}"
)
print(
"failed_artifact_sha256="
f"{ENGINE_L2_CLOSED_LOOP_FAILED_ARTIFACT_SHA256}"
)
print(
"recovery_backup="
f"{l2_closed_loop_preflight['recovery_backup'].name}"
)
print(
"predecessor_gateway_sha256="
f"{l2_closed_loop_preflight['predecessor_gateway_sha256']}"
)
print(
"partial_gateway_sha256="
f"{l2_closed_loop_preflight['partial_gateway_sha256']}"
)
print(
"target_gateway_sha256="
f"{l2_closed_loop_preflight['target_gateway_sha256']}"
)
print(
"predecessor_descriptor_sha256="
f"{ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL]}"
)
print(
"target_descriptor_sha256="
f"{ENGINE_L2_CLOSED_LOOP_TARGET_SHA256[ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL]}"
)
print(f"node_intelligence_release={descriptor['releaseId']}")
print(
"backend_current_barrier="
f"{l2_closed_loop_preflight['backend_mode']}"
)
print(
"backend_source_container_id="
f"{l2_closed_loop_preflight['backend_container_id']}"
)
print(
"app_source_container_id="
f"{l2_closed_loop_preflight['app_container_id']}"
)
print("node_intelligence_image=preserved")
print("backend_force_recreate=yes")
print("app_force_recreate=yes")
print("backend_pull=never")
print("n8n_l1_l2_data=preserved")
print("credentials=preserved")
print("databases=preserved")
print("ai_workspace=untouched")
if node_intelligence_preflight: if node_intelligence_preflight:
descriptor = node_intelligence_preflight["descriptor"] descriptor = node_intelligence_preflight["descriptor"]
print(f"node_intelligence_transition={descriptor['action']}") print(f"node_intelligence_transition={descriptor['action']}")
@ -8485,6 +9004,117 @@ def rollback_engine_apply(root, backup_dir, entries, current_stamp, runtime_star
return f"source+runtime-restored:{restored_count}" return f"source+runtime-restored:{restored_count}"
def validate_engine_l2_closed_loop_stable_source(root):
stable_entries = tuple(ENGINE_L2_CLOSED_LOOP_STABLE_SHA256)
actual = collect_exact_files(
root,
stable_entries,
"Engine L2 closed-loop stable rollback",
)
if actual != ENGINE_L2_CLOSED_LOOP_STABLE_SHA256:
changed = sorted(
rel
for rel in ENGINE_L2_CLOSED_LOOP_STABLE_SHA256
if actual.get(rel) != ENGINE_L2_CLOSED_LOOP_STABLE_SHA256[rel]
)
detail = changed[0] if changed else "unknown"
die(f"Engine L2 closed-loop stable rollback drift detected: {detail}")
graph_repository = root / "nodedc-source/server/l2/graphRepository.js"
if graph_repository.exists() or graph_repository.is_symlink():
die("Engine L2 closed-loop stable rollback retained candidate-only repository")
nginx_actual = collect_exact_files(
root,
("nginx-html/index.html", "nginx-html/assets"),
"Engine L2 closed-loop stable nginx rollback",
)
nginx_expected = {
rel.replace("nodedc-source/dist/", "nginx-html/", 1): digest
for rel, digest in ENGINE_L2_CLOSED_LOOP_STABLE_SHA256.items()
if rel.startswith("nodedc-source/dist/")
}
if nginx_actual != nginx_expected:
die("Engine L2 closed-loop stable nginx rollback drift detected")
descriptor = current_engine_node_intelligence_descriptor()
if descriptor is None or descriptor.get("action") != "activate":
die("Engine L2 closed-loop stable descriptor was not restored")
validate_installed_engine_node_intelligence_source(descriptor)
return descriptor
def rollback_engine_l2_closed_loop_reconciliation(
root,
candidate_backup_dir,
entries,
current_stamp,
runtime_started,
applied_services,
):
if tuple(entries) != ENGINE_L2_CLOSED_LOOP_ARTIFACT_ENTRIES:
die("Engine L2 closed-loop rollback entry set mismatch")
if tuple(applied_services) != ("nodedc-backend", "app"):
die("Engine L2 closed-loop rollback service set mismatch")
recovery_backup = validate_engine_l2_closed_loop_recovery_evidence()
candidate_restore_entries = [*entries, "nginx-html"]
validate_backup_partition(
candidate_restore_entries,
read_backup_path_list(candidate_backup_dir / "existing-files.txt"),
read_backup_path_list(candidate_backup_dir / "missing-files.txt"),
"Engine L2 closed-loop candidate rollback",
)
candidate_restored = restore_platform_overlay(
root,
candidate_backup_dir,
candidate_restore_entries,
f"{current_stamp}-l2-candidate",
)
original_entries = [
rel
for rel in ENGINE_L2_CLOSED_LOOP_ARTIFACT_ENTRIES
if rel != ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL
]
original_restore_entries = [*original_entries, "nginx-html"]
validate_backup_partition(
original_restore_entries,
read_backup_path_list(recovery_backup / "existing-files.txt"),
read_backup_path_list(recovery_backup / "missing-files.txt"),
"Engine L2 closed-loop original rollback",
)
original_restored = restore_platform_overlay(
root,
recovery_backup,
original_restore_entries,
f"{current_stamp}-l2-original",
)
validate_engine_l2_closed_loop_stable_source(root)
total_restored = candidate_restored + original_restored
if not runtime_started:
return f"stable-source-restored-runtime-unchanged:{total_restored}"
stable_entries = tuple(ENGINE_L2_CLOSED_LOOP_STABLE_SHA256)
run_component_runtime(
"engine",
stable_entries,
applied_services,
)
for service in applied_services:
healthcheck_compose_service("engine", service)
for check in component_healthchecks(
"engine",
stable_entries,
applied_services,
):
healthcheck_url(check)
backend = preflight_engine_credential_backend_runtime()
if backend["mode"] != "verified-derived-retry":
die("Engine L2 closed-loop stable backend rollback acceptance failed")
return f"stable-source+runtime-restored:{total_restored}"
def inactive_engine_n8n_descriptor(descriptor): def inactive_engine_n8n_descriptor(descriptor):
inactive = dict(descriptor) inactive = dict(descriptor)
inactive["action"] = "rollback-inactive" inactive["action"] = "rollback-inactive"
@ -8791,7 +9421,11 @@ def run_build(component, entries=None):
subprocess.run(cmd, cwd=str(build_root), env=env, check=True) subprocess.run(cmd, cwd=str(build_root), env=env, check=True)
def compose_base_cmd(component, allow_prepared_engine_backend=False): def compose_base_cmd(
component,
allow_prepared_engine_backend=False,
expected_node_intelligence_gateway_sha256=None,
):
cmd = [str(DOCKER), "compose"] cmd = [str(DOCKER), "compose"]
compose_project = component_compose_project(component) compose_project = component_compose_project(component)
if compose_project: if compose_project:
@ -8802,6 +9436,8 @@ def compose_base_cmd(component, allow_prepared_engine_backend=False):
for compose_file in component_compose_files( for compose_file in component_compose_files(
component, component,
allow_prepared_engine_backend=allow_prepared_engine_backend, allow_prepared_engine_backend=allow_prepared_engine_backend,
expected_node_intelligence_gateway_sha256=
expected_node_intelligence_gateway_sha256,
): ):
cmd.extend(["-f", str(compose_file)]) cmd.extend(["-f", str(compose_file)])
return cmd return cmd
@ -9429,9 +10065,83 @@ process.stdout.write('store-v2-full-developer:'+digest);
return result return result
def accept_engine_l2_closed_loop_runtime():
root = component_root("engine")
actual = collect_exact_files(
root,
ENGINE_L2_CLOSED_LOOP_ARTIFACT_ENTRIES,
"Engine L2 closed-loop installed target",
)
if actual != ENGINE_L2_CLOSED_LOOP_TARGET_SHA256:
die("Engine L2 closed-loop installed target digest mismatch")
nginx_actual = collect_exact_files(
root,
("nginx-html/index.html", "nginx-html/assets"),
"Engine L2 closed-loop nginx target",
)
nginx_expected = {
rel.replace("nodedc-source/dist/", "nginx-html/", 1): digest
for rel, digest in ENGINE_L2_CLOSED_LOOP_TARGET_SHA256.items()
if rel.startswith("nodedc-source/dist/")
}
if nginx_actual != nginx_expected:
die("Engine L2 closed-loop nginx target digest mismatch")
descriptor = current_engine_node_intelligence_descriptor()
if descriptor is None or descriptor.get("action") != "activate":
die("Engine L2 closed-loop target descriptor is missing")
validate_installed_engine_node_intelligence_source(descriptor)
script = """
const gateway=await import('file:///app/server/routes/engineAgentGateway.js');
const graph=await import('file:///app/server/l2/graphRepository.js');
const n8n=await import('file:///app/server/routes/n8n.js');
const embedded=await import('file:///app/server/routes/ndcAgentMcp.js');
const required=['engine_plan_l2_patch','engine_apply_l2_patch','engine_deploy_and_run','engine_get_node_output_profile'];
const names=new Set(gateway.engineAgentTools.map((tool)=>tool.name));
if(gateway.ENGINE_AGENT_MCP_VERSION!=='0.7.0'||required.some((name)=>!names.has(name)))throw new Error('mcp-catalog');
if(typeof graph.createL2GraphRepository!=='function'||typeof graph.l2GraphRepository?.commit!=='function')throw new Error('graph-repository');
const profile=n8n.toSafeNodeOutputProfile({id:'acceptance',data:{resultData:{runData:{Probe:[{data:{main:[[{json:{value:7,authorization:'must-not-escape'}}]]}}]}}}},'Probe');
if(!profile?.found||profile.valuesIncluded!==false||JSON.stringify(profile).includes('must-not-escape'))throw new Error('safe-output-profile');
const actor=embedded.graphMutationActor({demoAccess:{internal:true},body:{intent:'Deploy acceptance'},get:(name)=>name.toLowerCase()==='x-ndc-actor-plane'?'external_codex_mcp':''});
if(actor.plane!=='external_codex_mcp'||actor.editedBy!=='engine-agent-mcp')throw new Error('external-actor-plane');
process.stdout.write('engine-l2-closed-loop:0.7.0:cas+safe-profile+external-plane');
""".strip()
result = run_engine_backend_probe(
(
"node",
"--input-type=module",
"-e",
script,
),
"Engine L2 closed-loop live contract",
container_id=engine_backend_container_id(),
)
expected = (
"engine-l2-closed-loop:0.7.0:"
"cas+safe-profile+external-plane"
)
if result != expected:
die("Engine L2 closed-loop live contract acceptance mismatch")
backend = preflight_engine_credential_backend_runtime()
if backend["mode"] != "verified-derived-retry":
die("Engine L2 closed-loop immutable backend acceptance failed")
return result
def run_healthchecks(component, entries=None, services=None): def run_healthchecks(component, entries=None, services=None):
if component == "platform" and entries is not None and is_platform_provider_catalog_only(entries): if component == "platform" and entries is not None and is_platform_provider_catalog_only(entries):
return return
if is_engine_l2_closed_loop_slice(component, entries):
if tuple(services or ()) != ("nodedc-backend", "app"):
die("Engine L2 closed-loop acceptance service set mismatch")
for service in services:
healthcheck_compose_service("engine", service)
for check in component_healthchecks(component, entries, services):
healthcheck_url(check)
accept_engine_l2_closed_loop_runtime()
return
if is_engine_n8n_transition(component, entries): if is_engine_n8n_transition(component, entries):
descriptor = current_engine_n8n_transition_descriptor() descriptor = current_engine_n8n_transition_descriptor()
if descriptor is None: if descriptor is None:
@ -9631,6 +10341,7 @@ def apply_artifact(artifact):
services = None services = None
transition_descriptor = None transition_descriptor = None
node_intelligence_descriptor = None node_intelligence_descriptor = None
l2_closed_loop_preflight = None
node_intelligence_service_stopped = False node_intelligence_service_stopped = False
apply_started = False apply_started = False
engine_backend_recreated = False engine_backend_recreated = False
@ -9643,6 +10354,7 @@ def apply_artifact(artifact):
with tempfile.TemporaryDirectory(prefix=f"apply-{current_stamp}-", dir=TMP_DIR) as tmp: with tempfile.TemporaryDirectory(prefix=f"apply-{current_stamp}-", dir=TMP_DIR) as tmp:
work = Path(tmp) work = Path(tmp)
manifest, entries, payload_dir = load_artifact(artifact, work) manifest, entries, payload_dir = load_artifact(artifact, work)
reject_terminal_engine_l2_failed_artifact(manifest, sha)
patch_id = manifest["id"] patch_id = manifest["id"]
component = manifest["component"] component = manifest["component"]
root = component_root(component) root = component_root(component)
@ -9706,6 +10418,10 @@ def apply_artifact(artifact):
preflight_engine_mcp_ontology_sdk_predecessor(payload_dir) preflight_engine_mcp_ontology_sdk_predecessor(payload_dir)
if is_engine_mcp_autonomy_provider_v5_slice(component, entries): if is_engine_mcp_autonomy_provider_v5_slice(component, entries):
preflight_engine_mcp_autonomy_provider_v5_predecessor(payload_dir) preflight_engine_mcp_autonomy_provider_v5_predecessor(payload_dir)
if is_engine_l2_closed_loop_slice(component, entries):
l2_closed_loop_preflight = (
preflight_engine_l2_closed_loop_predecessor(payload_dir)
)
if is_engine_provider_security_catalog_slice(component, entries): if is_engine_provider_security_catalog_slice(component, entries):
preflight_engine_provider_security_catalog_predecessor() preflight_engine_provider_security_catalog_predecessor()
if not artifact_only and not compose_root.is_dir(): if not artifact_only and not compose_root.is_dir():
@ -9713,7 +10429,14 @@ def apply_artifact(artifact):
compose_root.mkdir(parents=True, exist_ok=True) compose_root.mkdir(parents=True, exist_ok=True)
else: else:
die(f"component compose root not found: {compose_root}") die(f"component compose root not found: {compose_root}")
compose_files = component_compose_files(component) compose_files = component_compose_files(
component,
expected_node_intelligence_gateway_sha256=(
l2_closed_loop_preflight["partial_gateway_sha256"]
if l2_closed_loop_preflight is not None
else None
),
)
if not artifact_only and compose_files: if not artifact_only and compose_files:
for compose_file in compose_files: for compose_file in compose_files:
if not compose_file.is_file(): if not compose_file.is_file():
@ -9803,6 +10526,18 @@ def apply_artifact(artifact):
# candidate start is always eligible for its domain rollback. # candidate start is always eligible for its domain rollback.
runtime_started = bool(services) runtime_started = bool(services)
run_component_runtime(component, entries, services) run_component_runtime(component, entries, services)
if (
l2_closed_loop_preflight is not None
and engine_backend_container_id()
== l2_closed_loop_preflight["backend_container_id"]
):
die("Engine L2 closed-loop backend generation was not recreated")
if (
l2_closed_loop_preflight is not None
and compose_service_container_id("engine", "app")
== l2_closed_loop_preflight["app_container_id"]
):
die("Engine L2 closed-loop app generation was not recreated")
run_healthchecks(component, entries, services) run_healthchecks(component, entries, services)
applied_path = move_artifact(artifact, APPLIED_DIR) applied_path = move_artifact(artifact, APPLIED_DIR)
@ -9880,6 +10615,37 @@ def apply_artifact(artifact):
except Exception as rollback_exc: except Exception as rollback_exc:
rollback_status = f"failed:{type(rollback_exc).__name__}" rollback_status = f"failed:{type(rollback_exc).__name__}"
print("engine-credential-automatic-rollback=failed", file=sys.stderr) print("engine-credential-automatic-rollback=failed", file=sys.stderr)
elif (
l2_closed_loop_preflight is not None
and is_engine_l2_closed_loop_slice(component, entries)
and services is not None
):
try:
restored_state = (
rollback_engine_l2_closed_loop_reconciliation(
root,
backup_dir,
entries,
current_stamp,
runtime_started,
services,
)
)
rollback_status = (
"ok:engine-l2-reconciliation:"
f"{restored_state}"
)
print(
"engine-l2-automatic-rollback="
f"{rollback_status}",
file=sys.stderr,
)
except Exception as rollback_exc:
rollback_status = f"failed:{type(rollback_exc).__name__}"
print(
"engine-l2-automatic-rollback=failed",
file=sys.stderr,
)
elif ( elif (
( (
is_engine_data_product_publish_grant_slice(component, entries) is_engine_data_product_publish_grant_slice(component, entries)

View File

@ -4,10 +4,12 @@ import importlib.util
import json import json
import os import os
from pathlib import Path from pathlib import Path
import shutil
import subprocess import subprocess
import tarfile import tarfile
import tempfile import tempfile
import unittest import unittest
from unittest import mock
SCRIPT_DIR = Path(__file__).resolve().parent SCRIPT_DIR = Path(__file__).resolve().parent
@ -16,6 +18,11 @@ BUILDER_PATH = SCRIPT_DIR / "build-engine-l2-closed-loop-artifact.mjs"
ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA" ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA"
PATCH_ID = "engine-l2-closed-loop-20991231-999" PATCH_ID = "engine-l2-closed-loop-20991231-999"
SOURCE_COMMIT = "dda405cff27977622af0c218abb4d4420c408536" SOURCE_COMMIT = "dda405cff27977622af0c218abb4d4420c408536"
BASELINE_ARTIFACT = (
SCRIPT_DIR.parent
/ "deploy-artifacts"
/ "nodedc-engine-provider-authority-diagnostics-20260723-029.tgz"
)
def load_runner(): def load_runner():
@ -57,6 +64,37 @@ class EngineL2ClosedLoopArtifactTest(unittest.TestCase):
) )
return json.loads(completed.stdout) return json.loads(completed.stdout)
def load_built_payload(self, root):
built = self.build(root / "artifact")
artifact = Path(built["artifact"])
manifest, entries, payload = RUNNER.load_artifact(
artifact,
root / "loaded",
)
return built, manifest, entries, payload
def materialize_partial_predecessor(self, payload, entries, root):
for relative_path in entries:
source = payload / relative_path
destination = root / relative_path
destination.parent.mkdir(parents=True, exist_ok=True)
if source.is_dir():
shutil.copytree(source, destination)
else:
shutil.copy2(source, destination)
with tarfile.open(BASELINE_ARTIFACT, "r:gz") as archive:
member = archive.extractfile(
"payload/nodedc-source/services/node-intelligence/activation.json"
)
self.assertIsNotNone(member)
(root / RUNNER.ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL).write_bytes(
member.read()
)
shutil.copytree(
root / "nodedc-source/dist",
root / "nginx-html",
)
def test_builder_is_commit_bound_and_byte_deterministic(self): def test_builder_is_commit_bound_and_byte_deterministic(self):
with tempfile.TemporaryDirectory(prefix="nodedc-engine-l2-closed-loop-") as directory: with tempfile.TemporaryDirectory(prefix="nodedc-engine-l2-closed-loop-") as directory:
root = Path(directory) root = Path(directory)
@ -74,6 +112,30 @@ class EngineL2ClosedLoopArtifactTest(unittest.TestCase):
self.assertEqual(first["services"], ["nodedc-backend", "app"]) self.assertEqual(first["services"], ["nodedc-backend", "app"])
self.assertEqual(first["mcpVersion"], "0.7.0") self.assertEqual(first["mcpVersion"], "0.7.0")
def test_failed_030_identity_is_terminal_and_cannot_be_retried(self):
with self.assertRaisesRegex(
RUNNER.DeployError,
"terminal failed",
):
RUNNER.reject_terminal_engine_l2_failed_artifact(
{
"id": RUNNER.ENGINE_L2_CLOSED_LOOP_FAILED_PATCH_ID,
},
"0" * 64,
)
with self.assertRaisesRegex(
RUNNER.DeployError,
"terminal failed",
):
RUNNER.reject_terminal_engine_l2_failed_artifact(
{"id": "different-successor"},
RUNNER.ENGINE_L2_CLOSED_LOOP_FAILED_ARTIFACT_SHA256,
)
RUNNER.reject_terminal_engine_l2_failed_artifact(
{"id": "engine-l2-closed-loop-20260723-031"},
"1" * 64,
)
def test_artifact_is_a_safe_exact_engine_overlay(self): def test_artifact_is_a_safe_exact_engine_overlay(self):
with tempfile.TemporaryDirectory(prefix="nodedc-engine-l2-overlay-") as directory: with tempfile.TemporaryDirectory(prefix="nodedc-engine-l2-overlay-") as directory:
root = Path(directory) root = Path(directory)
@ -134,6 +196,192 @@ class EngineL2ClosedLoopArtifactTest(unittest.TestCase):
self.assertTrue(all("/logs/" not in path for path in payload_files)) self.assertTrue(all("/logs/" not in path for path in payload_files))
self.assertTrue(all(not path.endswith(".env") for path in payload_files)) self.assertTrue(all(not path.endswith(".env") for path in payload_files))
def test_preflight_accepts_only_the_exact_failed_030_partial_state(self):
with tempfile.TemporaryDirectory(prefix="nodedc-engine-l2-preflight-") as directory:
workspace = Path(directory)
_built, _manifest, entries, payload = self.load_built_payload(
workspace / "build"
)
live = workspace / "live"
self.materialize_partial_predecessor(payload, entries, live)
recovery = workspace / RUNNER.ENGINE_L2_CLOSED_LOOP_RECOVERY_BACKUP_ID
backend = {
"mode": "verified-derived-retry",
"container_id": "a" * 64,
}
with (
mock.patch.object(RUNNER, "component_root", return_value=live),
mock.patch.object(
RUNNER,
"validate_engine_l2_closed_loop_recovery_evidence",
return_value=recovery,
),
mock.patch.object(
RUNNER,
"validate_installed_engine_node_intelligence_source",
) as validate_source,
mock.patch.object(
RUNNER,
"preflight_engine_credential_backend_runtime",
return_value=backend,
) as validate_backend,
mock.patch.object(
RUNNER,
"engine_compose_service_container_id_for_gateway",
return_value="b" * 64,
) as app_lookup,
):
result = RUNNER.preflight_engine_l2_closed_loop_predecessor(
payload
)
self.assertEqual(
result["mode"],
"failed-030-partial-source-reconciliation",
)
self.assertEqual(result["backend_container_id"], "a" * 64)
self.assertEqual(result["app_container_id"], "b" * 64)
self.assertEqual(
result["target_gateway_sha256"],
RUNNER.ENGINE_L2_CLOSED_LOOP_TARGET_SHA256[
RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
],
)
self.assertEqual(
validate_source.call_args.kwargs[
"expected_gateway_sha256"
],
RUNNER.ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[
RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
],
)
self.assertEqual(
validate_backend.call_args.kwargs[
"expected_node_intelligence_gateway_sha256"
],
RUNNER.ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[
RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
],
)
app_lookup.assert_called_once_with(
"app",
RUNNER.ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[
RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
],
)
index_path = live / "nodedc-source/dist/index.html"
index_path.write_bytes(index_path.read_bytes() + b"\n")
with (
mock.patch.object(RUNNER, "component_root", return_value=live),
mock.patch.object(
RUNNER,
"validate_engine_l2_closed_loop_recovery_evidence",
return_value=recovery,
),
mock.patch.object(
RUNNER,
"preflight_engine_credential_backend_runtime",
) as unreachable_backend,
):
with self.assertRaisesRegex(
RUNNER.DeployError,
"partial predecessor drift",
):
RUNNER.preflight_engine_l2_closed_loop_predecessor(payload)
unreachable_backend.assert_not_called()
def test_reconciliation_rollback_uses_failed_030_backup_and_restores_runtime(self):
entries = RUNNER.ENGINE_L2_CLOSED_LOOP_ARTIFACT_ENTRIES
services = ("nodedc-backend", "app")
with tempfile.TemporaryDirectory(prefix="nodedc-engine-l2-rollback-") as directory:
root = Path(directory)
candidate = root / "candidate-backup"
recovery = root / "recovery-backup"
candidate_entries = [*entries, "nginx-html"]
original_entries = [
rel
for rel in entries
if rel != RUNNER.ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL
]
original_entries.append("nginx-html")
def backup_paths(path):
if path.parent == candidate:
return candidate_entries if path.name == "existing-files.txt" else []
if path.parent == recovery:
if path.name == "existing-files.txt":
return [
rel
for rel in original_entries
if rel != "nodedc-source/server/l2/graphRepository.js"
]
return ["nodedc-source/server/l2/graphRepository.js"]
raise AssertionError(path)
with (
mock.patch.object(
RUNNER,
"validate_engine_l2_closed_loop_recovery_evidence",
return_value=recovery,
),
mock.patch.object(
RUNNER,
"read_backup_path_list",
side_effect=backup_paths,
),
mock.patch.object(
RUNNER,
"restore_platform_overlay",
side_effect=(len(candidate_entries), len(original_entries)),
) as restore,
mock.patch.object(
RUNNER,
"validate_engine_l2_closed_loop_stable_source",
) as validate_stable,
mock.patch.object(RUNNER, "run_component_runtime") as run_runtime,
mock.patch.object(RUNNER, "healthcheck_compose_service") as check_service,
mock.patch.object(
RUNNER,
"component_healthchecks",
return_value=(
"http://127.0.0.1:8080/",
"http://127.0.0.1:3001/health",
),
),
mock.patch.object(RUNNER, "healthcheck_url") as check_url,
mock.patch.object(
RUNNER,
"preflight_engine_credential_backend_runtime",
return_value={"mode": "verified-derived-retry"},
),
):
result = RUNNER.rollback_engine_l2_closed_loop_reconciliation(
root,
candidate,
entries,
"20991231-235959",
True,
services,
)
self.assertTrue(result.startswith("stable-source+runtime-restored:"))
self.assertEqual(
[call.args[1] for call in restore.call_args_list],
[candidate, recovery],
)
validate_stable.assert_called_once_with(root)
run_runtime.assert_called_once_with(
"engine",
tuple(RUNNER.ENGINE_L2_CLOSED_LOOP_STABLE_SHA256),
services,
)
self.assertEqual(
[call.args for call in check_service.call_args_list],
[("engine", "nodedc-backend"), ("engine", "app")],
)
self.assertEqual(check_url.call_count, 2)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main(verbosity=2) unittest.main(verbosity=2)

View File

@ -33,6 +33,20 @@ RUNNER = load_runner()
class EngineProviderTargetHostPolicyTest(unittest.TestCase): class EngineProviderTargetHostPolicyTest(unittest.TestCase):
def require_historical_target_source(self):
for relative_path, expected in (
RUNNER.ENGINE_PROVIDER_TARGET_HOST_POLICY_TARGET_SHA256.items()
):
path = ENGINE_ROOT / relative_path
if (
not path.is_file()
or hashlib.sha256(path.read_bytes()).hexdigest() != expected
):
self.skipTest(
"historical target-host policy source has advanced "
"to its exact successor"
)
def build(self, artifact_dir): def build(self, artifact_dir):
environment = os.environ.copy() environment = os.environ.copy()
environment["NODEDC_ENGINE_SOURCE_ROOT"] = str(ENGINE_ROOT) environment["NODEDC_ENGINE_SOURCE_ROOT"] = str(ENGINE_ROOT)
@ -47,6 +61,7 @@ class EngineProviderTargetHostPolicyTest(unittest.TestCase):
return json.loads(completed.stdout) return json.loads(completed.stdout)
def test_builder_emits_exact_deterministic_one_file_slice(self): def test_builder_emits_exact_deterministic_one_file_slice(self):
self.require_historical_target_source()
with tempfile.TemporaryDirectory(prefix="nodedc-provider-target-host-") as directory: with tempfile.TemporaryDirectory(prefix="nodedc-provider-target-host-") as directory:
root = Path(directory) root = Path(directory)
first = self.build(root / "first") first = self.build(root / "first")
@ -126,6 +141,7 @@ class EngineProviderTargetHostPolicyTest(unittest.TestCase):
acceptance.assert_called_once_with() acceptance.assert_called_once_with()
def test_live_acceptance_uses_exact_literal_host_contract(self): def test_live_acceptance_uses_exact_literal_host_contract(self):
self.require_historical_target_source()
expected_live = "engine-provider-target-host-policy:exact-literal-host:v1" expected_live = "engine-provider-target-host-policy:exact-literal-host:v1"
with ( with (
mock.patch.object(RUNNER, "component_root", return_value=ENGINE_ROOT), mock.patch.object(RUNNER, "component_root", return_value=ENGINE_ROOT),